API documentation

The API lets your software read your balance, list goods, buy and receive them automatically. You need an account the shop has approved and a key made at 🔑 API.

Quick start

  1. Log in at shop.apihub247.com → Account → API → "Request API access". Once the shop approves, make your key (it is shown once - keep it on your server).
  2. Top up your wallet on the website. The API buys from the wallet balance.
  3. GET /products: take the id or slug of what you want and your price.
  4. POST /orders with a new Idempotency-Key for every purchase, waiting up to 100 seconds. delivered = the goods are in items.
  5. processing = the shop is fetching it: ask GET /orders/{id} again every 10-30 seconds. Lost connection or 504 → resend the same key.

Base URL

https://shop.apihub247.com/api/v1

Authentication

X-Api-Key: ah247_... Authorization: Bearer ah247_...

Send the key in the X-Api-Key header (or Authorization: Bearer). A wrong, replaced or revoked key answers 401 invalid_key. Call it from your own server only; never put the key in a web page or an app your customers run.

Purchases are paid from your wallet. Top it up on the website as usual.

Endpoints

GET /balance

Read the wallet balance.

curl https://shop.apihub247.com/api/v1/balance \ -H "X-Api-Key: ah247_..."

Example

{ "balance": 250000, "currency": "VND" }

GET /products

Goods you can buy, at your own price. inStock=false means sold out right now. When options is present each plan is { id, label, price }: id 0 is the product itself at your price, other ids are extra warranty plans (send that id as option when buying).

curl https://shop.apihub247.com/api/v1/products \ -H "X-Api-Key: ah247_..."

Example

{ "products": [ { "id": 12, "slug": "product-slug", "name": "Product name", "category": "Category", "price": 50000, "inStock": true, "needsEmail": false, "options": [ { "id": 0, "label": "30 days", "price": 50000 }, { "id": 7, "label": "90 days", "price": 120000 } ] } ] }

POST /orders

Buy, paid from the wallet. The Idempotency-Key header is required. product is an id or slug, qty is 1 to 10, option (warranty option id) and email are optional depending on the item.

curl -X POST https://shop.apihub247.com/api/v1/orders \ -H "X-Api-Key: ah247_..." \ -H "Idempotency-Key: order-0001-abc" \ -H "Content-Type: application/json" \ -d '{"product": "product-slug", "qty": 1}'

Example

{ "orderId": 1234, "status": "delivered", "product": "Product name", "qty": 1, "total": 50000, "items": ["account or key text"], "createdAt": "2026-10-22T10:00:00Z", "deliveredAt": "2026-10-22T10:00:02Z", "warrantyDays": 30, "warrantyMode": "days", "warrantyMonths": null }

GET /orders/{id}

Read one of your orders again.

curl https://shop.apihub247.com/api/v1/orders/1234 \ -H "X-Api-Key: ah247_..."

Example

{ "orderId": 1234, "status": "delivered", "product": "Product name", "qty": 1, "total": 50000, "items": ["account or key text"], "createdAt": "2026-10-22T10:00:00Z", "deliveredAt": "2026-10-22T10:00:02Z", "warrantyDays": 30, "warrantyMode": "days", "warrantyMonths": null }

GET /orders?limit=20

Your latest API orders (limit 1 to 100).

curl "https://shop.apihub247.com/api/v1/orders?limit=20" \ -H "X-Api-Key: ah247_..."

Example

{ "orders": [ ... ] }

Order status: delivered (done, the goods are in items), processing (paid, the shop is still getting it - poll GET /orders/{id}), refunded (money is back in your wallet).

An order can also carry: short { got, want, refunded } (delivered short, the missing part refunded to your wallet), refundedBy: "bank" (refunded to the bank, not the wallet), and the warranty fields warrantyDays / warrantyMode (none, days, months, full) / warrantyMonths.

Idempotency-Key

Every purchase needs its own Idempotency-Key, 8 to 128 characters of letters, digits and . _ : -. Sending the same key again (after a lost connection or a 504 timeout, say) returns the same order with "replayed": true and is never charged twice - never make a new key while you do not know whether the order exists. A key is good for 24 hours; reusing it for a different purchase answers 422 idempotency_key_reused. All times (createdAt, deliveredAt) are UTC in ISO form, e.g. 2026-10-22T10:00:00Z.

Errors

{ "error": "insufficient", "total": 50000, "balance": 20000, "shortfall": 30000 }
  • 400 - Bad request: idempotency_key, bad_json, qty…
  • 401 - Wrong, replaced or revoked key.
  • 402 - Not enough balance; comes with total, balance, shortfall. Top up on the website and call again.
  • 404 - No such order or product.
  • 409 - out_of_stock (sold out), in_progress (a call with the same Idempotency-Key is still running, retry in a few seconds), option_gone (that warranty option no longer exists).
  • 422 - idempotency_key_reused: this key was used for a different purchase; idempotency_key_expired: the key was used more than 24 hours ago, use a new one.
  • 429 - rate_limited: too many calls, wait as the Retry-After header says (seconds).
  • 503 - api_off (API paused) or unavailable (system busy, try again).
  • 504 - timeout: the purchase has no answer yet and the order may exist. Resend with the SAME Idempotency-Key (or check GET /orders), never a new key.

