BuyCheapKeys Partner API
A REST/JSON API to import our catalogue and sell it on your own site. Orders are paid from your prepaid balance and delivered automatically from the cheapest available supplier.
Overview
Base URL: https://buycheapkeys.com/partner-api/v1. All requests and responses are JSON (UTF-8). Prices are in EUR with two decimals and do not include VAT. Dates are ISO 8601 in UTC.
- Catalogue: list and incrementally sync every product you can sell, with your partner price.
- Orders: buy up to 10 lines × 9 units per request; the total is debited from your balance at once.
- Delivery: keys, Steam Gift links, account credentials or activation links, via API or webhook.
- Refunds: units that could not be delivered are credited back automatically. Delivered keys, Steam Gifts, accounts and links are final: they cannot be returned or refunded.
Authentication and environments
Create keys in Dashboard → Partner → API keys. A key is shown once; we only store its hash. Send it in the Authorization header (X-Api-Key is accepted too).
curl https://buycheapkeys.com/partner-api/v1/ping \
-H "Authorization: Bearer bck_test_xxxxxxxxxxxx_..."| Key prefix | Mode | Behaviour |
|---|---|---|
| bck_test_ | Sandbox | Real catalogue and prices, separate test balance (resettable), fake keys like TEST-AB12C-…, no money moves. Webhooks are sent with mode: test. |
| bck_live_ | Production | Real purchases charged to your live balance. |
| Scope | Allows |
|---|---|
| catalog:read | GET /products, /products/{id}, /meta, /ping |
| orders:write | POST /orders |
| orders:read | GET /orders, /orders/{id}, /orders/{id}/keys, POST /webhooks/test |
| balance:read | GET /balance |
Optional IP allowlist (IPv4/IPv6, CIDR) per account. Keys never expire unless revoked; rotate them by creating a new key and revoking the old one.
Quick start
1) Create a bck_test_ key. 2) List products. 3) Place an order with ?wait=20 to receive it already completed. 4) Download the keys.
curl "https://buycheapkeys.com/partner-api/v1/products?limit=100&page=1&inStock=true" \
-H "Authorization: Bearer $BCK_API_KEY"Catalogue and synchronisation
GET /products returns up to 100 products ordered by updatedAt (then id). updatedAt only changes when something you care about changes (price, stock, name, region, texts, images). Sync with the cursor: every response has nextCursor and hasMore; pass nextCursor back as ?cursor= until hasMore is false, and keep it for the next run. It never skips or repeats products, even when many share the same timestamp. page and updatedSince remain available for browsing. Filters: inStock, platform, region, productType, deliveryType, search, ids (comma-separated, max 100).
curl "https://buycheapkeys.com/partner-api/v1/products?cursor=$SAVED_NEXT_CURSOR&includeRemoved=true&limit=100" \
-H "Authorization: Bearer $BCK_API_KEY"Recommended: full sync once (no cursor), then every 5–15 minutes starting from the stored nextCursor, always with includeRemoved=true. A product with removed: true or inStock: false must not be sold.
{
"id": "prod-17298",
"name": "Terraria (Steam Gift)",
"platform": "Steam",
"region": "Europe",
"productType": "game",
"deliveryType": "gift",
"price": 3.71,
"currency": "EUR",
"inStock": true,
"qty": 12,
"activation": { "excludedCountries": ["RU", "BY", "..."], "allowedCountryCount": 53 },
"coverImage": "https://buycheapkeys.com/api/media/catalog/category/.../cover.jpg",
"screenshots": ["https://buycheapkeys.com/api/media/..."],
"description": "Dig, Fight, Explore, Build...",
"activationDetails": "You will receive a gift link...",
"languages": ["English", "Spanish", "German"],
"genres": ["Action", "Adventure"],
"developer": "Re-Logic", "publisher": "Re-Logic", "releaseDate": "2011-05-16", "ageRating": "PEGI 12",
"systemRequirements": [ { "system": "Windows", "requirement": ["..."] } ],
"url": "https://buycheapkeys.com/en/product/terraria-steam-europe-238023",
"updatedAt": "2026-10-01T16:05:12.881Z",
"removed": false
}| Field | Meaning |
|---|---|
| nextCursor / hasMore | Response fields for sync: pass nextCursor as ?cursor= while hasMore is true; store the last one for the next run. |
| price | Your unit price in EUR. Re-checked live when you order. |
| deliveryType | key = code to redeem · gift = Steam Gift link · account = login credentials · link = activation link. Tell your customer which one they buy. |
| region / activation | Region label plus the exact ISO list of countries where it can NOT be activated (null = only the region is known). |
| qty | Approximate units available (capped at 99); null = unknown. |
| coverImage / screenshots | Served from our CDN; cache them on your side. |
| activationDetails | Redemption instructions (English) to show your customer. |
Orders
POST /orders with externalId (your unique reference) and items (productId, qty 1–9, maxUnitPrice). For each line we check the live price: if it is above maxUnitPrice the whole order is rejected with PRICE_CHANGED and nothing is charged. If it is lower, you pay the lower price.
const res = await fetch("https://buycheapkeys.com/partner-api/v1/orders?wait=20", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BCK_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `shop-order-${order.id}`, // same value on every retry
},
body: JSON.stringify({
externalId: String(order.id),
items: [{ productId: "prod-17298", qty: 1, maxUnitPrice: 4.5 }],
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
if (data.status === "completed") {
const { keys } = await (await fetch(`https://buycheapkeys.com/partner-api/v1/orders/${data.id}/keys`, {
headers: { Authorization: `Bearer ${process.env.BCK_API_KEY}` },
})).json();
// deliver keys[].value to your customer
}- Send an
Idempotency-Keyheader (8–100 chars) and reuse it when you retry: you get the original response and are never charged twice.externalIdis also unique per account. ?wait=N(0–25 s) waits for completion before answering. Without it the order is returned asprocessingand you are notified by webhook or by pollingGET /orders/{id}.- The total is debited when the order is accepted. Units that cannot be delivered are credited back automatically (
refunded).
| Status | Meaning |
|---|---|
| processing | Paid; keys are being purchased/delivered (usually seconds, pre-launch items can take longer). |
| completed | All units delivered. |
| partially_completed | Some units delivered; the rest was refunded to your balance. |
| failed | Nothing could be delivered; fully refunded. |
Key delivery
GET /orders/{id}/keys returns every delivered unit: value holds the code, the Steam Gift URL, the account credentials or the activation link depending on deliveryType. contentType is text/plain or an image type (then value is base64).
Store keys encrypted and show them only to the buyer. The first download of each order is logged on our side as delivery evidence.
No returns
Every delivered unit (key, Steam Gift link, account or activation link) is final: it is bought from the supplier for you at the moment of delivery and cannot be returned or refunded, even if it was not redeemed. Only units that could not be delivered are refunded, automatically. Check the region, activation.excludedCountries and deliveryType before selling, and show them to your customer.
If a delivered key does not work, contact [email protected] within 7 days with the order id and a screenshot of the activation error. The claim is forwarded to the supplier; a replacement or credit is only possible if the supplier replaces or refunds that key.
Webhooks
Set an https URL in Dashboard → Webhooks; you get a signing secret (whsec_…). Every event is a POST with headers X-BCK-Event, X-BCK-Delivery (unique id — de-duplicate on it) and X-BCK-Signature: t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, t + "." + rawBody).
| Event | When |
|---|---|
| order.completed | All units delivered. |
| order.partially_completed | Some units delivered, the rest refunded. |
| order.failed | Nothing delivered, fully refunded. |
| balance.credited | A top-up was added to your balance. |
| webhook.test | Sent by POST /webhooks/test or the dashboard button. |
{
"id": "evt_4m2k9q7x1z8c3v5b6n0p",
"event": "order.completed",
"mode": "live",
"createdAt": "2026-10-01T16:20:31.512Z",
"data": { "id": "po_7f3k2m9q4x8z1c5v6b0n", "externalId": "shop-10045", "status": "completed", "total": 3.71, "...": "same object as GET /orders/{id}" }
}- Answer 2xx within 8 s and do the work asynchronously. Redirects are not followed.
- Failed deliveries are retried after 10 s, 30 s, 1 min, 5 min, 15 min, 30 min, 1 h, 2 h, 4 h, 6 h and 12 h (~26 h).
- Events may arrive out of order or more than once: always re-read GET /orders/{id} when in doubt.
Errors
Errors use HTTP status codes and a stable code. Quote requestId when contacting support.
{
"error": {
"code": "PRICE_CHANGED",
"message": "The price of prod-17298 is now 3.95 EUR, above your maxUnitPrice.",
"details": { "productId": "prod-17298", "currentPrice": 3.95 },
"requestId": "req_k3m9x2q8z7c1v4b6"
}
}| code | HTTP | Meaning | What to do |
|---|---|---|---|
| UNAUTHORIZED | 401 | Missing, invalid, revoked or expired key. | Check the key; create a new one in the dashboard. |
| ACCOUNT_NOT_ACTIVE | 403 | Partner account pending, suspended or rejected. | Contact us. |
| IP_NOT_ALLOWED | 403 | Request IP not in your allowlist. | Add the IP in Dashboard → Webhooks & security. |
| INSUFFICIENT_SCOPE | 403 | The key lacks the scope for this endpoint. | Create a key with the scope. |
| NOT_FOUND | 404 | Unknown product, order or route. | — |
| VALIDATION_ERROR | 400 | Invalid body or parameters (details lists each field). | Fix the request; do not retry as is. |
| INSUFFICIENT_BALANCE | 402 | Balance too low. Nothing was charged. | Top up and retry. |
| PRICE_CHANGED | 409 | Live unit price above maxUnitPrice. Nothing was charged. | Re-price (details.currentPrice) and retry. |
| OUT_OF_STOCK | 409 | Not enough units available. Nothing was charged. | Retry later or hide the product. |
| PRODUCT_UNAVAILABLE | 409 | Product not sellable through the API. | Remove it from your shop. |
| DUPLICATE_EXTERNAL_ID | 409 | externalId already used; details.orderId is the existing order. | Fetch that order instead. |
| IDEMPOTENCY_CONFLICT | 409 | Idempotency-Key reused with a different body. | Use a new key per order. |
| IDEMPOTENCY_IN_PROGRESS | 409 | Same Idempotency-Key still being processed. | Retry in a few seconds. |
| ORDER_LIMIT_EXCEEDED | 422 | Order total above your per-order limit. | Split the order or ask for a higher limit. |
| DAILY_LIMIT_EXCEEDED | 422 | 24-hour spend limit reached. | Wait or ask for a higher limit. |
| RATE_LIMITED | 429 | Too many requests. | Back off (details.retryAfterMs). |
| SERVICE_UNAVAILABLE | 503 | Temporarily unable to sell. Nothing was charged. | Retry with exponential backoff. |
| INTERNAL_ERROR | 500 | Unexpected error (logged with requestId). | Retry with the same Idempotency-Key. |
Limits
| Limit | Value |
|---|---|
| Requests per key | 20 / second and 600 / minute |
| Orders per account | 120 / minute |
| Key downloads per account | 300 / minute |
| Lines per order / units per line | 10 / 9 |
| Order value and 24-hour spend | Per account (see Dashboard → Overview); ask us to raise them |
| Page size | 100 products or orders |
Balance and top-ups
GET /balance returns the balance of the key's mode. Top up from Dashboard → Balance: you receive a unique reference to include in the bank transfer; the amount is credited when it arrives (e-mail + balance.credited webhook). Every movement appears in the ledger.
Implementing with an AI assistant / full example
Machine-readable docs: https://buycheapkeys.com/developers.md (this page as Markdown), https://buycheapkeys.com/developers.es.md (Spanish), the OpenAPI 3.1 spec https://buycheapkeys.com/partner-api/v1/openapi.json and https://buycheapkeys.com/llms.txt. Give these URLs to your AI assistant together with the prompt below.
Complete reference integration (Node.js 18+, no dependencies): https://buycheapkeys.com/developers/bck-partner-client.mjs — catalogue sync with cursor, safe ordering with retries and idempotency, key download and a webhook receiver with signature verification.
curl -O https://buycheapkeys.com/developers/bck-partner-client.mjs
export BCK_API_KEY=bck_test_... # sandbox key from Dashboard → Partner → API keys
node bck-partner-client.mjs balance
node bck-partner-client.mjs sync
node bck-partner-client.mjs order <productId> 1 <maxUnitPrice>- Invariant 1 — sell only products with inStock=true and removed=false; re-sync every 5–15 min from the stored nextCursor.
- Invariant 2 — one customer order = one externalId = one Idempotency-Key; reuse both on every retry.
- Invariant 3 — always send maxUnitPrice; 4xx business errors are final and nothing was charged.
- Invariant 4 — retry only network errors, 429, 5xx and IDEMPOTENCY_IN_PROGRESS, with exponential backoff.
- Invariant 5 — an order in processing is already paid: wait for the webhook or poll GET /orders/{id}; never re-order.
- Invariant 6 — keep the API key server-side; verify webhook signatures; show customers the deliveryType and activation region.
Security checklist
- Keep API keys on your server only (never in browsers or apps) and in a secret manager / environment variables.
- Use separate keys per system and the minimum scopes; enable the IP allowlist.
- Always send maxUnitPrice and an Idempotency-Key.
- Verify every webhook signature and timestamp before trusting it.
- Rotate keys and the webhook secret if you suspect a leak, then revoke the old ones.