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" }.
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}
400s on POST /v1/orders.
The four delivery types
text_pool — a consumable pool of lines
text_pool — a consumable pool of lines
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 available → reserved (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.redirect — send the buyer somewhere
redirect — send the buyer somewhere
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.skippedDuplicates— only 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.
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 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’sstatus is completed. Until then every item’s delivery is null.
Each shape is discriminated by type:
file_sharednever exposes a storage key. You get afileNameand anorderItemId, which is what the download endpoint takes to mint a five-minute presigned URL.text_pool.linescan be shorter than the item’squantityif 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.