API reference
One REST endpoint stands between your storefront and everything tedious about selling: stock that can't oversell, payment gateways, key delivery, receipts and alerts. You create an invoice and send the buyer to the URL you get back. We handle the rest.
Quickstart
curl -X POST https://jester-guru.vercel.app/api/v1/checkout \
-H "Authorization: Bearer $JESTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [{ "sku": "NITRO-1M", "quantity": 1 }],
"customer": { "email": "buyer@example.com" },
"return_url": "https://yourshop.com/thanks"
}'
{
"checkout_url": "https://jester-guru.vercel.app/invoice/2145512-1-213123-2-2323",
"order": {
"id": "2145512-1-213123-2-2323",
"status": "pending",
"total": "9.99",
"expires_at": "2026-09-08T04:38:05.013Z"
}
}
Redirect the buyer to checkout_url. That page is themed to your shop, offers
whichever payment methods you've enabled, and hands over the keys the moment money lands.
- Base URL
- https://jester-guru.vercel.app/api/v1
- Content type
- application/json
- Auth
- Bearer jstr_live_…
Authentication
Every request carries an API key. Mint keys in the dashboard under API keys — they're shown once and stored only as a SHA-256 hash, so a database leak doesn't hand anyone a working key.
Authorization: Bearer jstr_live_a1b2c3d4…
Two alternatives exist for awkward clients: an X-API-Key header, or an
?api_key= query parameter. Prefer the header — query strings end up in
server logs and browser history.
Scopes
Each key carries scopes. A key missing the required scope gets
403 insufficient_scope, naming what it needed.
| Scope | Grants |
|---|---|
| products:read | List and fetch products, variants and stock levels. |
| products:write | Create and edit products, variants, statuses; push stock. |
| orders:read | List invoices, read their state and delivered keys. |
| orders:write | Open checkouts and cancel invoices. |
Errors
Errors are JSON with a stable machine-readable code and a message written
for a human reading logs at 3am.
{
"error": {
"code": "checkout_failed",
"message": "Nitro — 3 Months only has 2 in stock."
}
}
| Status | Meaning |
|---|---|
| 400 | Malformed request — usually bad JSON. |
| 401 | Missing, malformed, revoked or unknown API key. |
| 403 | Key lacks the scope this endpoint needs. |
| 404 | No such product, variant or invoice on your shop. |
| 409 | Conflict — SKU taken, out of stock, invoice already closed. |
| 422 | Understood but invalid — bad price, malformed SKU, unknown status. |
| 429 | Rate limited. Retry-After says how long to wait. |
| 5xx | Our fault. Safe to retry with backoff. |
Rate limits
120 requests per minute, per key. Every response carries the current budget:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 113
Exceed it and you get 429 with Retry-After in seconds. Mint a
second key for a separate service rather than sharing one budget across everything.
Data model
Four things, and the relationship between the first two is the one that matters:
- Product — the listing. Name, description, image. Doesn't have a price.
- Variant — what a buyer actually orders. "1 Month", "1 Year". Carries the price, the SKU and its own pool of keys. Every product has at least one.
- Category — how you group listings: "Fortnite", "Rust", "Spoofers". A product sits in at most one; anything unfiled is Uncategorised.
- Status — a coloured badge you define, e.g. "Undetected", "Detected", "Updating". Marking one not sellable blocks orders for anything wearing it.
- Order — an invoice. Reserves stock the moment it's created.
sku in a checkout call
resolves to a variant. SKUs are unique across your whole shop.
Stock modes
| Mode | Behaviour |
|---|---|
| serials | A pool of individual keys. One is reserved per unit ordered and handed to the buyer on payment. This is the default. |
| count | Just a number. Decremented on order, restored if it lapses. Nothing is auto-delivered — you fulfil it yourself. |
| unlimited | Never runs out, nothing to deliver. |
Stock reservation
Creating an invoice reserves stock immediately, inside a transaction. It's
released when the invoice is paid (handed over), cancelled, or expires. Two buyers can
never be sold the same key, even racing on the last one — the loser gets a
409.
Account
Your shop, the key being used, and which payment methods buyers will be offered. Useful as a credential health check on boot.
{
"shop": { "name": "Nitro Store", "slug": "nitro-store", "currency": "USD" },
"key": { "label": "Storefront", "prefix": "jstr_live_2a071585", "scopes": ["products:read", "orders:write"] },
"payment_methods": [{ "provider": "stripe", "label": "Stripe", "flow": "redirect" }]
}
Enabled, correctly-configured methods in display order — what the checkout page will actually show.
Products
Every product with its variants and live stock. Add
?include_inactive=true to include hidden ones.
curl https://jester-guru.vercel.app/api/v1/products -H "Authorization: Bearer $JESTER_KEY"
One product by numeric id or slug.
Create a product, its variants and their opening stock in a single call.
| Field | Type | Notes |
|---|---|---|
| name * | string | Listing name. |
| description | string | Shown on the checkout page. |
| image_url | string | Absolute http(s) URL. |
| status | string | Status name, e.g. "In Stock". Or status_id. |
| active | boolean | Defaults true. |
| variants | array | See below. Omit for a single-variant product and pass price/sku at the top level instead. |
Variant fields
| name | string | Defaults to "Standard". |
| price * | string | Decimal, e.g. "9.99". Or price_cents as an integer. |
| sku | string | Auto-generated if omitted. Unique per shop. |
| stock_mode | string | serials · count · unlimited |
| stock | string[] | Opening keys, serials mode only. |
| max_per_order | integer | Defaults 10. |
| low_stock_at | integer | Discord alert threshold. Defaults 3. |
curl -X POST https://jester-guru.vercel.app/api/v1/products \
-H "Authorization: Bearer $JESTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Nitro",
"status": "In Stock",
"variants": [
{ "name": "1 Month", "sku": "NITRO-1M", "price": "9.99",
"stock": ["AAAA-BBBB", "CCCC-DDDD"] },
{ "name": "1 Year", "sku": "NITRO-1Y", "price": "99.00",
"stock_mode": "unlimited" }
]
}'
Update listing fields: name, description,
image_url, status, active,
sort_order. Price and stock live on variants.
Deletes the product and its variants. Returns 409 product_in_use if it
sits on an open invoice — settle or cancel that first.
Variants
Look a variant up by id or SKU — handy when your storefront caches SKUs and wants a live price and stock check before rendering a buy button.
curl https://jester-guru.vercel.app/api/v1/variants/NITRO-1M -H "Authorization: Bearer $JESTER_KEY"
Add a variant to an existing product. Same fields as the variant array above.
Change name, sku, price, status,
active, max_per_order, low_stock_at, or
stock_count on counted variants.
curl -X PATCH https://jester-guru.vercel.app/api/v1/variants/1 \
-H "Authorization: Bearer $JESTER_KEY" \
-H "Content-Type: application/json" \
-d '{ "price": "12.50" }'
A variant with sales history is deactivated rather than deleted, so old receipts keep resolving. The response tells you which happened:
{ "deleted": false, "deactivated": true, "reason": "Variant has sales history." }
Deleting the last variant of a product returns 409 last_variant — delete
the product instead.
Stock
Push keys onto the shelf. Up to 5000 per call.
curl -X POST https://jester-guru.vercel.app/api/v1/variants/1/stock \
-H "Authorization: Bearer $JESTER_KEY" \
-H "Content-Type: application/json" \
-d '{ "stock": ["AAAA-BBBB", "CCCC-DDDD"] }'
{ "added": 2, "skipped": 0, "stock_available": 14 }
For count variants send an adjustment instead — negative reduces, and it
clamps at zero:
{ "add": 25 }
Categories
How your listings are grouped. Fetch these to build storefront navigation, then pull the products for whichever one the visitor picked.
{
"data": [
{ "id": 2, "name": "Rust", "slug": "rust", "description": null,
"image_url": null, "sort_order": 0, "active": true, "product_count": 1 }
],
"count": 1
}
product_count counts active products only, so a category
you have emptied shows zero without you having to work it out.
Filter the product list by category id or slug — one section of your storefront.
| name * | string | 1-60 characters, unique per shop. The slug is derived. |
| description | string | |
| image_url | string | |
| sort_order | integer | Lower sorts first. |
Assign a product with "category": "Rust" or
"category_id": 2 on create or PATCH.
Statuses
Your own product states with your own colours. They badge the dashboard and the checkout page, and one marked not sellable blocks orders.
{
"data": [
{ "id": 1, "name": "In Stock", "color": "#22c55e", "sellable": true, "is_default": true },
{ "id": 4, "name": "Sold Out", "color": "#ef4444", "sellable": false, "is_default": false }
]
}
| name * | string | 1–32 characters, unique per shop. |
| color * | string | 6-digit hex, e.g. #22c55e. |
| sellable | boolean | Defaults true. False blocks orders. |
Checkout
Prices the order, reserves the stock, and returns a URL to send the buyer to.
| Field | Type | Notes |
|---|---|---|
| items * | array | { sku, quantity } or { variant_id, quantity }. Max 50 lines. |
| customer.email | string | Pre-fills the gateway and appears (masked) in Discord. |
| customer.name | string | |
| customer.discord_id | string | Stored on the order for your own fulfilment. |
| customer.note | string | |
| return_url | string | Absolute http(s). A "back to shop" link on the invoice. |
| metadata | object | Echoed back on every read of the order. |
Returns 201. On 409 checkout_failed the message names the
variant and why — out of stock, over the per-order cap, or wearing a non-sellable
status.
Orders
Newest first. ?limit= (max 100), ?offset=,
?status=.
{ "data": [ … ], "count": 25, "total": 431, "limit": 25, "offset": 0 }
One invoice by its public id. Once paid, the response gains a
delivered array holding the actual keys — so you can show them in your
own UI as well as on the invoice page.
{
"data": {
"id": "2145512-1-213123-2-2323",
"status": "delivered",
"total": "24.99",
"provider": "stripe",
"paid_at": "2026-09-08T04:01:38.791Z",
"items": [
{ "sku": "NITRO-3M", "product": "Nitro", "variant": "3 Months", "quantity": 1 }
],
"delivered": [
{ "product": "Nitro", "variant": "3 Months", "content": "AAAA-BBBB-CCCC" }
]
}
}
Closes an open invoice and releases its reserved stock. Optional
{ "reason": "…" }. Returns 409 if it's already settled.
Order lifecycle
| Status | Meaning |
|---|---|
| pending | Invoice open, stock reserved, buyer hasn't picked a method. |
| awaiting_payment | Method chosen; waiting on the gateway or the buyer. |
| processing | Buyer submitted proof — a crypto TXID or voucher code — and the shop needs to confirm it. |
| paid | Money landed. |
| delivered | Paid, and keys handed to the buyer. Terminal for serials. |
| expired | The window closed. Stock released. |
| cancelled | Cancelled by the buyer, the shop or the API. Stock released. |
| refunded | Recorded as refunded in the dashboard. |
paid within seconds. Without one, it settles when the buyer returns to
the invoice page or when you approve it by hand.
Key delivery
Keys are handed over on the invoice page, the instant payment clears. There is no delivery email. That makes buying fast, and it makes the invoice URL the only copy of the licence.
What stops a buyer losing it
- The browser remembers. Every invoice a device opens is recorded in local storage and listed at /orders. Covers the common case of closing the tab.
- The page pushes them to save it. After payment the invoice shows the keys with copy-all and download-as-text, plus the link with its own copy button.
- You can re-send it. Your dashboard order list searches by email, and the order page has a one-click copy of the buyer's link.
Why there is no "look up my orders by email"
Because it would work for anyone who knows the address. With licence keys behind it, an email box on a public page is an enumeration tool. Recovery therefore runs through the shop, who already knows who their buyer is.
Pulling keys into your own UI
If you would rather show keys on your own site, read the order back once it is paid —
the delivered array carries them:
curl https://jester-guru.vercel.app/api/v1/orders/2145512-1-213123-2-2323 \
-H "Authorization: Bearer $JESTER_KEY"
# "delivered": [ { "product": "Rust Cheat", "variant": "1 Day",
# "content": "rust-key-1" } ]
Subscribe to order.paid on a Discord webhook, or poll the order,
and you can store the key against your own user account so the buyer never depends on the
invoice link at all.
Payment providers
Enable these in Payment methods. Buyers only ever see the ones that are switched on and correctly configured.
| Provider | Flow | Confirmation |
|---|---|---|
| Stripe
Cards, Apple Pay and Google Pay via Stripe Checkout. |
Hosted redirect | Signed webhook — instant |
| Square
Card checkout through a Square hosted payment link. |
Hosted redirect | Signed webhook — instant |
| OVGC
Gift-card / voucher codes, redeemed manually or through your redemption endpoint. |
On-page instructions | Confirmed in the dashboard |
| Crypto
Direct-to-wallet BTC, ETH, LTC, SOL, USDT, USDC or XMR. Non-custodial. |
On-page instructions | Confirmed in the dashboard |
| MoneyMotion
Hosted checkout via moneymotion.io. |
Hosted redirect | Signed webhook — instant |
Discord events
Point one or more webhooks at your channels in Discord, and pick which events each one carries.
| Event | Fires when |
|---|---|
| order.created | Fires the moment a checkout link is generated. |
| order.paid | The one you want. Fires when money lands. |
| order.delivered | Fires when stock is handed to the buyer. |
| order.cancelled | Abandoned or timed-out invoices. |
| order.refunded | Money went back out. |
| stock.low | A product dipped under its threshold. |
Embeds carry the invoice id, total, method, line items and — if you leave
include the buyer on — a masked email like bu•••••@mail.com.
Delivery attempts are logged, so a webhook that stops working is visible rather than
silent.
Object shapes
Product
{
"id": 1,
"slug": "nitro-s8qf",
"name": "Nitro",
"description": "Discord Nitro keys",
"image_url": null,
"active": true,
"currency": "USD",
"price_from": "9.99",
"status": { "id": 1, "name": "In Stock", "color": "#22c55e", "sellable": true },
"variants": [ … ]
}
Variant
{
"id": 1,
"sku": "NITRO-1M",
"name": "1 Month",
"price": "9.99",
"price_cents": 999,
"currency": "USD",
"active": true,
"stock_mode": "serials",
"stock_available": 12, // null when unlimited
"unlimited": false,
"low_stock": false,
"max_per_order": 5,
"status": null // null means it inherits the product's
}
Order
{
"id": "2145512-1-213123-2-2323",
"status": "delivered",
"currency": "USD",
"subtotal_cents": 2499,
"fee_cents": 0,
"total_cents": 2499,
"total": "24.99",
"provider": "stripe",
"checkout_url": "https://jester-guru.vercel.app/invoice/2145512-1-213123-2-2323",
"customer": { "email": "buyer@example.com", "name": null, "discord_id": null },
"items": [ { "sku": "…", "product": "…", "variant": "…", "quantity": 1,
"unit_price_cents": 2499, "total_cents": 2499 } ],
"delivered": [ { "product": "…", "variant": "…", "content": "KEY-HERE" } ],
"metadata": { },
"return_url": "https://yourshop.com/thanks",
"expires_at": "…", "paid_at": "…", "delivered_at": "…", "created_at": "…"
}
Invoice ids
Five digit groups, 7-1-6-1-4, e.g.
2145512-1-213123-2-2323. Around 1017 of space, so URLs can't be
enumerated and the id leaks nothing about how many orders you've taken.