Products
- 5 endpoints
List products#
GET /products
Lists the products of the caller's space, newest first: the listed ones, or every one with includeHidden=true (deleted products never); with subscription, only the subscription products (true) or the one-time ones (false); with reference, the product with that reference (hidden or not): 0 or 1 item.
- Auth
- X-API-Key: any keyA member's key needs the members.products policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl https://api.qbitflow.app/v2/products \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Product ({ items, hasMore, nextCursor? })// iterate walks every page (list returns one); hidden products only with includeHidden.for await (const product of client.products.iterate()) { console. log( `${product. reference}: ${product.name}, ${product.priceUsd} USD`);}# iterate walks every page (list returns one); hidden products only with include_hidden.for product in client.products.iterate(): print(product.reference, product.name, product.price_usd, "USD")// Iterate walks every page (List returns one); hidden products only with IncludeHidden.for p, err := range client.Products.Iterate(ctx, nil) { if err != nil { log.Fatal(err) } fmt.Printf("%s %q: %s USD (%s)\n", p.UUID, p.Name, p.PriceUSD, p.Reference)}// iterate() walks every page (list() returns one); hidden products only with includeHidden.foreach ($client->products->iterate() as $product) { echo "{$product->reference} {$product->name}: {$product->priceUsd} USD\n";}Also: Act in a member's space
Query parameters
limitintegerOptionalPage size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)
cursorstringOptionalThe previous page's nextCursor, verbatim; omit for the first page
max=200includeHiddenbooleanOptionaltrue lists the hidden products too (isActive false)
subscriptionbooleanOptionaltrue: only the subscription products; false: only the one-time ones
referencestringOptionalOnly the product with this reference (hidden ones included): a list of 0 or 1 product
referencemax=100
Response
A page: { "items": Product[], "hasMore": …, "nextCursor"?: … }. Pass nextCursor back as cursor for the next page; it is absent when hasMore is false.
12 fields of each Product
uuidstringIts only id (a UUIDv7)
namestringdescriptionstringAbsent without one
priceUsdstringThe price, in USD (a decimal string, e.g. "12.5"): what a checkout of the product charges, before its fees
createdAtstringisActivebooleanfalse for a hidden product: out of GET /products (unless includeHidden), still readable and editable by uuid. Hidden are the inline products of subscription checkouts, those of a removed member or a closed organization, and those hidden with PATCH isActive=false
referencestringOptionalReference is the merchant's (unique per owner and mode). Only for the merchant: never in the public reads (a checkout, GET /hosted/transactions/:txUuid)
Only for the resource's owner
metadatamapOptionalMetadata is the merchant's own data on the product (naming: 50 keys, keys of 1–40 characters, values of at most 500). Always present ({} when empty); only for the merchant. Never copied onto the checkouts, payments or subscriptions of the product
Only for the resource's owner
subscriptionSubscriptionTermsOptionalSubscription are its subscription terms: a subscription product (its payment link opens a subscription checkout, and its subscription checkouts take these terms by default). Omitted for a one-time product
3 fields of
SubscriptionTermsfrequencyDurationHow often it is billed (e.g. {"value": 1, "unit": "months"}: 30 days)
2 fields of
DurationvalueintegerNumber of units; 0 means no duration
unitstringRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
trialPeriodDurationOptionalA free trial before the first bill; omitted without one
2 fields of
DurationvalueintegerNumber of units; 0 means no duration
unitstringRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
minPeriodsintegerOptionalThe periods the customer commits to before they can cancel; omitted without a minimum
paymentLinkstringOptionalPaymentLink is its shareable link: opening it starts a checkout of the product (a subscription one with its terms, else a one-time payment) and goes to the checkout page. Only for the merchant
Only for the resource's owner
testbooleanWhether this is test data (testnets) or live data
userUuidstringOptionalMember the resource belongs to; omitted for the organization's own resources
Only for the resource's owner
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Create product#
POST /products
Creates a product in the caller's space. A member gives it subscription terms only while the members.subscriptions policy allows them (403 policy_disabled): a subscription product sells subscriptions through its link.
- Auth
- X-API-Key: any keyA member's key needs the members.products policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Accepted: send one per operation
- Success
- 201
request.sh
curl https://api.qbitflow.app/v2/products \ -H "X-API-Key: $QBITFLOW_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "T-shirt", "description": "Blue, size M", "priceUsd": "4.99", "reference": "tshirt-blue-m" }'# 201: a Productconst product = await client.products.create({ name: 'T-shirt', description: 'Blue, size M', priceUsd: '4.99', // USD, a decimal string, never a float reference: 'tshirt-blue-m', // your own id: unique per space});console.log(`product ${product.uuid}, payment link ${product.paymentLink}`);product = client.products.create( name="T-shirt", description="Blue, size M", price_usd="4.99", # USD, a decimal string: never a float reference="tshirt-blue-m", # your own id: unique per space)print(product.uuid, product.payment_link)product, err := client.Products.Create(ctx, &qbitflow.CreateProductParams{ Name: "T-shirt", Description: "Blue, size M", PriceUSD: "4.99", // a decimal string, never a float Reference: "tshirt-blue-m", // your own id: unique per space})if err != nil { log.Fatal(err)}fmt.Println("created", product.UUID, "payment link:", product.PaymentLink)$product = $client->products->create(new \QBitFlow\Params\CreateProductParams( name: 'T-shirt', priceUsd: '4.99', // a decimal string, never a float description: 'Blue, size M', reference: 'tshirt-blue-m', // your own reference, unique per space));echo "Created {$product->uuid}, payment link {$product->paymentLink}\n";Body
namestringRequirednamemin=2max=100descriptionstringOptionalOptional
textmin=2max=500priceUsdnumberRequiredThe price in USD, a string ("12.50"): above 0, at most 2 decimals, at most 1,000,000; at most 5 in test mode (details.max); a subscription product's above 0.25 in live mode (details.min)
usd=1000000referencestringOptionalOptional merchant reference, unique per owner and mode. The product's uuid if omitted
referencemax=100subscriptionSubscriptionTermsDtoOptionalSubscription makes it a subscription product: its terms (frequency required). Omitted: a one-time product
3 fields of
SubscriptionTermsDtofrequencyDurationOptionalHow often it is billed: at least 1 hour (5 minutes in test mode), at most 1 year
2 fields of
DurationvalueintegerOptionalNumber of units; 0 means no duration
unitstringOptionalRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
trialPeriodDurationOptionalA free trial before the first bill ({"value": 0}: none, removing a product's)
2 fields of
DurationvalueintegerOptionalNumber of units; 0 means no duration
unitstringOptionalRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
minPeriodsintegerOptionalThe periods the customer commits to before they can cancel (0: none, removing a product's), at most 1000
max=1000
metadatamapOptionalMetadata is the merchant's own data (optional): at most 50 keys of 1–40 characters (letters, digits, _ - . :), values of at most 500 characters; a key set to "" isn't stored
metadata
Response
A Product.
12 fields of Product
uuidstringIts only id (a UUIDv7)
namestringdescriptionstringAbsent without one
priceUsdstringThe price, in USD (a decimal string, e.g. "12.5"): what a checkout of the product charges, before its fees
createdAtstringisActivebooleanfalse for a hidden product: out of GET /products (unless includeHidden), still readable and editable by uuid. Hidden are the inline products of subscription checkouts, those of a removed member or a closed organization, and those hidden with PATCH isActive=false
referencestringOptionalReference is the merchant's (unique per owner and mode). Only for the merchant: never in the public reads (a checkout, GET /hosted/transactions/:txUuid)
Only for the resource's owner
metadatamapOptionalMetadata is the merchant's own data on the product (naming: 50 keys, keys of 1–40 characters, values of at most 500). Always present ({} when empty); only for the merchant. Never copied onto the checkouts, payments or subscriptions of the product
Only for the resource's owner
subscriptionSubscriptionTermsOptionalSubscription are its subscription terms: a subscription product (its payment link opens a subscription checkout, and its subscription checkouts take these terms by default). Omitted for a one-time product
3 fields of
SubscriptionTermsfrequencyDurationHow often it is billed (e.g. {"value": 1, "unit": "months"}: 30 days)
2 fields of
DurationvalueintegerNumber of units; 0 means no duration
unitstringRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
trialPeriodDurationOptionalA free trial before the first bill; omitted without one
2 fields of
DurationvalueintegerNumber of units; 0 means no duration
unitstringRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
minPeriodsintegerOptionalThe periods the customer commits to before they can cancel; omitted without a minimum
paymentLinkstringOptionalPaymentLink is its shareable link: opening it starts a checkout of the product (a subscription one with its terms, else a one-time payment) and goes to the checkout page. Only for the merchant
Only for the resource's owner
testbooleanWhether this is test data (testnets) or live data
userUuidstringOptionalMember the resource belongs to; omitted for the organization's own resources
Only for the resource's owner
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
- 409idempotency_
key_ in_ use the
Idempotency-Key's first request is still running (idempotency): retry once it answered - 422idempotency_
key_ reused the
Idempotency-Keywas used for another request (idempotency): send a new key - 413request_
too_ large an
Idempotentroute's body over 1 MiB (idempotency)
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Get product#
GET /products/
A product of the caller's space by its uuid (hidden products included).
- Auth
- X-API-Key: any keyA member's key needs the members.products policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl "https://api.qbitflow.app/v2/products/$PRODUCT_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a ProductPath parameters
uuidstringRequired
Response
A Product.
12 fields of Product
uuidstringIts only id (a UUIDv7)
namestringdescriptionstringAbsent without one
priceUsdstringThe price, in USD (a decimal string, e.g. "12.5"): what a checkout of the product charges, before its fees
createdAtstringisActivebooleanfalse for a hidden product: out of GET /products (unless includeHidden), still readable and editable by uuid. Hidden are the inline products of subscription checkouts, those of a removed member or a closed organization, and those hidden with PATCH isActive=false
referencestringOptionalReference is the merchant's (unique per owner and mode). Only for the merchant: never in the public reads (a checkout, GET /hosted/transactions/:txUuid)
Only for the resource's owner
metadatamapOptionalMetadata is the merchant's own data on the product (naming: 50 keys, keys of 1–40 characters, values of at most 500). Always present ({} when empty); only for the merchant. Never copied onto the checkouts, payments or subscriptions of the product
Only for the resource's owner
subscriptionSubscriptionTermsOptionalSubscription are its subscription terms: a subscription product (its payment link opens a subscription checkout, and its subscription checkouts take these terms by default). Omitted for a one-time product
3 fields of
SubscriptionTermsfrequencyDurationHow often it is billed (e.g. {"value": 1, "unit": "months"}: 30 days)
2 fields of
DurationvalueintegerNumber of units; 0 means no duration
unitstringRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
trialPeriodDurationOptionalA free trial before the first bill; omitted without one
2 fields of
DurationvalueintegerNumber of units; 0 means no duration
unitstringRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
minPeriodsintegerOptionalThe periods the customer commits to before they can cancel; omitted without a minimum
paymentLinkstringOptionalPaymentLink is its shareable link: opening it starts a checkout of the product (a subscription one with its terms, else a one-time payment) and goes to the checkout page. Only for the merchant
Only for the resource's owner
testbooleanWhether this is test data (testnets) or live data
userUuidstringOptionalMember the resource belongs to; omitted for the organization's own resources
Only for the resource's owner
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Update product#
PATCH /products/
Updates a product of the caller's space: only the fields sent (metadata merges). Setting subscription terms follows the members.subscriptions policy, as at creation; removing them never does.
- Auth
- X-API-Key: any keyA member's key needs the members.products policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl -X PATCH "https://api.qbitflow.app/v2/products/$PRODUCT_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "isActive": false }'# 200: a ProductPath parameters
uuidstringRequired
Body
namestringOptionalnamemin=2max=100descriptionstringOptional2 to 500 characters, the text rule; "" clears it
max=500len=0|min=2len=0|textpriceUsdnumberOptionalThe new price, in USD, a string (as at creation): for new checkouts and subscribers only (a subscription keeps the price it was subscribed at)
usd=1000000isActivebooleanOptionalfalse hides the product from GET /products, true lists it again
metadatamapOptionalMetadata updates the merchant's data by merging: a key set to a value sets it, a key set to "" deletes it, keys not named are kept (absent: unchanged). At most 50 keys after the merge
metadatasubscriptionSubscriptionTermsDtoOptionalSubscription sets its subscription terms: each field given replaces the current one (a one-time product needs frequency). For new subscribers only: a running subscription keeps its terms
3 fields of
SubscriptionTermsDtofrequencyDurationOptionalHow often it is billed: at least 1 hour (5 minutes in test mode), at most 1 year
2 fields of
DurationvalueintegerOptionalNumber of units; 0 means no duration
unitstringOptionalRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
trialPeriodDurationOptionalA free trial before the first bill ({"value": 0}: none, removing a product's)
2 fields of
DurationvalueintegerOptionalNumber of units; 0 means no duration
unitstringOptionalRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
minPeriodsintegerOptionalThe periods the customer commits to before they can cancel (0: none, removing a product's), at most 1000
max=1000
removeSubscriptionbooleanOptionalRemoveSubscription makes it a one-time product again (not with subscription)
Response
A Product.
12 fields of Product
uuidstringIts only id (a UUIDv7)
namestringdescriptionstringAbsent without one
priceUsdstringThe price, in USD (a decimal string, e.g. "12.5"): what a checkout of the product charges, before its fees
createdAtstringisActivebooleanfalse for a hidden product: out of GET /products (unless includeHidden), still readable and editable by uuid. Hidden are the inline products of subscription checkouts, those of a removed member or a closed organization, and those hidden with PATCH isActive=false
referencestringOptionalReference is the merchant's (unique per owner and mode). Only for the merchant: never in the public reads (a checkout, GET /hosted/transactions/:txUuid)
Only for the resource's owner
metadatamapOptionalMetadata is the merchant's own data on the product (naming: 50 keys, keys of 1–40 characters, values of at most 500). Always present ({} when empty); only for the merchant. Never copied onto the checkouts, payments or subscriptions of the product
Only for the resource's owner
subscriptionSubscriptionTermsOptionalSubscription are its subscription terms: a subscription product (its payment link opens a subscription checkout, and its subscription checkouts take these terms by default). Omitted for a one-time product
3 fields of
SubscriptionTermsfrequencyDurationHow often it is billed (e.g. {"value": 1, "unit": "months"}: 30 days)
2 fields of
DurationvalueintegerNumber of units; 0 means no duration
unitstringRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
trialPeriodDurationOptionalA free trial before the first bill; omitted without one
2 fields of
DurationvalueintegerNumber of units; 0 means no duration
unitstringRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
minPeriodsintegerOptionalThe periods the customer commits to before they can cancel; omitted without a minimum
paymentLinkstringOptionalPaymentLink is its shareable link: opening it starts a checkout of the product (a subscription one with its terms, else a one-time payment) and goes to the checkout page. Only for the merchant
Only for the resource's owner
testbooleanWhether this is test data (testnets) or live data
userUuidstringOptionalMember the resource belongs to; omitted for the organization's own resources
Only for the resource's owner
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Delete product#
DELETE /products/
Deletes a product of the caller's space (204): gone from the lists and reads, its link closed; payments and subscriptions of it keep naming it.
- Auth
- X-API-Key: any keyA member's key needs the members.products policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 204
request.sh
curl -X DELETE "https://api.qbitflow.app/v2/products/$PRODUCT_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 204: no contentPath parameters
uuidstringRequired
Response
No JSON body: the edge cases below say what it answers.
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Edge cases
The contract's rules for these routes: Authentication & roles · Spaces · The product · Validation · Metadata · Identifiers · References · Active vs hidden products · Subscription products · The payment link (paymentLink) · Reads · Update (PATCH, partial) · Delete.
Read all 13
Authentication & roles
- There is no role gate.
- Every route is under the
members.productspolicy: a member has products (reads them, creates, updates and deletes them, with the dashboard or their own key) while the organization'smembers.productspolicy allows it (on by default). Off →403policy_disabled,details.policy: "members.products", on every route, reads included. The team always can, acting in the member's space (switched into it, or On-Behalf-Of with an organization key): with the policy off, the organization manages the member's products. The policy also closes the member's checkouts (Checkout sessions) and the links of every product in their space (Payment links).
Spaces
- Every product belongs to one space (spaces): the organization's or a member's, in one mode. Every route works in the request's space: the team (and organization keys) sees the organization's products, a member (and their keys) their own, and an organization key with
On-Behalf-Of: <userUuid>(or the dashboard switched into the member's space) the member's. - A product of another space, of another organization or of the other mode is never listed (the
referencefilter included: an empty list) and answers404by uuid, for reads, updates and deletes: existence is never revealed. Another organization's product uuid answers like an unknown one.
The product
priceUsd: the price in USD, a decimal string in its canonical form ("12.5","1000", no trailing zeros; naming). What a checkout of the product charges, before its fees.description: absent when the product has none (never"").metadata: the merchant's own data (below); always present,{}when empty.reference,metadataandpaymentLinkare the merchant's: the public reads that show a product (a checkout,GET /hosted/transactions/:txUuid) omit them (resource:"whenAuth"); every authenticated read has them.userUuidis present for a member's product and absent for the organization's own.subscriptionis absent on a one-time product (below).
Validation
name: thenamerule, length 2–100.description(optional):text, length 2–500; an update clears it with"".textpermits letters (including accented/unicode), digits, spaces, newlines/tabs and common punctuation (e.g.. , ! ? ' - & ( ) / : @ # %). It rejects control characters and< > { } [ ] ` \ | ; " ~ ^— so HTML/markup (<script>,<img onerror=…>,<svg onload=…>) is refused, and a blank (whitespace-only) value is refused. Non-markup text is accepted even when it looks script-like (e.g. the bare stringjavascript:alert(1)is allowed) — fields are rendered as escaped text. Any violation →400validation_failedon the field.priceUsd(required on create, optional on update): a JSON string holding an amount in USD ("12.50"): above 0, at most 2 decimals, at most"1000000"(theusd=1000000rule). A JSON number (12.5), a third decimal, an exponent ("1e3"), a sign,"0"or more than 1,000,000 →400validation_failedonpriceUsd. Trailing zeros are accepted ("12.50") and written back canonical ("12.5"). The v1price(a number) is not read: a create withoutpriceUsd→400onpriceUsd.- Test mode: a price is at most $5 (test payments use faucet tokens, whose supply is limited), on create and update: above →
400validation_failedonpriceUsd,details.max: "5"(a string). A checkout of a test product priced above it (one priced before) is refused the same way. Live mode has no cap of its own: what the token chosen at a checkout's intent can hold bounds it (Payments and transactions). reference(optional) defaults to the product'suuidwhen omitted. A providedreferencemust pass thereferencevalidator (1–100 characters: ASCII letters, digits and. _ : @ -) → otherwise400validation_failedonreference.metadata(optional): at most 50 keys; a key is 1–40 characters ofA–Z a–z 0–9 _ - . :; a value is a string of at most 500 characters (Unicode code points) without control or bidirectional characters. Too many keys or a bad key →400validation_failedonmetadata(the key is not echoed); a bad value → onmetadata.<key>(a value that isn't a string too). On create, a key set to""is not stored.- Success →
201with the createdProduct. Idempotency-Key(idempotency): a create retried with the same key answers the firstProduct(Idempotent-Replayed: true) and creates no second one.
Metadata
- The merchant's own key–value data (an id in its system, a SKU…), never read by QBitFlow. Product metadata is never copied onto the checkouts, payments or subscriptions of the product (a checkout session takes its own
metadata: Checkout sessions). PATCHmerges (Stripe's semantics):metadataabsent → unchanged;{"k": "v"}setsk;{"k": ""}deletesk; keys not named are kept. There is no "replace the whole map": to clear it, send each key with"". The 50-key limit is checked on the merged result: over →400validation_failedonmetadata, and nothing of the update is applied.- Never put secrets or personal data in metadata: it is shown to anyone reading the product with the merchant's credentials.
Identifiers
- A product is named by its
uuid(it was a numericidin v1; v1's numbers are gone).
References
- A
referenceis unique per space (the organization's, each member's, per mode) among its products that aren't deleted (hidden ones included). Reusing a reference →409(code: "unique_violation",details.field: "reference", the value not echoed). A deleted product frees its reference: a new product may take it. - A product is found by its reference with the list's filter:
GET /products?reference=<ref>answers the list envelope with 0 or 1 item (hidden products included, deleted ones never). The v1 routeGET /product/reference/:referenceis gone (a reference no longer travels in a path).
Active vs hidden products
isActivedefaults totrue(POST /productsalways creates a listed product). A product is hidden (isActive=false) when:- it was created "behind the scenes": the inline product of a subscription checkout session (Checkout sessions). A one-time payment's inline product creates no product (the payment keeps a snapshot of its name, description and price);
- its member was removed, or its organization closed: every product of the space is hidden then (Members and held funds). Re-inviting the member lists again the products the removal hid, unless the merchant listed or hid them since;
- its owner hid it:
PATCH /products/:uuidwith"isActive": false.
- Hidden products are excluded from
GET /productsunless?includeHidden=true(or thereferencefilter names one), but remain readable byGET /products/:uuidand editable byPATCH /products/:uuid."isActive": truelists a hidden product again.
Subscription products
A product with subscription terms is a subscription product; without, a one-time product.
POST /productswithsubscription: { frequency, trialPeriod?, minPeriods? }(types.Durations;minPeriods0 to 1000) creates one:json { "name": "Team plan", "priceUsd": "49", "subscription": { "frequency": { "value": 1, "unit": "months" }, "trialPeriod": { "value": 14, "unit": "days" }, "minPeriods": 3 } }The answer writes each duration in its largest exact unit (14 days →{ "value": 2, "unit": "weeks" }).- The terms are validated as a subscription checkout's are, with the product's price (
400 validation_failed):subscription.frequencyrequired, at least 1 hour live (5 minutes in test mode), at most 1 year; a live subscription product priced at $0.25 or less → onpriceUsd(details.min: "0.25", a string), on create and on a later price change. PATCH /products/:uuidwithsubscriptionchanges the fields it sends and keeps the others (a one-time product becoming a subscription one must sendfrequency:subscription.frequencyotherwise);trialPeriod: { "value": 0 }andminPeriods: 0remove them.removeSubscription: truemakes it a one-time product (not withsubscription:400onremoveSubscription). Like a price, terms apply to new subscribers only: a running subscription keeps the terms it signed.GET /products?subscription=truelists the subscription products only,falsethe one-time ones (withincludeHiddenas usual).- A product's terms are the defaults of its subscription checkouts:
POST /checkout-sessions/subscription{ "productUuid" }alone takes them, and each term the request sends overrides the product's (Checkout sessions § Creating a session). A one-time product can still be sold as a subscription (the request gives the terms), and any product can be sold once (POST /checkout-sessions/payment). - A member gives a product terms (
subscriptiononPOST, or onPATCH) only while the organization'smembers.subscriptionspolicy allows them: off →403policy_disabled,details.policy: "members.subscriptions", nothing changed. One-time products, other edits of a subscription product andremoveSubscriptionstill work. The team always can, acting in the member's space. With the policy off, the link of a member's subscription product opens nothing (Payment links), whoever gave it its terms.
The payment link (paymentLink)
- Every product has
paymentLink(merchant only): its link,GET /pay/:productUuid(Payment links). Opening it starts a checkout (a subscription one for a subscription product) and goes to the checkout page. Nothing to create: hiding the product (isActive: false) or deleting it turns its link off.
Reads
GET /products/:uuidreturns a single product of the caller's space (active or hidden). Unknown, soft-deleted, or another space's uuid →404. A:uuidthat is not a UUID →400validation_failedonuuid.GET /productsreturns the list envelope (pagination:{ items, hasMore, nextCursor? }) of the caller's active products, newest first (createdAt, thenuuid): pages oflimit(default 20, at most 100: a larger one is clamped; not an integer →400onlimit), the next page with?cursor=<nextCursor>(one the API didn't give →400oncursor). Filters, kept across pages:includeHidden=true(the hidden ones too, never the deleted ones; not a boolean →400onincludeHidden),subscription=true|false,reference=<ref>(0 or 1 item, hidden included). (It was a bare array, cheapest first, without pages.)
Update (PATCH, partial)
UpdateProductDtofields (name,description,priceUsd,isActive,subscription,removeSubscription,metadata) are all optional. Updates are partial: only the fields present in the body are modified; omitted fields are left unchanged. An empty body is a200no-op. Answers the product as stored.- A hidden product is updated like a listed one.
isActivehides (false) or lists again (true) the product. - A
priceUsdthat isn't a valid USD string, or above $5 in test mode, →400validation_failedonpriceUsd(the product is left unchanged).textand length rules apply to updates the same as to creates. - A product is updated in place (same
uuidandreference). A new price applies to new checkouts and new subscribers only: an existing subscription keeps the price it was subscribed at (itspriceUsd, Subscriptions and bills § Billing), and so does a checkout session already open. referenceis not updatable and is ignored if sent.PATCH /products/:uuid: an unknown, deleted or another space's uuid →404(nothing changed); a malformed one →400onuuid.PUTis not a route:405method_not_allowed(Allowlists the path's methods).- Mass-assignment is prevented: any field outside the DTO (
uuid,userUuid,reference,test,createdAt, …) is ignored; the request still succeeds (200) and those fields remain unchanged. In particular a product cannot be moved to another organization.
Delete
DELETE /products/:uuid→204with no body (it answered a message). Delete is a soft delete: the product is removed from every read (404by uuid, absent from the list and thereferencefilter) and its link closes. An unknown, already deleted or another space's uuid →404; a malformed one →400onuuid.- Deleting a product doesn't stop its subscriptions: they keep billing at the price they were subscribed at, and their bills still name the product (
productUuid). Cancel them (POST /subscriptions/:uuid/cancel) if the product is gone for good.
The codes named here
- 400validation_
failed path/query/body failed binding or validation, or a business rule about one input (a field named in
errors) - 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
- 409unique_
violation a unique value already exists (e.g. a duplicate
referenceoremail) - 405method_
not_ allowed the path exists, not with this method; the
Allowheader lists its methods. Answered before any credential is read, like the404of a path no route has