API reference

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

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

  • limitintegerOptional

    Page size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)

  • cursorstringOptional

    The previous page's nextCursor, verbatim; omit for the first page

    max=200

  • includeHiddenbooleanOptional

    true lists the hidden products too (isActive false)

  • subscriptionbooleanOptional

    true: only the subscription products; false: only the one-time ones

  • referencestringOptional

    Only 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
  • uuidstring

    Its only id (a UUIDv7)

  • namestring
  • descriptionstring

    Absent without one

  • priceUsdstring

    The price, in USD (a decimal string, e.g. "12.5"): what a checkout of the product charges, before its fees

  • createdAtstring
  • isActiveboolean

    false 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

  • referencestringOptional

    Reference 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

  • metadatamapOptional

    Metadata 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

  • subscriptionSubscriptionTermsOptional

    Subscription 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 SubscriptionTerms
    • frequencyDuration

      How often it is billed (e.g. {"value": 1, "unit": "months"}: 30 days)

      2 fields of Duration
      • valueinteger

        Number of units; 0 means no duration

      • unitstring

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • trialPeriodDurationOptional

      A free trial before the first bill; omitted without one

      2 fields of Duration
      • valueinteger

        Number of units; 0 means no duration

      • unitstring

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • minPeriodsintegerOptional

      The periods the customer commits to before they can cancel; omitted without a minimum

  • testboolean

    Whether this is test data (testnets) or live data

  • userUuidstringOptional

    Member 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

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

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 Product
const 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

  • namestringRequired

    namemin=2max=100

  • descriptionstringOptional

    Optional

    textmin=2max=500

  • priceUsdnumberRequired

    The 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=1000000

  • referencestringOptional

    Optional merchant reference, unique per owner and mode. The product's uuid if omitted

    referencemax=100

  • subscriptionSubscriptionTermsDtoOptional

    Subscription makes it a subscription product: its terms (frequency required). Omitted: a one-time product

    3 fields of SubscriptionTermsDto
    • frequencyDurationOptional

      How often it is billed: at least 1 hour (5 minutes in test mode), at most 1 year

      2 fields of Duration
      • valueintegerOptional

        Number of units; 0 means no duration

      • unitstringOptional

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • trialPeriodDurationOptional

      A free trial before the first bill ({"value": 0}: none, removing a product's)

      2 fields of Duration
      • valueintegerOptional

        Number of units; 0 means no duration

      • unitstringOptional

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • minPeriodsintegerOptional

      The periods the customer commits to before they can cancel (0: none, removing a product's), at most 1000

      max=1000

  • metadatamapOptional

    Metadata 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
  • uuidstring

    Its only id (a UUIDv7)

  • namestring
  • descriptionstring

    Absent without one

  • priceUsdstring

    The price, in USD (a decimal string, e.g. "12.5"): what a checkout of the product charges, before its fees

  • createdAtstring
  • isActiveboolean

    false 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

  • referencestringOptional

    Reference 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

  • metadatamapOptional

    Metadata 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

  • subscriptionSubscriptionTermsOptional

    Subscription 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 SubscriptionTerms
    • frequencyDuration

      How often it is billed (e.g. {"value": 1, "unit": "months"}: 30 days)

      2 fields of Duration
      • valueinteger

        Number of units; 0 means no duration

      • unitstring

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • trialPeriodDurationOptional

      A free trial before the first bill; omitted without one

      2 fields of Duration
      • valueinteger

        Number of units; 0 means no duration

      • unitstring

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • minPeriodsintegerOptional

      The periods the customer commits to before they can cancel; omitted without a minimum

  • paymentLinkstringOptional

    PaymentLink 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

  • testboolean

    Whether this is test data (testnets) or live data

  • userUuidstringOptional

    Member the resource belongs to; omitted for the organization's own resources

    Only for the resource's owner

Errors

Get product#

GET /products/:uuid

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

request.sh

curl "https://api.qbitflow.app/v2/products/$PRODUCT_UUID" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a Product

Path parameters

  • uuidstringRequired

Response

A Product.

12 fields of Product
  • uuidstring

    Its only id (a UUIDv7)

  • namestring
  • descriptionstring

    Absent without one

  • priceUsdstring

    The price, in USD (a decimal string, e.g. "12.5"): what a checkout of the product charges, before its fees

  • createdAtstring
  • isActiveboolean

    false 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

  • referencestringOptional

    Reference 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

  • metadatamapOptional

    Metadata 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

  • subscriptionSubscriptionTermsOptional

    Subscription 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 SubscriptionTerms
    • frequencyDuration

      How often it is billed (e.g. {"value": 1, "unit": "months"}: 30 days)

      2 fields of Duration
      • valueinteger

        Number of units; 0 means no duration

      • unitstring

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • trialPeriodDurationOptional

      A free trial before the first bill; omitted without one

      2 fields of Duration
      • valueinteger

        Number of units; 0 means no duration

      • unitstring

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • minPeriodsintegerOptional

      The periods the customer commits to before they can cancel; omitted without a minimum

  • testboolean

    Whether this is test data (testnets) or live data

  • userUuidstringOptional

    Member 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

Update product#

PATCH /products/:uuid

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

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 Product

Path parameters

  • uuidstringRequired

Body

  • namestringOptional

    namemin=2max=100

  • descriptionstringOptional

    2 to 500 characters, the text rule; "" clears it

    max=500len=0|min=2len=0|text

  • priceUsdnumberOptional

    The 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=1000000

  • isActivebooleanOptional

    false hides the product from GET /products, true lists it again

  • metadatamapOptional

    Metadata 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

    metadata

  • subscriptionSubscriptionTermsDtoOptional

    Subscription 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 SubscriptionTermsDto
    • frequencyDurationOptional

      How often it is billed: at least 1 hour (5 minutes in test mode), at most 1 year

      2 fields of Duration
      • valueintegerOptional

        Number of units; 0 means no duration

      • unitstringOptional

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • trialPeriodDurationOptional

      A free trial before the first bill ({"value": 0}: none, removing a product's)

      2 fields of Duration
      • valueintegerOptional

        Number of units; 0 means no duration

      • unitstringOptional

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • minPeriodsintegerOptional

      The periods the customer commits to before they can cancel (0: none, removing a product's), at most 1000

      max=1000

  • removeSubscriptionbooleanOptional

    RemoveSubscription makes it a one-time product again (not with subscription)

Response

A Product.

12 fields of Product
  • uuidstring

    Its only id (a UUIDv7)

  • namestring
  • descriptionstring

    Absent without one

  • priceUsdstring

    The price, in USD (a decimal string, e.g. "12.5"): what a checkout of the product charges, before its fees

  • createdAtstring
  • isActiveboolean

    false 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

  • referencestringOptional

    Reference 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

  • metadatamapOptional

    Metadata 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

  • subscriptionSubscriptionTermsOptional

    Subscription 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 SubscriptionTerms
    • frequencyDuration

      How often it is billed (e.g. {"value": 1, "unit": "months"}: 30 days)

      2 fields of Duration
      • valueinteger

        Number of units; 0 means no duration

      • unitstring

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • trialPeriodDurationOptional

      A free trial before the first bill; omitted without one

      2 fields of Duration
      • valueinteger

        Number of units; 0 means no duration

      • unitstring

        Required when value > 0. "months" = 30 days, "years" = 365 days

        oneof=seconds minutes hours days weeks months years

    • minPeriodsintegerOptional

      The periods the customer commits to before they can cancel; omitted without a minimum

  • testboolean

    Whether this is test data (testnets) or live data

  • userUuidstringOptional

    Member 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

Delete product#

DELETE /products/:uuid

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

request.sh

curl -X DELETE "https://api.qbitflow.app/v2/products/$PRODUCT_UUID" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 204: no content

Path 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

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.products policy: a member has products (reads them, creates, updates and deletes them, with the dashboard or their own key) while the organization's members.products policy allows it (on by default). Off → 403 policy_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 reference filter included: an empty list) and answers 404 by 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, metadata and paymentLink are 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.
  • userUuid is present for a member's product and absent for the organization's own.
  • subscription is absent on a one-time product (below).

Validation

  • name: the name rule, length 2–100. description (optional): text, length 2–500; an update clears it with "". text permits letters (including accented/unicode), digits, spaces, newlines/tabs and common punctuation (e.g. . , ! ? ' - & ( ) / : @ # %). It rejects control characters and &lt; > { } [ ] ` \ | ; " ~ ^ — so HTML/markup (&lt;script>, &lt;img onerror=…>, &lt;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 string javascript:alert(1) is allowed) — fields are rendered as escaped text. Any violation → 400 validation_failed on 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" (the usd=1000000 rule). A JSON number (12.5), a third decimal, an exponent ("1e3"), a sign, "0" or more than 1,000,000 → 400 validation_failed on priceUsd. Trailing zeros are accepted ("12.50") and written back canonical ("12.5"). The v1 price (a number) is not read: a create without priceUsd → 400 on priceUsd.
  • Test mode: a price is at most $5 (test payments use faucet tokens, whose supply is limited), on create and update: above → 400 validation_failed on priceUsd, 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's uuid when omitted. A provided reference must pass the reference validator (1–100 characters: ASCII letters, digits and . _ : @ -) → otherwise 400 validation_failed on reference.
  • metadata (optional): at most 50 keys; a key is 1–40 characters of A–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 → 400 validation_failed on metadata (the key is not echoed); a bad value → on metadata.<key> (a value that isn't a string too). On create, a key set to "" is not stored.
  • Success → 201 with the created Product.
  • Idempotency-Key (idempotency): a create retried with the same key answers the first Product (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).
  • PATCH merges (Stripe's semantics): metadata absent → unchanged; {"k": "v"} sets k; {"k": ""} deletes k; 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 → 400 validation_failed on metadata, 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 numeric id in v1; v1's numbers are gone).

References

  • A reference is 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 route GET /product/reference/:reference is gone (a reference no longer travels in a path).

Active vs hidden products

  • isActive defaults to true (POST /products always 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/:uuid with "isActive": false.
  • Hidden products are excluded from GET /products unless ?includeHidden=true (or the reference filter names one), but remain readable by GET /products/:uuid and editable by PATCH /products/:uuid. "isActive": true lists a hidden product again.

Subscription products

A product with subscription terms is a subscription product; without, a one-time product.

  • POST /products with subscription: { frequency, trialPeriod?, minPeriods? } (types.Durations; minPeriods 0 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.frequency required, at least 1 hour live (5 minutes in test mode), at most 1 year; a live subscription product priced at $0.25 or less → on priceUsd (details.min: "0.25", a string), on create and on a later price change.
  • PATCH /products/:uuid with subscription changes the fields it sends and keeps the others (a one-time product becoming a subscription one must send frequency: subscription.frequency otherwise); trialPeriod: { "value": 0 } and minPeriods: 0 remove them. removeSubscription: true makes it a one-time product (not with subscription: 400 on removeSubscription). Like a price, terms apply to new subscribers only: a running subscription keeps the terms it signed.
  • GET /products?subscription=true lists the subscription products only, false the one-time ones (with includeHidden as 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 (subscription on POST, or on PATCH) only while the organization's members.subscriptions policy allows them: off → 403 policy_disabled, details.policy: "members.subscriptions", nothing changed. One-time products, other edits of a subscription product and removeSubscription still 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.
  • 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/:uuid returns a single product of the caller's space (active or hidden). Unknown, soft-deleted, or another space's uuid → 404. A :uuid that is not a UUID → 400 validation_failed on uuid.
  • GET /products returns the list envelope (pagination: { items, hasMore, nextCursor? }) of the caller's active products, newest first (createdAt, then uuid): pages of limit (default 20, at most 100: a larger one is clamped; not an integer → 400 on limit), the next page with ?cursor=<nextCursor> (one the API didn't give → 400 on cursor). Filters, kept across pages: includeHidden=true (the hidden ones too, never the deleted ones; not a boolean → 400 on includeHidden), subscription=true|false, reference=<ref> (0 or 1 item, hidden included). (It was a bare array, cheapest first, without pages.)

Update (PATCH, partial)

  • UpdateProductDto fields (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 a 200 no-op. Answers the product as stored.
  • A hidden product is updated like a listed one. isActive hides (false) or lists again (true) the product.
  • A priceUsd that isn't a valid USD string, or above $5 in test mode, → 400 validation_failed on priceUsd (the product is left unchanged). text and length rules apply to updates the same as to creates.
  • A product is updated in place (same uuid and reference). A new price applies to new checkouts and new subscribers only: an existing subscription keeps the price it was subscribed at (its priceUsd, Subscriptions and bills § Billing), and so does a checkout session already open.
  • reference is not updatable and is ignored if sent.
  • PATCH /products/:uuid: an unknown, deleted or another space's uuid → 404 (nothing changed); a malformed one → 400 on uuid. PUT is not a route: 405 method_not_allowed (Allow lists 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 → 204 with no body (it answered a message). Delete is a soft delete: the product is removed from every read (404 by uuid, absent from the list and the reference filter) and its link closes. An unknown, already deleted or another space's uuid → 404; a malformed one → 400 on uuid.
  • 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 reference or email)

  • 405method_not_allowed

    the path exists, not with this method; the Allow header lists its methods. Answered before any credential is read, like the 404 of a path no route has