Limits

Each key may make up to 60 calls per minute, of which at most 10 are purchases, and at most 10 items per order. There is no cap on the total you buy.

OpenAPI (Swagger / Postman)

The API described in standard OpenAPI 3, like the suppliers’ docs. Postman: Import → Link → paste the address below. Swagger Editor (editor.swagger.io): File → Import URL.

https://shop.apihub247.com/api/v1/openapi.json

Code samples

One purchase done right: one key per purchase, the same key resent while there is no answer, then waiting while it is processing. Replace ah247_... with your key and product-slug with a slug from GET /products.

Python

import time, uuid, requests BASE = "https://shop.apihub247.com/api/v1" HEADERS = {"X-Api-Key": "ah247_..."} def buy(product, qty=1): key = str(uuid.uuid4()) # one key per purchase for _ in range(10): try: r = requests.post(f"{BASE}/orders", json={"product": product, "qty": qty}, headers={**HEADERS, "Idempotency-Key": key}, timeout=100) except requests.RequestException: time.sleep(5); continue # no answer: resend the SAME key is_json = r.headers.get("content-type", "").startswith("application/json") err = r.json().get("error") if is_json else None if r.status_code == 429 or r.status_code >= 500 or err == "in_progress": time.sleep(int(r.headers.get("Retry-After", 5))); continue r.raise_for_status() # 402 insufficient, 409 out_of_stock... order = r.json() for _ in range(40): # processing: the shop is fetching it if order["status"] != "processing": break time.sleep(15) order = requests.get(f"{BASE}/orders/{order['orderId']}", headers=HEADERS, timeout=15).json() return order # delivered: order["items"] raise RuntimeError("no answer - check GET /orders") print(requests.get(f"{BASE}/balance", headers=HEADERS, timeout=15).json()) print(buy("product-slug"))

Node.js

// Node.js 18+ import { randomUUID } from 'node:crypto'; const BASE = 'https://shop.apihub247.com/api/v1'; const HEADERS = { 'X-Api-Key': 'ah247_...', 'Content-Type': 'application/json' }; const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000)); async function buy(product, qty = 1) { const key = randomUUID(); // one key per purchase for (let i = 0; i < 10; i++) { let r; try { r = await fetch(`${BASE}/orders`, { method: 'POST', headers: { ...HEADERS, 'Idempotency-Key': key }, body: JSON.stringify({ product, qty }), signal: AbortSignal.timeout(100_000) }); } catch { await sleep(5); continue; } // no answer: resend the SAME key const body = await r.json().catch(() => ({})); if (r.status === 429 || r.status >= 500 || body.error === 'in_progress') { await sleep(Number(r.headers.get('retry-after')) || 5); continue; } if (!r.ok) throw new Error(`${r.status} ${body.error}`); // 402 insufficient, 409 out_of_stock... let order = body; for (let j = 0; j < 40 && order.status === 'processing'; j++) { await sleep(15); // processing: the shop is fetching it order = await (await fetch(`${BASE}/orders/${order.orderId}`, { headers: HEADERS })).json(); } return order; // delivered: order.items } throw new Error('no answer - check GET /orders'); } console.log(await (await fetch(`${BASE}/balance`, { headers: HEADERS })).json()); console.log(await buy('product-slug'));

PHP

<?php const BASE = 'https://shop.apihub247.com/api/v1'; const KEY = 'ah247_...'; function call(string $method, string $path, ?array $body = null, ?string $idem = null): array { $h = ['X-Api-Key: ' . KEY, 'Content-Type: application/json']; if ($idem) $h[] = 'Idempotency-Key: ' . $idem; $c = curl_init(BASE . $path); curl_setopt_array($c, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 100]); if ($body !== null) curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($body)); $out = curl_exec($c); $code = $out === false ? 0 : curl_getinfo($c, CURLINFO_RESPONSE_CODE); curl_close($c); return [$code, $out === false ? [] : (json_decode($out, true) ?? [])]; } function buy(string $product, int $qty = 1): array { $key = bin2hex(random_bytes(16)); // one key per purchase for ($i = 0; $i < 10; $i++) { [$code, $o] = call('POST', '/orders', ['product' => $product, 'qty' => $qty], $key); if ($code === 0 || $code === 429 || $code >= 500 || ($o['error'] ?? '') === 'in_progress') { sleep(5); continue; // no answer: resend the SAME key } if ($code !== 200) throw new Exception("$code " . ($o['error'] ?? '')); // 402, 409 out_of_stock... for ($j = 0; $j < 40 && $o['status'] === 'processing'; $j++) { sleep(15); // processing: the shop is fetching it [, $o] = call('GET', '/orders/' . $o['orderId']); } return $o; // delivered: $o['items'] } throw new Exception('no answer - check GET /orders'); } print_r(call('GET', '/balance')[1]); print_r(buy('product-slug'));
Chat on TelegramChat with the shop on Zalo