AIX Store API v1 Home

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_idonly this category
qsearch name and description
limit offsetpaging (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.typeavailablemax_quantity
limiteditems leftthat many, up to 500
unlimitednull1 (sold one at a time)
activationnull1 (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.

  1. POST /orders → gives you activation.id and the instructions
  2. POST /activations/{id}/details with {"details": "email / password"}
  3. 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 } }
CodeHTTPMeaning
unauthorized401key missing or wrong
key_revoked403key was revoked in the bot
insufficient_scope403key not allowed to do this
not_your_account403tried to act on someone else
rate_limited429too many requests — slow down
product_not_found404no such product
product_unavailable409product is off sale
out_of_stock / insufficient_stock409none left, or fewer than asked
insufficient_balance402top up your wallet first
invalid_quantity / missing_field400bad 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.