Shop API
Buy digital products straight from your own code. Browse the catalog, check a price, and place an order against your wallet balance — the same catalog, the same prices, the same discounts you see in the bot.
Base URL: /api/v1
Get a key
Open the bot, go to My Profile → API Access → Create a key. The key is shown once — copy it and keep it somewhere safe.
Your key is yours: it can browse the shop, check prices, and buy for your own account. It cannot touch anyone else's balance, and you can revoke it from the same screen at any time. Use it from anywhere.
Send it on every request:
Authorization: Bearer aix_your_key_here
Quick start
KEY=aix_your_key_here
BASE=https://shop.example.com/api/v1
# what's for sale
curl -H "Authorization: Bearer $KEY" "$BASE/products"
# what a purchase would cost you
curl -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"product_id": 1, "quantity": 2}' "$BASE/quote"
# buy it
curl -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"product_id": 1, "quantity": 2}' "$BASE/orders"
You never send your own user id — the key already knows who you are.
Browse the catalog
GET/categories
The groups products are organised into.
GET/products
Everything on sale, priced for you.
| Query | |
|---|---|
category_id | only this category |
q | search name and description |
limit offset | paging (default 50, max 200) |
GET/products/{id}
One product in full — price, stock, terms, live discounts, and the bulk price ladder.
{
"id": 1, "name": "Netflix 1M", "delivery_mode": "limited",
"list_price": 10.0,
"price": { "quantity": 3, "unit": 7.0, "total": 21.0,
"saved": 9.0, "discounted": true, "label": "3+ bulk price" },
"stock": { "type": "limited", "available": 42, "in_stock": true },
"terms": { "duration": "1 month", "warranty": "7 days warranty" },
"bulk_tiers": [ { "min_quantity": 3, "unit_price": 7.0 } ],
"discounts": [
{ "name": "Summer Sale", "label": "Summer Sale — 20% off",
"unit_price": 8.0, "conditions": {}, "qualifies": true }
],
"max_quantity": 42
}
qualifies tells you whether you can use a discount right now. Offers never stack — the cheapest one you qualify for is already in price.
GET/discounts
Every live offer and what unlocks it.
Stock, by product type
stock.type | available | max_quantity |
|---|---|---|
limited | items left | that many, up to 500 |
unlimited | null | 1 (sold one at a time) |
activation | null | 1 (sold one at a time) |
Buy
POST/quote
Price a basket without buying anything.
{ "product_id": 1, "quantity": 3 }
{ "total": 21.0, "unit_price": 7.0, "saved": 9.0, "discount": "3+ bulk price",
"balance": 50.0, "affordable": true, "shortfall": 0 }
POST/orders
Buy, charged to your wallet balance.
{ "product_id": 1, "quantity": 2 }
Always send an Idempotency-Key header. If the connection drops and you retry, the first result comes back instead of charging you twice:
Idempotency-Key: 3f9c1e7a-6b2d-4c9a-8e1f-0a2b4d6e8c0a
The price is worked out on the server — a price you send is ignored.
Instant products (limited, unlimited) hand the goods back in items:
{ "order": {
"id": 1041, "delivery_mode": "limited", "quantity": 2, "charged": 16.0,
"unit_price": 8.0, "saved": 4.0, "discount": "Summer Sale — 20% off",
"items": ["user0@mail.com:pass0", "user1@mail.com:pass1"],
"partial": false,
"terms": { "duration": "1 month", "warranty": "7 days warranty" } } }
If only part of the stock was left, partial is true, quantity is what you actually got, and the difference is refunded automatically.
Activation products need a step from you — a request is opened instead:
{ "order": {
"id": 1042, "delivery_mode": "activation", "charged": 16.0, "items": [],
"activation": { "id": 88, "status": "awaiting",
"instructions": "Add our mail as a family member.",
"next": "POST /api/v1/activations/88/details" } } }
POST/orders/bulk
Buy several products in one call — a whole basket at once.
{ "lines": [ { "product_id": 1, "quantity": 2 },
{ "product_id": 7, "quantity": 1 },
{ "product_id": 9 } ] }
Each line is filled on its own. The ones in stock are delivered and charged; the ones that can't be filled are skipped and reported — the basket is never cancelled because one line failed. Up to 50 lines per call. Send an Idempotency-Key header, exactly like POST /orders, and a retry replays the first outcome instead of charging the filled lines twice.
The reply lists what went through in delivered (each entry is the same shape as a single POST /orders result) and what didn't in failed:
{ "delivered": [
{ "id": 1041, "product_id": 1, "quantity": 2, "charged": 16.0,
"items": ["user0@mail.com:pass0", "user1@mail.com:pass1"], "partial": false },
{ "id": 1042, "product_id": 9, "quantity": 1, "charged": 5.0, "items": ["code-xyz"] }
],
"failed": [
{ "product_id": 7, "quantity": 1,
"error": { "code": "out_of_stock", "message": "That product is sold out.",
"available": 0 } }
],
"summary": { "lines": 3, "delivered": 2, "failed": 1, "charged": 21.0 } }
The status is 201 when every line was filled, or 207 Multi-Status when some were skipped — so a 207 is your cue to read failed.
GET/orders/{id}
An order you placed, with its delivered content.
Activation products
Some products are set up by hand: you send account details, the shop activates it, and the result comes back to you in the bot.
POST /orders→ gives youactivation.idand the instructionsPOST /activations/{id}/detailswith{"details": "email / password"}GET /activations/{id}to follow it
{ "activation": { "id": 88, "status": "submitted",
"details_submitted": true, "awaiting_details": false } }
status: awaiting → submitted → activated, or rejected. If it comes back resend, the shop needs different details — note says why.
Replacements
If something you bought stops working, ask for a replacement by its Order ID. A buyer key may only replace its own orders. An operator reviews it and either sends a fresh item or declines — you follow the outcome from here. Needs the orders:write scope to open one, orders:read to check it.
POST/replacements
Open a replacement for a paid order.
{ "order_id": "EXA123OK", "reason": "the login stopped working" }
order_id is your EXA-style Order ID (a numeric id also works). reason is an optional note the operator sees. If the order already has an open request you get that one back with "already_open": true — asking twice never stacks up.
{ "replacement": { "id": 3, "code": "RPL7K2M9", "order_id": 5,
"order_code": "EXA123OK", "product_name": "Netflix",
"status": "open", "already_open": false } }
code is the Replacement ID — the same one the buyer is shown in the bot. Keep it: it's how you look the request up and what support asks for.
Only a paid order can be replaced (order_not_replaceable otherwise), and an Order ID that isn't yours answers order_not_found — the same as one that does not exist, so the API never confirms other people's orders.
GET/replacements/code/{code}
Where a request stands, by its Replacement ID — e.g. GET /replacements/code/RPL7K2M9. Same body as below.
GET/replacements/{id}
Where a request stands.
{ "replacement": { "id": 3, "code": "RPL7K2M9",
"order_code": "EXA123OK", "product_name": "Netflix",
"status": "sent", "outcome": "A replacement was sent.",
"reason": "the login stopped working",
"admin_note": "here is a fresh account",
"delivery": "user@mail.com:newpass" } }
status: open → sent or rejected. When it is sent, delivery holds the fresh content and admin_note any message back; when rejected, admin_note says why.
GET/replacements
Your replacement requests, newest first. ?status=open|sent|rejected · ?limit= · ?offset=.
You
GET/me
Your balance and totals — the easiest first call.
{ "user": { "id": 7913103255, "balance": 34.00,
"orders": 12, "paid_orders": 11, "total_spent": 86.00 } }
GET/users/{id}/orders
Your order history. ?status=paid · ?limit= · ?offset=.
Errors
Every error looks the same, with a code you can branch on:
{ "error": { "code": "insufficient_balance", "message": "Not enough balance.",
"required": 16.0, "balance": 5.0, "shortfall": 11.0 } }
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | key missing or wrong |
key_revoked | 403 | key was revoked in the bot |
insufficient_scope | 403 | key not allowed to do this |
not_your_account | 403 | tried to act on someone else |
rate_limited | 429 | too many requests — slow down |
product_not_found | 404 | no such product |
product_unavailable | 409 | product is off sale |
out_of_stock / insufficient_stock | 409 | none left, or fewer than asked |
insufficient_balance | 402 | top up your wallet first |
invalid_quantity / missing_field | 400 | bad request |
insufficient_balance includes shortfall — top up that much in the bot and try again.
What keeps it safe
You do not have to configure anything. The API is safe by design:
- Your key only spends your balance. Even if it leaked, no one could touch
another account or reach the admin panel with it.
- Prices are always server-side. A tampered price in a request is ignored.
- Idempotency means a retry never double-charges.
- Rate limiting stops a runaway or leaked key being hammered.
- Revoke instantly from the bot if a key is ever exposed.
The API does one thing — sell products — and can do nothing else.