Guides

Sell a product

Create a hidden product, add a variant, and set a one-time price.

A product is what the customer sees; a variant is the option they buy. The product holds the title and description, while each variant sets the price, payment methods, stock, and delivery. This example creates Design kit, with a $19.99 one-time purchase purchased by Maya in the checkout walkthrough.

1. Prepare credentials

Use Bash, cURL, and jq. Replace the key and store slug below. Your token needs the listing ability and corresponding store permissions. These are real records, not sandbox data; use a dedicated store with Stripe configured. No payment is collected by these catalog requests.

Run once in the same Bash session
set -euo pipefail
export SELLAPP_API_BASE_URL='https://sell.app/api'
export SELLAPP_API_KEY='replace-me'
export SELLAPP_STORE='launch-lab'

api() {
  local method="$1" path="$2"
  shift 2
  curl --silent --show-error --fail-with-body \
    --request "$method" --url "${SELLAPP_API_BASE_URL}${path}" \
    --header "Authorization: Bearer ${SELLAPP_API_KEY}" \
    --header "X-STORE: ${SELLAPP_STORE}" \
    --header 'Accept: application/json' \
    --dump-header /dev/stderr "$@"
}

api GET '/v2/products?limit=1' | jq '.data'

2. Create the product and variant

HIDDEN keeps the product off normal storefront listings; it is not an is_draft flag or a security boundary. The API rejects writes to is_draft. Do not share checkout links until setup is complete.

Create Design kit
product_json=$(api POST /v2/products \
  --header 'Content-Type: application/json' \
  --data '{
    "title":"Design kit",
    "description":"Templates for your next project.",
    "visibility":"HIDDEN"
  }')
SELLAPP_PRODUCT_ID=$(jq -er '.data.id' <<< "$product_json")

variant_json=$(api POST "/v2/products/${SELLAPP_PRODUCT_ID}/variants" \
  --header 'Content-Type: application/json' \
  --data '{
    "title":"Standard",
    "description":"Design files delivered by our team.",
    "deliverable":{"types":["MANUAL"],"data":{"stock":null,"comment":"We will send your design files."}},
    "pricing":{"humble":false,"price":{"price":1999,"currency":"USD"}},
    "payment_methods":["STRIPE"]
  }')
SELLAPP_VARIANT_ID=$(jq -er '.data.id' <<< "$variant_json")

Both requests return 201. Selected fields from their responses:

Product response — abbreviated
{"data":{"id":120,"title":"Design kit","visibility":"HIDDEN","variants":[]}}
Variant response — abbreviated
{"data":{"id":4321,"product_id":120,"title":"Standard","payment_methods":["STRIPE"]}}

USD amounts use cents: send 1999 for $19.99, not 19.99. The variant starts with a one-time price. The MANUAL deliverable means your team or integration must send the design files; a successful API response does not do that work for you.

3. Verify the result

Retrieve the product and variant
api GET "/v2/products/${SELLAPP_PRODUCT_ID}" | jq '.data | {id, title, visibility}'
api GET "/v2/products/${SELLAPP_PRODUCT_ID}/variants/${SELLAPP_VARIANT_ID}" \
  | jq '.data | {id, title, pricing, payment_methods}'

Keep the IDs from your responses; 120 and 4321 are examples, not test resources you can reuse. Send the most recently read updated_at as expected_updated_at when updating the product to catch changes made since you last read it. Sensitive deliverables are hidden or write-only, so keep your own secure copy; a read response is not a backup.

Creation has no general idempotency key. After a lost response, check whether the product or variant was created before repeating a POST. Keep the product hidden until you intend to list it, then change visibility deliberately.

Next, create a checkout, registering and testing your webhook before order creation. For the full subscription integration, use the advanced walkthrough.

On this page