BuyCheapKeys Partner API
A REST/JSON API to sell Windows and Office keys on your own site. Each order is paid from your prepaid balance and the keys come back in the same response, bought from the cheapest seller with stock at that moment.
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: the keys are in the response of the order and can be read again with GET /orders/{id}/keys.
- Claims: a faulty key can be claimed for 14 days from your partner dashboard, with messages and screenshots. A replacement key reaches your order automatically and a refund goes back to your balance.
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, /me, /ping, /gift-cards |
| webhooks:write | PUT and DELETE /webhook (register or remove your webhook URL) |
| orders:write | POST /orders, POST /gift-cards/orders |
| orders:read | GET /orders, /orders/{id}, /orders/{id}/keys, /gift-cards/orders, /gift-cards/orders/{id}, 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: it comes back completed, with the keys. 4) Read the keys again at any time with GET /orders/{id}/keys. Partner accounts still being approved can use sandbox 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 | Units in stock (capped at 999). |
| 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. Promo code: add couponCode: MICROSOFT20 to the order body to pay 20 % less for the Microsoft products while the promotion is active (maxUnitPrice is compared with the discounted price; an unknown code or an ended promotion returns INVALID_COUPON and nothing is charged).
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. - The order is completed in the same request: the response has status
completedand akeysarray. A replayed request (same Idempotency-Key) returns the order without its keys andkeysOmitted: true: read them with GET /orders/{id}/keys. - If there is no stock or your balance is too low nothing is charged and no key is used (
OUT_OF_STOCK,INSUFFICIENT_BALANCE).maxUnitPriceis a ceiling for every key; a line can be split in several items when sellers with different prices are needed. - If a request times out, send it again with the same externalId: you get
DUPLICATE_EXTERNAL_IDwith the order id and can read its keys.
| Status | Meaning |
|---|---|
| completed | All units delivered. The keys are in the response. |
| partially_refunded | Some keys were refunded to your balance after claims. |
| refunded | Every key of the order was refunded to your balance. |
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.
Claims and refunds
Each key comes from a seller who is paid after a holding period, so a faulty key can be claimed for 14 days. Open the order in Dashboard → Orders (API orders), pick the key, write to the seller and attach screenshots of the activation error. The seller can send a replacement key (it appears in your order and you get the key.replaced webhook) or refund the key (the price returns to your API balance once and you get order.refunded). If you do not agree, escalate and BuyCheapKeys decides.
Check the region, excludedCountries and the licence type before selling, and show them to your customer. A refund to your balance does not refund your own customer: that is your decision.
BuyCheapKeys Gift Cards in bulk
Buy our own gift cards to resell: codes BCK-XXXX-XXXX-XXXX-XXXX that add their value to the BuyCheapKeys Wallet of whoever redeems them at buycheapkeys.com/wallet. GET /gift-cards returns the values on sale, the volume discounts, the maximums per order and your Wallet balance. POST /gift-cards/orders buys N cards of one value and returns the codes at once; GET /gift-cards/orders/{id} shows them again with the status of each code.
curl https://buycheapkeys.com/partner-api/v1/gift-cards \
-H "Authorization: Bearer $BCK_API_KEY"- Paid only from the BuyCheapKeys Wallet of your shop account (top it up at buycheapkeys.com/wallet), not from the API balance. Price = face value minus the volume discount for that quantity.
- Values, discounts and maximums can change: send
maxTotalto refuse a higher total (PRICE_CHANGED, nothing charged).Idempotency-Keyand the uniqueexternalIdwork as for orders. - Sale paused:
PRODUCT_UNAVAILABLE(409). Value not sold or quantity out of range:VALIDATION_ERROR. Order too large:ORDER_LIMIT_EXCEEDED. Not enough Wallet balance:INSUFFICIENT_BALANCE(402). - Sandbox keys return fake
BCK-TEST-…codes that cannot be redeemed and charge nothing. - Sold codes are final. Each code has an expiry date (
expiresAt) and redemption limits per account. A code we block for fraud is no longer shown (code: null, statusblocked). Store the codes encrypted.
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 | An order was completed. |
| order.refunded | A claim ended in a refund to your balance (`refundedSale`, `amount`). |
| product.updated | Price, stock or sheet of a product changed (`data.product` has the full product, `removed: true` when it is retired). |
| key.replaced | A seller replaced a key: `oldKeyId`, `newKeyId`; read the new key with GET /orders/{id}/keys. |
| 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. |
| INVALID_COUPON | 400 | couponCode unknown, or the promotion has ended. Nothing was charged. | Remove the code or send the active one (MICROSOFT20). |
| 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 — never re-order after a timeout: repeat the same externalId and read the keys of the existing 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.