Advanced: subscription checkout
Create Design kit membership, prepare webhook delivery, and take Maya from product creation to subscription checkout.
Take one purchase all the way through: Maya Chen buys Design kit membership
for $19.99 per month. You will set up webhooks, create the product, and get
a checkout link. The commands carry returned IDs forward automatically;
the illustrative responses use product 120, variant 4321, order
9001, and SellApp subscription 55. Your IDs will differ.
1. Prepare a dedicated store
There is no separate SellApp API sandbox. These requests create real records and payment-provider objects. Use a dedicated store, a configured Stripe connection that supports subscriptions, and provider test credentials only where your store's integration supports them. Do not complete a real payment unless you intend to pay.
You need Bash, cURL for HTTP requests, jq for reading JSON, and a key with listing, invoice, and webhook
abilities. Subscription lifecycle requests also use invoice, not a separate
subscription ability. The account needs the corresponding store permissions.
Before continuing, deploy a receiver at a public HTTPS URL, configure the store's signing secret in Developer settings, and securely give the same secret to the receiver. Follow the crash-safe receiver flow. Keep secret keys out of browser code and source control.
set -euo pipefail
export SELLAPP_API_BASE_URL='https://sell.app/api'
export SELLAPP_API_KEY='replace-me'
export SELLAPP_STORE='launch-lab'
export SELLAPP_WEBHOOK_URL='https://your-server.example/sellapp/webhooks'
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'Replace replace-me with your key, launch-lab with your store slug, and the
.example webhook URL with your own reachable endpoint. Expect 200 from the
read request; an empty array is valid. Response headers go to stderr so you can
retain X-Request-ID without corrupting the JSON output.
2. Register and test the webhook first
Do this before creating an order or opening checkout, otherwise you can miss the events you want to observe. This creates a new channel; if the URL already has a channel, retrieve it and update its filter instead.
channel_json=$(api POST /v2/webhook-channels \
--header 'Content-Type: application/json' \
--data "$(jq -nc --arg url "$SELLAPP_WEBHOOK_URL" '{
name: "Design kit receiver",
url: $url,
allowed_notifications: ["order.created", "order.paid", "order.completed", "subscription.created"]
}')")
SELLAPP_WEBHOOK_CHANNEL_ID=$(jq -er '.data.id' <<< "$channel_json")
jq -e '.data.signing_secret_configured == true' <<< "$channel_json"
api POST "/v2/webhook-channels/${SELLAPP_WEBHOOK_CHANNEL_ID}/test" \
--header 'Content-Type: application/json' \
--data '{"event":"order.created"}' | jq -e '.data.status == "delivered"'The channel response is 201. The test is 200 with data.status: "delivered".
Confirm the receiver durably stored the test payload and pending work.
Test payload IDs are synthetic and will not match the sale below.
3. 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 membership",
"description":"New design templates each month.",
"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":"Monthly 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 membership","visibility":"HIDDEN","variants":[]}}{"data":{"id":4321,"product_id":120,"title":"Monthly membership","payment_methods":["STRIPE"]}}4. 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.
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 order with the initial one-time price by accident. The MANUAL
deliverable means your team or integration must give the customer design files;
a successful API response does not do that work for you.
5. Create Maya's order and checkout session
order_json=$(api POST /v2/orders \
--header 'Idempotency-Key: launch-lab-maya-order-001' \
--header 'Content-Type: application/json' \
--data "$(jq -nc --arg variant "$SELLAPP_VARIANT_ID" '{
customer_email:"maya@example.com",
payment_method:"STRIPE",
product_variants:{($variant):{quantity:1}}
}')")
SELLAPP_ORDER_ID=$(jq -er '.data.id' <<< "$order_json")
checkout_json=$(api POST "/v2/orders/${SELLAPP_ORDER_ID}/checkout" \
--header 'Idempotency-Key: launch-lab-maya-checkout-001')
SELLAPP_CHECKOUT_URL=$(jq -er '.data.payment.checkout_url' <<< "$checkout_json")
printf '%s\n' "$SELLAPP_CHECKOUT_URL"Order creation returns 201; checkout returns 201 for a new payment session
or 200 when reusing an existing session. Both return an order
under data and include data.payment.checkout_url when a URL is available.
{"data":{"id":9001,"status":"PENDING","customer":{"email":"maya@example.com"},"payment":{"checkout_url":"https://checkout.stripe.com/c/pay/cs_example"},"totals":{"currency":"USD","total_cents":1999}}}{"data":{"id":9001,"status":"PENDING","payment":{"checkout_url":"https://checkout.stripe.com/c/pay/cs_example"}}}Open the URL printed by your request, not the illustrative URL above. Completing checkout can charge the configured payment method and start recurring billing. Use a unique idempotency key for each intended order and a separate key for checkout. If a response is lost, retry the same operation with the same key and identical body. Do not reuse these illustrative keys for another purchase. See idempotency for retention and conflicts.
6. Verify payment, completion, and subscription
A selected portion of the signed order.completed payload can look like this:
{
"id":"01992a65-e064-71ba-b38f-902b7966a6be",
"created_at":"2026-08-30T12:05:00.000000Z",
"event":"order.completed",
"version":"1",
"store":1,
"data":{"id":9001,"subscription_id":55,"customer_information":{"email":"maya@example.com"}}
}The top-level id identifies the delivery; data.id identifies the order.
Match data.id with SELLAPP_ORDER_ID. Events can arrive out of order, and
the subscription may be linked to the purchase a little later. Retrieve the order again:
api GET "/v2/orders/${SELLAPP_ORDER_ID}" \
| jq '.data | {id, customer, status, line_items}'order.paid means the order entered the paid state and fulfillment was queued.
order.completed means SellApp entered its completed order state; it does not
prove your receiver's work or every external notification has finished.
Track processed delivery IDs so a repeated event does not repeat the work.
Also make granting access safe to retry for the same order and variant.
Find the purchased variant in data.line_items by product_variant_id.
After its subscription_id becomes available, use that integer SellApp ID
(55 here), not a provider ID like sub_..., in
the subscription lifecycle walkthrough. Review and
cancel disposable recurring test subscriptions so they do not renew.