Skip to main content
A product is a fixed-price catalogue item. Orders are always for products, and each product may carry a delivery — the digital good handed to the buyer once the order completes.
Products are created and edited in the dashboard. An API key can read them and append delivery stock, but not create, edit or delete them. See Current limitations.

Product types

productType currently has exactly one value: single-purchase. It is present so the field is stable as more types arrive; there is nothing to choose today.

Reading products

GET /v1/products and GET /v1/products/{id} — both require the products:read scope. Products are addressed by UUID, not a short id. The list is newest first.
200 OK
GET /v1/products/{id} returns a single object in exactly that shape. An unknown product, or one from another store, answers 404 with { "error": "Product not found" }.
A product must be listed for an order to be created against it. An unlisted product is rejected with Product is not available. Unlisting is how you retire a product without deleting its order history.

What the API exposes about delivery — and what it never does

This is the important part of the page.
Fulfilment secrets are simply not part of the merchant read model. The serializer reads only the type and the quantity bounds from the delivery configuration — the columns holding secrets are never selected, so there is no field to accidentally leak. The buyer receives the actual goods on the completed checkout page, and nowhere else.
stock is the live count of available units, and is meaningful only for text_pool — the one finite delivery type. For every other type, and for a product with no delivery configured at all, stock is null.

Quantity bounds

minQuantity defaults to 1 and maxQuantity to null (no maximum) when a product has no delivery configuration. Both are enforced at order creation:
  • Below the minimum: Minimum order quantity for {title} is {n}
  • Above the maximum: Maximum order quantity for {title} is {n}
Both are 400s on POST /v1/orders.

The four delivery types

A pool of text units, one per sellable item: licence keys, gift-card codes, account credentials, e-pins. This is the only finite type, and the only one with a stock count.Units move availablereserved (when an order is created) → delivered (when it completes). An order that expires returns its reservations to the pool, which is why expiring a stale order is worth doing.Stock is stored encrypted at rest and can be topped up through the API — see Appending stock.
A single piece of text every buyer receives: a coupon code, a Discord invite, a set of instructions. Unlimited — there is nothing to run out of, so stock is null.
A single file, held in private storage and never public-read. Buyers reach it only through a short-lived presigned URL minted by the gated download endpoint — see the download route. Unlimited; stock is null.
An HTTPS URL the buyer is sent to after paying — your own fulfilment page, a vendor portal, a course platform. Unlimited; stock is null.
A product with no delivery configuration is perfectly valid. The order simply completes as a receipt — useful when fulfilment happens entirely on your side, off the back of the order.completed webhook.

Appending stock

POST /v1/products/{id}/stock — requires the products:write scope. Works on text_pool products only. This is the one write an API key has over the catalogue, and it exists so a key generator can top up a pool without a human logging in.
Request
text is one blob, 1 to 5,000,000 characters. It is processed in a fixed order:

Split, trim, drop blanks, de-duplicate within the batch

The blob is split on the product’s configured delimiter (the default is a newline, and CRLF line endings are handled), each line is trimmed, empty lines are dropped, and repeats within the submitted text collapse to one line. Three identical keys in one paste become one line here, silently.

Apply the 10,000-line cap

The cap is checked after that de-duplication, on the surviving line count. A blob of 12,000 lines that is only 9,000 distinct lines is accepted; 10,001 distinct lines is 400 { "error": "Cannot add more than 10000 lines at once" }. An empty result — nothing survived splitting and trimming — is 400 No stock lines found.

Compare against existing stock

Each surviving line is compared against the units already available or reserved on the product. Lines that collide are skipped; the rest are inserted.
200 OK
  • added — units newly inserted.
  • skippedDuplicatesonly the surviving lines that collided with existing non-delivered stock on the product. Collisions are reported, not rejected, which is what makes re-sending a batch safe.
  • available — the pool’s available count after the append.
added + skippedDuplicates can be less than the number of lines you sent. Repeats within the submitted text are collapsed in step 1 and are never counted anywhere — not in added, not in skippedDuplicates. The two counters describe the de-duplicated batch, not the raw input. If you need to know how many of your own lines were redundant, count distinct lines before sending.
Because duplicates are skipped rather than erroring, appending is naturally idempotent for whole batches: re-POST the same blob after a timeout and you get added: 0 with everything counted as a duplicate.
A unit that has already been delivered is not part of the duplicate check — the same string can be re-added later. Dedup guards against double-loading a batch, not against reselling a code you deliberately reissue.

Example

Errors

The 8 MiB limit is on encoded bytes, not characters, so a text of well under 5,000,000 characters can still hit 413 once non-ASCII content is UTF-8 encoded. Batch in chunks of a few thousand lines and neither limit is close.
The delimiter, selection order (FIFO or LIFO) and custom checkout fields are configured in the dashboard. They are not settable through the public API.

What delivery looks like on a completed order

Delivery is exposed to the buyer, on the public checkout payload, and only once the order’s status is completed. Until then every item’s delivery is null. Each shape is discriminated by type:
Notes that matter:
  • file_shared never exposes a storage key. You get a fileName and an orderItemId, which is what the download endpoint takes to mint a five-minute presigned URL.
  • text_pool.lines can be shorter than the item’s quantity if the pool ran short at fulfilment time. Do not assume a one-to-one match.
  • The delivered payload is snapshotted at completion, in the same transaction that flips the order to completed. Later edits to the product never change what a past buyer received.
Delivery content is deliberately absent from the merchant API. GET /v1/orders/{id} returns line items and payment facts, not the codes that were handed over. If your own system needs to know what was delivered, generate the units yourself and push them with the stock endpoint so you keep your own record.