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.
Rate limits
| Scope | Limit |
|---|---|
| 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).
| Code | HTTP | Meaning |
|---|---|---|
invalid_key | 401 | Missing, invalid, or revoked API key. |
rate_limited | 429 | Rate limit exceeded — wait per Retry-After. |
not_found | 404 | Resource does not exist or is not yours. |
insufficient_balance | 402 | Balance too low (includes required and balance). |
out_of_stock | 409 | Quantity unavailable (includes available). |
quantity_invalid | 400 | Invalid quantity or request body. |
idempotency_key_required | 400 | Idempotency-Key header missing or longer than 64 chars. |
idempotency_conflict | 409 | Another request with the same key is in flight — retry shortly. |
product_unavailable | 400 | Product cannot be purchased right now. |
invalid_webhook_url | 400 | Webhook URL rejected (see URL rules below). |
internal_error | 500 | Internal error — retry later. |
Service health check — no authentication. Returns {"status": "ok"}.
Account
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
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}
]
}
]
| Field | Description |
|---|---|
price_usd | Unit 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_tiers | Bulk pricing tiers if the product has them: order quantity ≥ min_qty ⇒ unit price becomes unit_price. null when absent. |
daily_tiers | Daily 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. |
stock | Real purchasable stock right now. null = unlimited, or unknown but still sellable. |
unlimited | true when the product has no stock cap. |
instant | true = automatic instant delivery. false = manual handling or extra data required — the order stays processing until completed. |
activation_url | Final resolved activation/redemption link for the product, or null. |
description_ar/description_en | Product description with the activation-link variable resolved. |
A single product, same shape. Missing or inactive → 404 not_found.
Purchase
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
}
- If the order comes back as
processing, delivery is completing — pollGET /v1/orders/{id}or wait for the webhook. The response also carries"delivery": "pending"in that case. This happens when the upstream supplier is slow: your order is already accepted and paid, the codes follow within a few minutes, and if the supplier fails to deliver in time the full amount is refunded to your balance automatically (you get arefundedwebhook). refundedmeans delivery failed and the full amount was automatically returned to your balance.- The delivery message is also sent to the Telegram account linked to your key; file items (
type: "file") are delivered via Telegram only. - Pricing is computed at execution time with your own rules (negotiated price, or bulk tiers by quantity).
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.
- Re-sending the same key after a successful purchase returns the same order with no new charge, flagged
"idempotent_replay": true. - If two requests with the same key race, exactly one wins; the other gets
409 idempotency_conflict— retry shortly to receive the original result. - Use a fresh key for every new purchase.
Orders
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... } ]
}
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
| Status | Meaning |
|---|---|
| processing | Being executed or delivered — poll the order or wait for the webhook. |
| delivered | Delivered — codes are in items. |
| refunded | Delivery failed; the full amount was automatically credited back. |
| cancelled | Cancelled before payment — nothing charged. |
Wallet ledger
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.
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_..."}
Returns the current URL and its status (never the secret).
Disables notifications.
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"])
key_prefix and order id — and never share your full API key with anyone.