Jester.guru

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.

The whole integration is two calls. Create a checkout, then read the order back when you want to know how it went — or let a Discord webhook tell you.

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.

ScopeGrants
products:readList and fetch products, variants and stock levels.
products:writeCreate and edit products, variants, statuses; push stock.
orders:readList invoices, read their state and delivered keys.
orders:writeOpen checkouts and cancel invoices.
Server-side only. A key is full write access to your shop. Never ship one in browser JavaScript or a mobile app — call this API from your backend and let your own frontend talk to that.

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."
  }
}
StatusMeaning
400Malformed request — usually bad JSON.
401Missing, malformed, revoked or unknown API key.
403Key lacks the scope this endpoint needs.
404No such product, variant or invoice on your shop.
409Conflict — SKU taken, out of stock, invoice already closed.
422Understood but invalid — bad price, malformed SKU, unknown status.
429Rate limited. Retry-After says how long to wait.
5xxOur fault. Safe to retry with backoff.
409 on checkout is normal. It means someone bought the last one between your stock check and your checkout call. Surface it to the buyer and refresh your stock — don't retry blindly.

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.
You order variants, not products. The sku in a checkout call resolves to a variant. SKUs are unique across your whole shop.

Stock modes

ModeBehaviour
serialsA pool of individual keys. One is reserved per unit ordered and handed to the buyer on payment. This is the default.
countJust a number. Decremented on order, restored if it lapses. Nothing is auto-delivered — you fulfil it yourself.
unlimitedNever 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

GET /me no scope required

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" }]
}
GET /payment-methods

Enabled, correctly-configured methods in display order — what the checkout page will actually show.

Products

GET /products products:read

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"
GET /products/:idOrSlug products:read

One product by numeric id or slug.

POST /products products:write

Create a product, its variants and their opening stock in a single call.

FieldTypeNotes
name *stringListing name.
descriptionstringShown on the checkout page.
image_urlstringAbsolute http(s) URL.
statusstringStatus name, e.g. "In Stock". Or status_id.
activebooleanDefaults true.
variantsarraySee below. Omit for a single-variant product and pass price/sku at the top level instead.

Variant fields

namestringDefaults to "Standard".
price *stringDecimal, e.g. "9.99". Or price_cents as an integer.
skustringAuto-generated if omitted. Unique per shop.
stock_modestringserials · count · unlimited
stockstring[]Opening keys, serials mode only.
max_per_orderintegerDefaults 10.
low_stock_atintegerDiscord 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" }
    ]
  }'
PATCH /products/:id products:write

Update listing fields: name, description, image_url, status, active, sort_order. Price and stock live on variants.

DELETE /products/:id products:write

Deletes the product and its variants. Returns 409 product_in_use if it sits on an open invoice — settle or cancel that first.

Variants

GET /variants/:idOrSku products:read

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"
POST /products/:id/variants products:write

Add a variant to an existing product. Same fields as the variant array above.

PATCH /variants/:id products:write

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" }'
DELETE /variants/:id products:write

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

POST /variants/:id/stock products:write

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 }
Re-posting a list is safe. Keys already sitting unsold are skipped rather than duplicated, so a restock script can be idempotent without tracking what it sent last time.

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.

GET /categories products:read
{
  "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.

GET /products?category=rust products:read

Filter the product list by category id or slug — one section of your storefront.

POST /categories products:write
name *string1-60 characters, unique per shop. The slug is derived.
descriptionstring
image_urlstring
sort_orderintegerLower 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.

GET /statuses products:read
{
  "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 }
  ]
}
POST /statuses products:write
name *string1–32 characters, unique per shop.
color *string6-digit hex, e.g. #22c55e.
sellablebooleanDefaults true. False blocks orders.

Checkout

POST /checkout orders:write

Prices the order, reserves the stock, and returns a URL to send the buyer to.

FieldTypeNotes
items *array{ sku, quantity } or { variant_id, quantity }. Max 50 lines.
customer.emailstringPre-fills the gateway and appears (masked) in Discord.
customer.namestring
customer.discord_idstringStored on the order for your own fulfilment.
customer.notestring
return_urlstringAbsolute http(s). A "back to shop" link on the invoice.
metadataobjectEchoed 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.

Invoices expire. Default is 60 minutes (configurable in shop settings). On expiry the reserved stock goes back on the shelf and the invoice can't be paid. Create it when the buyer is ready to pay, not when they add to cart.

Orders

GET /orders orders:read

Newest first. ?limit= (max 100), ?offset=, ?status=.

{ "data": [ … ], "count": 25, "total": 431, "limit": 25, "offset": 0 }
GET /orders/:id orders:read

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" }
    ]
  }
}
POST /orders/:id/cancel orders:write

Closes an open invoice and releases its reserved stock. Optional { "reason": "…" }. Returns 409 if it's already settled.

Order lifecycle

Pending Awaiting payment Paid Delivered
StatusMeaning
pendingInvoice open, stock reserved, buyer hasn't picked a method.
awaiting_paymentMethod chosen; waiting on the gateway or the buyer.
processingBuyer submitted proof — a crypto TXID or voucher code — and the shop needs to confirm it.
paidMoney landed.
deliveredPaid, and keys handed to the buyer. Terminal for serials.
expiredThe window closed. Stock released.
cancelledCancelled by the buyer, the shop or the API. Stock released.
refundedRecorded as refunded in the dashboard.
Poll or subscribe, don't guess. An order with a signed gateway webhook goes 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.

The invoice URL is the receipt. Anyone holding it can read the keys, and a buyer who loses it has lost their way back. Both halves of that matter.

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.

ProviderFlowConfirmation
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
Stripe and Square need their webhook registered to settle instantly. The endpoint URL is shown on the payment methods page. Without it, an order only settles when the buyer lands back on the invoice.

Discord events

Point one or more webhooks at your channels in Discord, and pick which events each one carries.

EventFires when
order.createdFires the moment a checkout link is generated.
order.paidThe one you want. Fires when money lands.
order.deliveredFires when stock is handed to the buyer.
order.cancelledAbandoned or timed-out invoices.
order.refundedMoney went back out.
stock.lowA 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.

The invoice URL is the buyer's receipt. Anyone holding it can see the delivered keys. Treat it like a password — don't put it anywhere public.