Digital Store — Reseller APIv1
Public documentation

Reseller API

Connect your store or system directly to ours: browse products at your personal prices, purchase automatically from your wallet balance, receive codes instantly in the response — with webhook notifications when orders reach a final state.

Base URL: https://api.jayseller.store

Getting a key: in the Telegram bot @jayseller_bot → 🔌 API button → “Create API key”. The key is shown once only — store it immediately. Regenerating instantly revokes the old key. Note: regenerating your key also deactivates the webhook registered on it — re-register your webhook URL with the new key.

Top-up: wallet top-up happens in the bot only (💰 Balance). There is no deposit endpoint in the API — the balance is shared between the bot and the API.

Contents

Authentication

All /v1/* endpoints require your key in the X-API-Key header (primary) or Authorization: Bearer — both are accepted:

curl https://api.jayseller.store/v1/me \
  -H "X-API-Key: sapi_YOUR_KEY"

# or Bearer style:
curl https://api.jayseller.store/v1/me \
  -H "Authorization: Bearer sapi_YOUR_KEY"

Keys always start with sapi_. A missing, invalid, or revoked key returns 401 with code invalid_key.

Keep the key secret — never embed it in front-end code or public repositories. If you suspect a leak, regenerate it from the bot immediately; the old key stops working the same instant.

Rate limits

ScopeLimit
All requests (per key)120 requests / minute
POST /v1/buy (per key)20 requests / minute
Failed auth attempts (per IP)30 / minute

Exceeding a limit returns 429 with code rate_limited and a Retry-After header (seconds to wait).

Errors

Every non-2xx response uses the same shape, with messages in both English and Arabic:

{
  "error": {
    "code": "insufficient_balance",
    "message_en": "Insufficient wallet balance.",
    "message_ar": "رصيد المحفظة غير كافٍ لإتمام العملية. يمكنك شحن الرصيد من البوت."
  },
  "required": 9.5,
  "balance": 4.0
}

Context fields may appear at the top level next to error (like available, or required/balance above).

CodeHTTPMeaning
invalid_key401Missing, invalid, or revoked API key.
rate_limited429Rate limit exceeded — wait per Retry-After.
not_found404Resource does not exist or is not yours.
insufficient_balance402Balance too low (includes required and balance).
out_of_stock409Quantity unavailable (includes available).
quantity_invalid400Invalid quantity or request body.
idempotency_key_required400Idempotency-Key header missing or longer than 64 chars.
idempotency_conflict409Another request with the same key is in flight — retry shortly.
product_unavailable400Product cannot be purchased right now.
invalid_webhook_url400Webhook URL rejected (see URL rules below).
internal_error500Internal error — retry later.
GET/health

Service health check — no authentication. Returns {"status": "ok"}.

Account

GET/v1/me
curl https://api.jayseller.store/v1/me -H "X-API-Key: sapi_YOUR_KEY"
{
  "tg_id": 123456789,
  "name": "Ahmed",
  "balance": 250.0,
  "currency": "USD",
  "key_prefix": "sapi_ab12",
  "created_at": "2026-07-14T12:00:00Z"
}

balance is your shared wallet balance (same one the bot uses); created_at is when your current key was created.

Catalog

GET/v1/products

All active products. Prices are personalized: if you have a negotiated price for a product, that is what is shown and charged.

curl https://api.jayseller.store/v1/products -H "X-API-Key: sapi_YOUR_KEY"
[
  {
    "id": 17,
    "category": {"id": 3, "name_ar": "بطاقات الألعاب", "name_en": "Game Cards"},
    "name_ar": "بطاقة 100 جوهرة",
    "name_en": "100 Gems Card",
    "description_ar": "تسليم فوري. التفعيل عبر: https://redeem.example.com/abc",
    "description_en": "Instant delivery. Redeem at: https://redeem.example.com/abc",
    "stock": 42,
    "unlimited": false,
    "instant": true,
    "activation_url": "https://redeem.example.com/abc",
    "price_usd": 4.75,
    "bulk_tiers": [
      {"min_qty": 5,  "unit_price": 4.5},
      {"min_qty": 20, "unit_price": 4.25}
    ]
  }
]
FieldDescription
price_usdUnit price for your account — exactly what a quantity-1 purchase would charge. If you have a negotiated flat price it overrides bulk tiers (and bulk_tiers comes back null).
bulk_tiersBulk pricing tiers if the product has them: order quantity ≥ min_qty ⇒ unit price becomes unit_price. null when absent.
daily_tiersDaily volume discount, if the product has it: when the total pieces you buy of this product within one day (09:00 AM → 01:00 AM Cairo local time — UTC+3 in summer, UTC+2 in winter — bot and API purchases combined; purchases between 01:00 and 09:00 are not counted) reach min_daily_qty, every piece of that day costs unit_price. New orders are charged the discounted price directly, and the difference on your earlier orders of the same day is credited back to your balance automatically. null when absent or when you have a negotiated price.
stockReal purchasable stock right now. null = unlimited, or unknown but still sellable.
unlimitedtrue when the product has no stock cap.
instanttrue = automatic instant delivery. false = manual handling or extra data required — the order stays processing until completed.
activation_urlFinal resolved activation/redemption link for the product, or null.
description_ar/description_enProduct description with the activation-link variable resolved.
GET/v1/products/{id}

A single product, same shape. Missing or inactive → 404 not_found.

Purchase

POST/v1/buy

Creates an order, charges your wallet balance, and delivers immediately for instant products. The Idempotency-Key header is required (1–64 characters).

curl -X POST https://api.jayseller.store/v1/buy \
  -H "X-API-Key: sapi_YOUR_KEY" \
  -H "Idempotency-Key: my-order-10021" \
  -H "Content-Type: application/json" \
  -d '{"product_id": 17, "quantity": 2}'

Successful instant delivery response:

{
  "order": {
    "id": 5120,
    "via": "api",
    "product_id": 17,
    "product_name_ar": "بطاقة 100 جوهرة",
    "product_name_en": "100 Gems Card",
    "quantity": 2,
    "price_usd": 9.5,
    "status": "delivered",
    "created_at": "2026-07-14T18:03:21Z",
    "delivered_at": "2026-07-14T18:03:24Z",
    "items": [
      {"type": "text", "content": "GEM-AB12-CD34"},
      {"type": "text", "content": "GEM-EF56-GH78"}
    ],
    "activation_url": "https://redeem.example.com/abc"
  },
  "idempotent_replay": false
}

Idempotency

To protect you from double purchases on network failures and retries, send a unique value of your own (e.g. your internal order id) in the Idempotency-Key header with every purchase.

Orders

GET/v1/orders?limit=&offset=

Your own orders only, newest first. limit defaults to 20, max 100.

curl "https://api.jayseller.store/v1/orders?limit=20&offset=0" \
  -H "X-API-Key: sapi_YOUR_KEY"
{
  "total": 154,
  "orders": [ { ...same order shape as /v1/buy... } ]
}
GET/v1/orders/{id}

A single order. The via field marks the origin: api or bot. items appear for delivered orders only: text codes as {"type":"text","content":"..."}, files as {"type":"file", "note_en":"delivered via Telegram", "note_ar":"تم التسليم عبر تيليجرام"}.

Order statuses

StatusMeaning
processingBeing executed or delivered — poll the order or wait for the webhook.
deliveredDelivered — codes are in items.
refundedDelivery failed; the full amount was automatically credited back.
cancelledCancelled before payment — nothing charged.

Wallet ledger

GET/v1/ledger?limit=&offset=

Your wallet movement history (top-ups, purchases, refunds…) — an immutable append-only ledger, newest first.

{
  "total": 89,
  "entries": [
    {
      "amount": -9.5,
      "balance_after": 240.5,
      "kind": "purchase",
      "note": "طلب #5120",
      "created_at": "2026-07-14T18:03:22Z"
    }
  ]
}

Webhooks & signatures

Instead of polling, register a URL and we will notify you when any API order reaches a final state.

POST/v1/webhook
curl -X POST https://api.jayseller.store/v1/webhook \
  -H "X-API-Key: sapi_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/store-hook"}'

# Response — the secret is shown ONCE, and rotates on every registration:
{"url": "https://example.com/store-hook", "secret": "whsec_..."}
GET/v1/webhook

Returns the current URL and its status (never the secret).

DELETE/v1/webhook

Disables notifications.

URL rules: https:// only, port 443 only, no userinfo in the URL, and the hostname must resolve to a public address (loopback/private/link-local ranges are rejected). Violations return invalid_webhook_url.

Events & payload

Events: order.delivered and order.refunded. Delivered as a POST with a JSON body:

{
  "event": "order.delivered",
  "order": { ...same order shape as GET /v1/orders/{id}... },
  "sent_at": "2026-07-14T18:03:25Z"
}

If your endpoint is unreachable we retry up to 5 times with increasing backoff (30s → 2h). Any 2xx response counts as received.

Verifying the signature

Every notification carries an X-Store-Signature header of the form sha256=<hex> — the HMAC-SHA256 of the raw request body with your secret. Always verify before processing:

import hashlib
import hmac

WEBHOOK_SECRET = "whsec_..."  # the secret you received at registration

def verify_signature(raw_body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

# FastAPI example:
# @app.post("/store-hook")
# async def hook(request: Request):
#     raw = await request.body()
#     if not verify_signature(raw, request.headers.get("X-Store-Signature", "")):
#         return Response(status_code=401)
#     event = json.loads(raw)
#     ...

Full Python example

import uuid
import httpx

BASE = "https://api.jayseller.store"
KEY = "sapi_YOUR_KEY"
H = {"X-API-Key": KEY}

with httpx.Client(base_url=BASE, headers=H, timeout=30) as client:
    # 1) my balance
    me = client.get("/v1/me").json()
    print("balance:", me["balance"], me["currency"])

    # 2) available products
    products = client.get("/v1/products").json()
    p = next(x for x in products if x["stock"] is None or x["stock"] > 0)

    # 3) buy one unit (with a unique idempotency key)
    r = client.post(
        "/v1/buy",
        headers={"Idempotency-Key": str(uuid.uuid4())},
        json={"product_id": p["id"], "quantity": 1},
    )
    data = r.json()
    if r.status_code == 200:
        order = data["order"]
        if order["status"] == "delivered":
            codes = [i["content"] for i in order["items"] if i["type"] == "text"]
            print("codes:", codes)
        else:
            print("processing — poll /v1/orders/", order["id"])
    else:
        print("error:", data["error"]["code"], data["error"]["message_en"])
Support: inside the bot @jayseller_bot → Support button. Mention your key_prefix and order id — and never share your full API key with anyone.