Reseller API Documentation
Sell our products on your own website, bot or panel. The AI Store Plus Reseller API gives you the same catalog, prices and automatic delivery as our Telegram store, over a simple JSON API. Orders are paid from your store balance.
https://api.aistoreplus.com/v1Authorization: Bearer rk_…JSON · UTF-8 · HTTPS onlyUSD (amounts are strings)1. Get started
- Open our Telegram store @aistoreplus_bot and press Start.
- Top up your balance in Wallet (USDT TRC20 / BEP20 or Binance Pay). API orders are paid from this balance.
- Go to Profile → Reseller API (or send
/api) and tap Create new key. - Copy the key right away. It is shown only once: we store only an encrypted fingerprint and cannot show it again. Then delete the message with the button under it.
- Call
GET https://api.aistoreplus.com/v1/mewith your key to confirm everything works.
You can have up to 3 active keys (for example one for production and one for testing) and create up to 5 keys in 24 hours. Revoke a key at any time from the same screen; it stops working immediately.
2. Authentication
Send your key in the Authorization header. X-API-Key is accepted as an alternative.
Authorization: Bearer rk_YOUR_KEY
# or
X-API-Key: rk_YOUR_KEYA missing, wrong or revoked key returns 401 unauthorized. Every response carries an X-Request-Id header; include it when you contact support.
IP restriction (recommended)
In Reseller API → IP limit you can allow a key only from your server's IP addresses or ranges (IPv4/IPv6, up to 10, e.g. 203.0.113.7, 198.51.100.0/24). Requests from any other address get 403 ip_not_allowed, so a leaked key cannot be used elsewhere.
3. Limits
| Requests | 60 per minute per key. Each response returns X-RateLimit-Limit and X-RateLimit-Remaining. Above the limit: 429 rate_limited with Retry-After: 60. |
|---|---|
| Invalid keys | After 20 requests with an invalid key from the same IP within a minute, that IP gets 429 too_many_failed_attempts for a minute. |
| Quantity | 1 to max_quantity of the product per order (25 for most products; 1 for products that need customer inputs). |
| Request body | Up to 64 KB. |
4. Account and balance
GET/meAccount, balance and key details
Returns your balance, total spent, membership tier and the key in use. /balance returns the same data.
{
"customer": "CUS-7K2M9QX4",
"balance": "125.5000",
"currency": "USD",
"total_spent": "310.0000",
"tier": "Silver",
"tier_discount": "0.0300",
"api_key": {
"prefix": "rk_AbCdEfGh",
"label": "API key 1",
"rate_limit_per_minute": 60,
"ip_restricted": true
}
}tier_discount is applied automatically to every order; the catalog shows list prices and the order response shows what you actually paid.
GET/v1API status (no key needed)
{
"name": "AI Store Plus Reseller API",
"version": "v1",
"status": "ok",
"base_url": "https://api.aistoreplus.com/v1",
"documentation": "https://aistoreplus.com/en/reseller-api/"
}5. Products
GET/productsEverything you can order
The list is exactly what our Telegram store sells right now. Products that are hidden, not yet added to the store, or delivered manually by our team are not included and cannot be ordered through the API.
{
"total": 2,
"currency": "USD",
"products": [
{
"id": 127,
"name": "Example Pro · 18 Months",
"category": "AI",
"price": "0.7000",
"currency": "USD",
"stock": 1581,
"in_stock": true,
"product_type": "code",
"warranty_days": 0,
"min_quantity": 1,
"max_quantity": 25,
"required_inputs": []
},
{
"id": 2210,
"name": "Example Team Seat · 1 Month",
"category": "AI",
"price": "14.0000",
"currency": "USD",
"stock": null,
"in_stock": true,
"product_type": "service",
"warranty_days": 0,
"min_quantity": 1,
"max_quantity": 1,
"required_inputs": [
{ "key": "customer_email", "label": "Email address (the invitation is sent here)" }
]
}
]
}| price | List price per unit in USD, as a string with 4 decimals. Never parse it as a float in money calculations. |
|---|---|
| stock | Units available. null means the stock is not reported (the product is still orderable); 0 means sold out. |
| in_stock | true if you can order now. |
| product_type | ready_account, code, coupon or service. |
| required_inputs | Information the product needs from your customer (see Required inputs). Empty for most products. |
| max_quantity | Highest quantity allowed in one order. |
Prices and stock change during the day. Refresh the catalog every few minutes and check in_stock and price right before you sell.
GET/products/{id}One product with its description
Same fields plus description. A product that is not in the store returns 404 product_not_found.
6. Place an order
POST/ordersBuy from your balance
| product_id | Integer, required. From /products. |
|---|---|
| quantity | Integer, required. 1 to the product's max_quantity. |
| idempotency_key | String, required, 1–64 characters. Your own unique ID for this order (for example your order number). |
| inputs | Object, only for products with required_inputs: {"customer_email": "buyer@example.com"}. |
{"product_id": 127, "quantity": 2, "idempotency_key": "my-order-10045"}{
"order": {
"order_code": "ORD-C19QV1RX",
"status": "completed",
"status_detail": "delivered",
"product_id": 127,
"product_name": "Example Pro · 18 Months",
"quantity": 2,
"delivered_quantity": 2,
"unit_price": "0.7000",
"total": "1.4000",
"refunded": "0.0000",
"currency": "USD",
"created_at": "2026-10-28T10:15:02.123456+00:00",
"delivered_at": "2026-10-28T10:15:03.004512+00:00",
"items": [
"https://example.com/activate/AAAA-BBBB",
"https://example.com/activate/CCCC-DDDD"
],
"instructions": "Open the link while signed in to your own account.",
"outcome": "delivered"
},
"replay": false
}Idempotency: safe retries
If your connection drops, send the same request with the same idempotency_key. You will get the original order back with status 200 and "replay": true; you are never charged twice. Use a new key for every new order. Never reuse a key for a different product or quantity.
What happens to your money
- The order total is taken from your balance when the order is created. A
402 insufficient_balancemeans nothing was charged. - Most products are delivered instantly: the delivered accounts, codes or links are in
items, and the activation note (if any) ininstructions. - Some products (invitations, upgrades) are completed by the provider a little later: the order stays
processing; poll order status. - If an order cannot be fulfilled, the amount is refunded to your balance automatically and shown in
refunded. If only part is delivered, the rest is refunded (partial).
| outcome | delivered all delivered · partial partly delivered, rest refunded · failed not delivered, fully refunded · supplier accepted, completed later · needs_review result unclear, our team checks it (no refund yet) · replay same idempotency_key, original order returned |
|---|
7. Order status and history
GET/orders/{order_code}One order, with delivered items
Returns the order in the same shape as the order response (with items and instructions). Orders of other accounts return 404 not_found.
| status | processing in progress · completed delivered · partial partly delivered, rest refunded · refunded fully refunded · failed not delivered · cancelled cancelled |
|---|---|
| status_detail | The internal state, for logs and support. Build your logic on status. |
While an order is processing, poll every 1–2 minutes. Most deferred orders finish within minutes; invitations can take a few hours.
GET/orders?limit=20&offset=0Your orders, newest first
limit 1–100 (default 20). The list does not include delivered items; fetch a single order for those.
{
"total": 37,
"limit": 20,
"offset": 0,
"orders": [
{
"order_code": "ORD-C19QV1RX",
"status": "completed",
"status_detail": "delivered",
"product_id": 127,
"product_name": "Example Pro · 18 Months",
"quantity": 2,
"delivered_quantity": 2,
"unit_price": "0.7000",
"total": "1.4000",
"refunded": "0.0000",
"currency": "USD",
"created_at": "2026-10-28T10:15:02.123456+00:00",
"delivered_at": "2026-10-28T10:15:03.004512+00:00"
}
]
}8. Webhooks (order status notifications)
Instead of polling, let us call your server when an API order is finished. Register one HTTPS URL per account; for every order you place through the API we send a signed POST when it reaches a final status.
PUT/webhookSet (or change) your webhook URL
{"url": "https://example.com/aistore/webhook"}{
"webhook": {
"url": "https://example.com/aistore/webhook",
"created_at": "2026-10-28T13:05:12+00:00",
"last_success_at": null,
"last_failure_at": null,
"last_error": null,
"consecutive_failures": 0
},
"secret": "whsec_k3Jx9Qm2LpV7tR8wYz4AbCdEfGhIjKlMnOpQrStUvWx"
}The secret is returned only once, when the webhook is created. Store it securely; you need it to verify signatures. Send "rotate_secret": true to get a new secret (the old one stops working immediately). Changing only the URL keeps the secret.
GET/webhookCurrent URL and delivery health
POST/webhook/testSend a signed ping now
{"delivered": true, "status": 200, "error": null}DELETE/webhookRemove the webhook
Events
| order.completed | All units delivered; order.items contains them. |
|---|---|
| order.partial | Some units delivered, the rest refunded to your balance. |
| order.refunded | Not delivered, fully refunded. |
| order.failed | Not delivered. |
| order.cancelled | Cancelled. |
| ping | Sent only by POST /webhook/test; order is null. |
The body carries the order in exactly the same shape as GET /orders/{order_code}:
{
"id": "evt_1842",
"event": "order.completed",
"created_at": "2026-10-28T13:10:41+00:00",
"order": {
"order_code": "ORD-C19QV1RX",
"status": "completed",
"status_detail": "delivered",
"product_id": 127,
"product_name": "Example Pro · 18 Months",
"quantity": 1,
"delivered_quantity": 1,
"unit_price": "0.7000",
"total": "0.7000",
"refunded": "0.0000",
"currency": "USD",
"created_at": "2026-10-28T13:10:38+00:00",
"delivered_at": "2026-10-28T13:10:39+00:00",
"items": ["https://example.com/activate/AbC123"],
"instructions": "Open the link and sign in with your own account."
}
}Headers and signature
| X-AIStore-Event | Event name, e.g. order.completed. |
|---|---|
| X-AIStore-Delivery | Unique event ID (evt_…). The same event can arrive more than once; skip IDs you have already processed. |
| X-AIStore-Timestamp | Unix time (seconds) when we signed the request. |
| X-AIStore-Signature | v1= + hex HMAC-SHA256 of timestamp + "." + raw body, keyed with your secret. |
import hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
timestamp = headers["X-AIStore-Timestamp"]
if abs(time.time() - int(timestamp)) > 300: # older than 5 minutes
return False
expected = "v1=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, headers["X-AIStore-Signature"])<?php
$secret = getenv('AISTORE_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // raw body, before json_decode
$timestamp = $_SERVER['HTTP_X_AISTORE_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_AISTORE_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
if (abs(time() - (int) $timestamp) > 300 || !hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true); // $event['event'], $event['order']
http_response_code(200);Delivery and retries
- Answer with any
2xxstatus within 10 seconds. The response body is ignored and redirects are not followed. - Otherwise we retry after 30 s, 2 min, 10 min, 30 min, 2 h, 6 h and 12 h (8 attempts in total), then give up.
GET /webhookshows the last error. - Only orders placed through the API after you set the webhook are sent. You can always check an order with
GET /orders/{order_code}. - The URL must be
https://and resolve to a public address; local, private and internal addresses are refused. - Reject requests whose signature does not match or whose timestamp is older than 5 minutes.
9. Test order
To test your integration end to end, order the product API Test Order from /products. It costs $0.01, is delivered instantly and returns a dummy single-use code such as API-TEST-3F9A1C2B7D10 in items. It is a real order: the amount is taken from your balance, the order appears in /orders and your webhook receives order.completed. The test product is visible only in the API, not in our Telegram store.
10. Required inputs
Some products are delivered to your customer's own account (for example a team invitation). Their required_inputs list tells you what to send in inputs. Such orders are limited to quantity 1. We never ask for passwords.
| customer_email | Email address (the invitation is sent here) |
|---|---|
| customer_gmail | Gmail address (the invitation is sent here) |
| customer_account | Email or ID of the account to upgrade |
| customer_account_gmail | Gmail address of the account to upgrade |
| customer_username | Username of the account to upgrade |
| customer_phone | Phone number in international format (+905551112233) |
| full_name | Full name |
| country | Country |
A missing input returns 422 input_required with the missing keys in error.fields; nothing is charged.
11. Errors
Errors always have the same shape. Use code in your logic; message is for people and may change.
{
"error": {
"code": "insufficient_balance",
"message": "Insufficient balance."
}
}| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json · missing_field · invalid_field · invalid_quantity | The request is malformed. Nothing was charged. |
| 401 | unauthorized | Missing, wrong or revoked key. |
| 402 | insufficient_balance | Top up your balance in the bot. |
| 403 | ip_not_allowed | The key has an IP limit and your IP is not on it. |
| 404 | product_not_found · not_found | Product not in the store / order not found. |
| 409 | out_of_stock · price_changed · product_unavailable · order_refused | Cannot be sold right now. Refresh the catalog and retry. Nothing was charged. |
| 422 | input_required · invalid_input | A required customer input is missing or invalid. |
| 429 | rate_limited · too_many_failed_attempts | Slow down; see Retry-After. |
| 503 | api_disabled | Short maintenance. Retry later. |
12. Code examples
# Products
curl https://api.aistoreplus.com/v1/products \
-H "Authorization: Bearer rk_YOUR_KEY"
# Place an order (use a unique idempotency_key per order)
curl -X POST https://api.aistoreplus.com/v1/orders \
-H "Authorization: Bearer rk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"product_id": 127, "quantity": 1, "idempotency_key": "my-order-10045"}'import requests
API = "https://api.aistoreplus.com/v1"
HEADERS = {"Authorization": "Bearer rk_YOUR_KEY"}
products = requests.get(f"{API}/products", headers=HEADERS, timeout=30).json()["products"]
item = next(p for p in products if p["id"] == 127 and p["in_stock"])
resp = requests.post(
f"{API}/orders",
headers=HEADERS,
json={"product_id": item["id"], "quantity": 1, "idempotency_key": "my-order-10045"},
timeout=90,
)
data = resp.json()
if resp.status_code in (200, 201):
order = data["order"]
print(order["order_code"], order["status"], order.get("items"))
else:
print("Error:", data["error"]["code"], data["error"]["message"])<?php
$api = 'https://api.aistoreplus.com/v1';
$key = 'rk_YOUR_KEY';
$ch = curl_init("$api/orders");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'product_id' => 127,
'quantity' => 1,
'idempotency_key' => 'my-order-10045',
]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status === 200 || $status === 201) {
echo $body['order']['order_code'], ' ', $body['order']['status'], PHP_EOL;
} else {
echo 'Error: ', $body['error']['code'], PHP_EOL;
}const API = "https://api.aistoreplus.com/v1";
const headers = { Authorization: "Bearer rk_YOUR_KEY", "Content-Type": "application/json" };
const res = await fetch(`${API}/orders`, {
method: "POST",
headers,
body: JSON.stringify({ product_id: 127, quantity: 1, idempotency_key: "my-order-10045" }),
});
const data = await res.json();
if (res.ok) {
console.log(data.order.order_code, data.order.status, data.order.items);
} else {
console.error(data.error.code, data.error.message);
}13. Security
- Keep the key on your server. Never put it in a website, browser extension, mobile app or public repository.
- Set an IP limit for production keys.
- If a key may have leaked, revoke it in the bot and create a new one. Revoking takes effect immediately.
- We store only an encrypted fingerprint of your key. Our team will never ask you for your key or your password.
- Delivered accounts and codes are in API responses only. Store them securely and deliver them to your customer over a secure channel.
14. Support
Questions or a problem with an order? Write to @aistoreplus on Telegram with the order_code and the X-Request-Id of the response.
API version v1 · last updated 28 October 2026.


