Guides

Sell a product

Create a hidden product, add a variant, and configure a recurring 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 Founder memo circle, with a $19.99 monthly membership purchased by Natya 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 subscriptions configured. No payment is collected by these catalog requests, but recurring pricing can create provider objects.

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_SLUG='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_SLUG}" \
    --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 Founder memo circle
product_json=$(api POST /v2/products \
  --header 'Content-Type: application/json' \
  --data '{
    "title":"Founder memo circle",
    "description":"A monthly collection of annotated operating memos.",
    "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":"Monthly membership",
    "description":"One operating memo each month; access is provisioned by our team.",
    "deliverable":{"types":["MANUAL"],"data":{"stock":null,"comment":"We will send your reading-room invitation."}},
    "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":"Founder memo circle","visibility":"HIDDEN","variants":[]}}
Variant response — abbreviated
{"data":{"id":4321,"product_id":120,"title":"Monthly membership","payment_methods":["STRIPE"]}}

3. Configure monthly recurring pricing

The variant starts with a one-time price. This next request changes it to a monthly subscription. USD amounts use cents: send 1999 for $19.99, not 19.99.

Set the recurring price before checkout
api PUT "/v2/products/${SELLAPP_PRODUCT_ID}/variants/${SELLAPP_VARIANT_ID}/pricing" \
  --header 'Content-Type: application/json' \
  --data '{
    "pricing":{
      "type":"SUBSCRIPTION",
      "humble":false,
      "price":{"price":1999,"currency":"USD"},
      "frequency":{"value":1,"interval":"MONTH"}
    },
    "payment_methods":["STRIPE"]
  }' | jq -e '.data.pricing.type == "SUBSCRIPTION"'

Expect 200. Resolve provider/configuration errors before continuing; do not create the invoice with the initial one-time price by accident. The MANUAL deliverable means your team or integration must give the customer reading-room access; a successful API response does not do that work for you.

4. 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 invoice creation. For all steps in one page, use the complete quickstart.

On this page