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.
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.
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:
{"data":{"id":120,"title":"Design kit","visibility":"HIDDEN","variants":[]}}{"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
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.