{"openapi":"3.0.3","info":{"title":"KRAB.GG API: Telegram Stars, Premium, GRAM and Steam","version":"1.0.0","description":"Deliver **Telegram Stars**, **Premium** and **GRAM** to a `@username`, pay for channel **giveaways**, top up **Telegram Ads** accounts by link and **Steam** by login. You fund a USDT balance and send `POST /orders`, we deliver.\n\n**Pricing.** Stars, Premium, GRAM, giveaways and Telegram Ads: the Fragment wholesale price plus a 1% fee; for `premium_giveaway` it is multiplied by the number of subscriptions. Steam: no fee, we charge exactly the dollar amount you order. `GET /price` shows the exact amount before you order.\n\n## Quick start\n\n1. Open the [@apistarsbot](https://t.me/apistarsbot?start=api) bot and press \"Test key now\": your `sk_test_…` key arrives at once. We issue a live `sk_live_…` key from the \"Live access\" button once we approve your request.\n2. Send an order:\n\n```bash\ncurl https://api.krab.gg/v1/orders \\\n  --request POST \\\n  --header 'Authorization: Bearer sk_live_…' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"type\":\"stars\",\"qty\":100,\"username\":\"@johnsmith\",\"orderId\":\"order-123\"}'\n```\n\n3. Look at `status` in the response. `completed`: delivered. `processing`: delivery is running, the outcome arrives by webhook, or ask `GET /orders/{id}`. `failed`: we could not deliver, the cause is in `reason`.\n\nWith a test key we charge and deliver nothing: the response carries the price but no `status`.\n\n## Products\n\n| `type` | `qty` | `username` |\n| --- | --- | --- |\n| `stars` | stars, 50…10,000,000 | Telegram `@username` |\n| `premium` | months: 3, 6 or 12 | Telegram `@username` |\n| `gram` | whole TON | Telegram `@username` |\n| `steam` | dollars, cents allowed: `10.5` | Steam login without `@` |\n| `stars_giveaway` | total Stars for the giveaway, one of the packages; winners in `winners` | channel `@handle` |\n| `premium_giveaway` | each subscription's length: 3, 6 or 12; as many subscriptions as `winners` | channel `@handle` |\n| `ads_recharge` | whole GRAM | the \"Add Funds on Fragment\" link or its token |\n\nYou may drop the `@` and send the number as a string. Limits for `gram`, `ads_recharge` and `steam` move: outside them you get `min_quantity` or `max_quantity` with a `min` or `max` field.\n\n`orderId` is your own order number, up to 128 chars from `A-Za-z0-9._:-`. Repeat a request with the same `orderId` and we return the same order without charging again. A repeat with a different `type`, `qty` or `username` gets `409 order_id_conflict`.\n\n## Money\n\nWhile delivery runs, the order amount sits in reserve on your balance. `settlement` tells you what happened to it:\n\n- `charged`: charged, delivered;\n- `pending`: reserved, delivery is running;\n- `refunded`: back on your balance;\n- `fail_hold`: delivery is unconfirmed and we hold the money until an operator checks it. If we charge, `order.completed` arrives; if we refund, `order.updated` arrives with `settlement: refunded`.\n\nRetry a failed delivery with a new order and a new `orderId`, and only when the order says `retryable: true`. It appears once the money is back on your balance.\n\n## Errors\n\nAn error comes as `{\"ok\": false, \"error\": \"code\", \"message\": \"text\"}`. Branch on `error` and log `message`.\n\n- `4xx`: fix the request;\n- `429`: wait as many seconds as the `Retry-After` header says. The limit is 60 requests per minute per account, and your live and test keys share one counter;\n- `5xx`: retry later.\n\n## Contacts\n\nBase URL: `https://api.krab.gg/v1`. Responses may gain new fields; skip the ones you do not know. By issuing a live key you accept the [API partner terms](https://krab.gg/en/api-terms). Questions and disputed orders: [@heIIo_stars](https://t.me/heIIo_stars) or hello@krab.gg, quote the `orderId`.\n\n\n## Request identifier\n\nEvery response carries an `X-Request-Id` header (for example `req_mu88via8_53d8cd`). Store it in your own logs: it lets us find exactly the request you are asking about, with the full context of the failure. Without it we search by timestamp and address, which takes far longer.\n\n```\nX-Request-Id: req_mu88via8_53d8cd\n```\n","contact":{"name":"KRAB.GG support","url":"https://t.me/heIIo_stars","email":"hello@krab.gg"}},"servers":[{"url":"https://api.krab.gg/v1","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Orders","description":"Create an order and check its status."},{"name":"Pricing and checks","description":"Price and recipient check before ordering, no charges."},{"name":"Balance and deposits","description":"Balance and deposit details."},{"name":"Webhooks","description":"Order outcome notifications to your URL."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"sk_live_… | sk_test_…","description":"The key goes in the `Authorization: Bearer sk_live_…` header (live) or `sk_test_…` (test). We show a key once, when you issue it. An account has one live and one test key: to replace a key, revoke the old one in the cabinet and issue a new one. There (\"Keys\" → \"IP access\") you can also allow requests only from listed addresses: up to 50 exact IPv4/IPv6 addresses, and the list applies to all keys of the account."}},"responses":{"Error":{"description":"An error, body follows the `Error` schema. Besides the codes listed on the method, any request may get:\n\n- `401` missing_api_key, invalid_api_key: the key is missing, wrong or revoked, or access to the account is suspended or closed;\n- `403` ip_not_allowed: the request came from an address outside the account IP list; the response body carries `seen_ip` — the address we actually saw (add exactly that one: behind NAT or a load balancer it differs from the address you see locally);\n- `429` rate_limited: the account limit is exceeded (60 per minute by default), or one IP sent more than 120 requests per minute, and we block that address for 5 minutes. Wait `Retry-After` seconds; the remaining quota is in the `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers;\n- `400` bad_request, `413` payload_too_large: the body is not JSON or exceeds 32 KB;\n- `404` not_found: no such method (wrong path or HTTP method);\n- `404` partner_not_found: the account was closed while the request was running; the next requests get 401;\n- `500`, `503`: a failure on our side or the API is stopped (service_unavailable), retry later.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"missing_api_key","message":"Missing API key. Send header 'Authorization: Bearer sk_...'."}},"examples":{"missing_api_key":{"summary":"missing_api_key","value":{"ok":false,"error":"missing_api_key","message":"Missing API key. Send header 'Authorization: Bearer sk_...'."}},"rate_limited":{"summary":"rate_limited","value":{"ok":false,"error":"rate_limited","message":"Too many requests. Retry after the indicated delay (see retryAfter).","retryAfter":30}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}}},"schemas":{"Error":{"type":"object","required":["ok","error","message"],"example":{"ok":false,"error":"min_quantity","message":"Quantity is below the allowed minimum (see min).","min":50},"properties":{"ok":{"type":"boolean","example":false},"error":{"type":"string","description":"Error code. 400, fix the request: unsupported_type, invalid_quantity, min_quantity, max_quantity, invalid_premium_duration, username_required, invalid_username, order_id_required, invalid_order_id, invalid_dry_run, hold_settled, hold_amount_mismatch, url_required, invalid_url, unsupported_protocol, private_address_not_allowed, invalid_stars_package, winners_required, invalid_winners, winners_not_supported, recipient_not_channel, recipient_not_found, invalid_ads_account, ads_account_not_found, fragment_rejected, bad_request. 401: missing_api_key, invalid_api_key. 402: insufficient_funds. 403: ip_not_allowed, test_key_readonly, sandbox_no_deposit. 404: not_found, partner_not_found. 409: in_progress, order_id_conflict, hold_requires_resolution. 413: payload_too_large. 429: rate_limited. 502: fetch_failed. 503, retry later: service_unavailable, category_disabled, plan_unavailable, pricing_unavailable, rate_unavailable, order_create_failed, hold_failed. 500, retry: internal_error, auth_error, tenant_lookup_failed."},"message":{"type":"string","description":"English phrase for logs. Build your logic on `error`."},"min":{"type":"number","description":"Lower bound for `qty`. Comes with `min_quantity`."},"max":{"type":"number","description":"Upper bound for `qty`. Comes with `max_quantity`."},"allowed":{"type":"array","items":{"type":"integer"},"description":"Allowed Premium terms. Comes with `invalid_premium_duration`."},"whole":{"type":"boolean","description":"Comes with `invalid_quantity` when stars, premium or gram got a fractional `qty`: a whole number is required."},"step":{"type":"number","description":"Comes with `invalid_quantity` when a steam amount has more than two decimals: the amount must be in whole cents."},"retryable":{"type":"boolean","description":"Comes with `in_progress` and `rate_unavailable`: you can retry the request."},"balance":{"type":"number","description":"Your balance in dollars. Comes with `insufficient_funds`."},"required":{"type":"number","description":"Amount the order needs, in dollars. Comes with `insufficient_funds`."},"retryAfter":{"type":"integer","description":"Seconds to wait before retrying. Comes with `rate_limited`."},"type":{"type":"string","description":"Product the check ran for. Comes with `GET /check` recipient errors: invalid_username, invalid_premium_duration, not_found, fetch_failed."}}},"Price":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"type":{"type":"string","example":"stars"},"qty":{"type":"number","example":100},"price":{"type":"number","example":1.515,"description":"Amount to charge in USDT dollars, up to six decimals."}}},"Recipient":{"type":"object","example":{"ok":true,"type":"stars","accepts":true},"properties":{"ok":{"type":"boolean","example":true},"type":{"type":"string","enum":["stars","premium","gram","steam","stars_giveaway","premium_giveaway","ads_recharge"],"example":"stars"},"accepts":{"type":"boolean","example":true,"description":"`true`: you can send the product. `false`: do not send it, the cause is in `reason`."},"reason":{"type":"string","enum":["already_subscribed","recipient_rejects"],"description":"Only with `accepts: false`. `already_subscribed`: the recipient already has Premium. `recipient_rejects`: the recipient exists but an order to them will not go through (for Steam: the login cannot be topped up). An order to such a recipient fails with the same `reason`."}}},"Balance":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"balance":{"type":"number","example":42.5,"description":"Available balance in USDT dollars."}}},"OrderRequest":{"type":"object","required":["type","qty","username","orderId"],"properties":{"type":{"type":"string","enum":["stars","premium","gram","steam","stars_giveaway","premium_giveaway","ads_recharge"],"example":"stars","description":"Product: `stars`, `premium`, `gram`, `steam`, `stars_giveaway`, `premium_giveaway` or `ads_recharge`."},"qty":{"type":"number","example":100,"description":"How much: stars, Premium months (3, 6, 12), whole TON for gram or dollars for steam. For `stars_giveaway` it is the TOTAL number of stars (one of Fragment packages, see `allowed` in the error): the platform splits them between winners. For `premium_giveaway` it is the term of EACH subscription (3, 6, 12), and you buy as many subscriptions as there are winners. For `ads_recharge` — whole GRAM."},"winners":{"type":"integer","minimum":1,"description":"Giveaways only: how many people share the prize. For `stars_giveaway` the cap depends on the amount (Fragment gives at least 100 stars per person, so no more than `qty / 100` winners), for `premium_giveaway` — up to 24000 subscriptions. Other products do not accept this field.","example":5},"username":{"type":"string","example":"@johnsmith","description":"Recipient: a Telegram `@username`, or a Steam account login for steam. For giveaways — a CHANNEL `@handle` (a giveaway cannot run in a bot or a personal account). For `ads_recharge` — the token from the Telegram Ads link \"Add Funds on Fragment\" (`fragment.com/ads/pay?account=…`); the whole link is accepted too. It is not a @username and not a numeric id."},"orderId":{"type":"string","example":"order-123","description":"Your order number, up to 128 chars from `A-Za-z0-9._:-`. Repeating the same orderId returns the existing order and does not charge again; with a different `type`, `qty` or `username` you get 409 `order_id_conflict`."},"dryRun":{"type":"boolean","example":false,"description":"`true`: sandbox. We only price the order: no charge, no delivery, no order. A test key always works this way. The response has the price, `dryRun: true` and `mode`, but no `status` or `settlement`; `GET /orders/{id}` for that orderId returns 404 and no webhooks fire. Besides `true`/`false` we accept `1`/`0` and the strings `\"true\"`/`\"false\"`; any other value → 400 `invalid_dry_run`."}}},"Order":{"type":"object","example":{"ok":true,"orderId":"order-123","status":"completed","type":"stars","qty":100,"username":"@johnsmith","price":1.515,"settlement":"charged"},"properties":{"ok":{"type":"boolean","example":true},"orderId":{"type":"string","example":"order-123"},"status":{"type":"string","enum":["completed","processing","failed","refunded","cancelled"],"example":"completed","description":"`completed`: delivered. `processing`: delivery is running. `failed`: delivery failed. `cancelled`, `refunded`: the order closed without delivery. Sandbox responses omit this field."},"reason":{"type":"string","nullable":true,"enum":["already_subscribed","partial_delivery","recipient_not_found","recipient_rejects","unsupported_type","under_review","temporary"],"description":"Why delivery failed. Comes with `retryable`, including when the order later closed as `cancelled` or `refunded`.\n\n- `recipient_not_found`: no such recipient;\n- `recipient_rejects`: the recipient cannot take this product;\n- `already_subscribed`: the recipient already has Premium;\n- `unsupported_type`: we cannot deliver this product right now;\n- `temporary`: a failure before sending; retry with a new orderId once `retryable: true`;\n- `under_review`: the transfer may have gone through, we are checking, do not create a new order;\n- `partial_delivery`: we delivered part and will finish the rest by hand, do not retry.\n\nFor `recipient_not_found`, `recipient_rejects`, `already_subscribed` and `unsupported_type` the money returns on its own, usually within a minute. `settlement` shows where the money is now."},"retryable":{"type":"boolean","description":"`true`: the money for this order is back on your balance, retry with a new orderId. `false`: a retry will not help, or the money is still reserved and delivery may still finish."},"type":{"type":"string","example":"stars"},"qty":{"type":"number","example":100,"description":"For steam, dollars rounded to the cent."},"username":{"type":"string","example":"@johnsmith","description":"As you sent it in the order."},"price":{"type":"number","example":1.515,"description":"Price in USDT dollars. Only in the `POST /orders` response."},"idempotent":{"type":"boolean","description":"`true`: this orderId already existed, we returned that order and charged nothing."},"settlement":{"type":"string","enum":["charged","refunded","fail_hold","pending"],"description":"What happened to the money: `charged`, `pending`, `refunded` or `fail_hold` (see \"Money\" in the introduction). Sandbox responses omit it."},"mode":{"type":"string","enum":["live","test"],"description":"Sandbox only: `test` for a test key, `live` for a live key with dryRun."},"dryRun":{"type":"boolean","description":"Sandbox only: `true`."}}}},"headers":{"RequestId":{"description":"Request identifier. Present on every response, including errors; quote it in support requests and we will find this exact call.","schema":{"type":"string","example":"req_mu88via8_53d8cd"}}}},"paths":{"/price":{"get":{"tags":["Pricing and checks"],"operationId":"getPrice","summary":"Price","description":"How much we will charge for an order. Creates and reserves nothing; we price the order again when you create it.","parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["stars","premium","gram","steam"]},"description":"Product.","required":true,"example":"stars"},{"name":"qty","in":"query","required":true,"schema":{"type":"number"},"example":100,"description":"Same as `qty` in `POST /orders`."}],"responses":{"200":{"description":"Price","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Price"}],"example":{"ok":true,"type":"stars","qty":100,"price":1.515}},"examples":{"stars":{"summary":"100 Stars","value":{"ok":true,"type":"stars","qty":100,"price":1.515}},"premium":{"summary":"Premium for 3 months","value":{"ok":true,"type":"premium","qty":3,"price":12.1099}},"steam":{"summary":"Steam for $10.50, no fee","value":{"ok":true,"type":"steam","qty":10.5,"price":10.5}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"400":{"description":"unsupported_type, invalid_quantity, min_quantity, max_quantity, invalid_premium_duration.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"min_quantity","message":"Quantity is below the allowed minimum (see min).","min":50}},"examples":{"min_quantity":{"summary":"min_quantity","value":{"ok":false,"error":"min_quantity","message":"Quantity is below the allowed minimum (see min).","min":50}},"invalid_premium_duration":{"summary":"invalid_premium_duration","value":{"ok":false,"error":"invalid_premium_duration","message":"Premium term must be 3, 6, or 12 months (see allowed).","allowed":[3,6,12]}},"invalid_quantity":{"summary":"invalid_quantity","value":{"ok":false,"error":"invalid_quantity","message":"Invalid quantity. Stars/premium/gram take a whole number (see whole); steam takes dollars with at most 2 decimals (see step).","whole":true}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"503":{"description":"Retry later: category_disabled (sales of this product are paused), plan_unavailable (no Premium for this term), pricing_unavailable, rate_unavailable (no fresh rate: Steam, or TON for GRAM).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"rate_unavailable","message":"The exchange rate is temporarily unavailable. Please retry.","retryable":true}},"examples":{"rate_unavailable":{"summary":"rate_unavailable","value":{"ok":false,"error":"rate_unavailable","message":"The exchange rate is temporarily unavailable. Please retry.","retryable":true}},"category_disabled":{"summary":"category_disabled","value":{"ok":false,"error":"category_disabled","message":"Sales for this product type are temporarily paused. Please retry later."}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"default":{"$ref":"#/components/responses/Error"}}}},"/check":{"get":{"tags":["Pricing and checks"],"operationId":"checkRecipient","summary":"Recipient check","description":"Checks the recipient before you order, so a typo does not freeze money. Fragment checks Stars, Premium and GRAM with the same lookup delivery uses, the payment gateway checks Steam. The answer matches how an order to this recipient would end.\n\n`accepts: false` comes with `reason`.","parameters":[{"name":"username","in":"query","required":true,"schema":{"type":"string"},"example":"@johnsmith","description":"Telegram `@username` or Steam login."},{"name":"type","in":"query","schema":{"type":"string","enum":["stars","premium","gram","steam","stars_giveaway","premium_giveaway","ads_recharge"],"default":"stars"},"description":"Product: `stars` (default), `premium`, `gram` or `steam`.","required":false},{"name":"qty","in":"query","required":false,"schema":{"type":"integer"},"description":"Premium only: term of 3, 6 or 12, default 3."}],"responses":{"200":{"description":"Checked","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Recipient"}],"example":{"ok":true,"type":"stars","accepts":true}},"examples":{"accepts":{"summary":"Can be sent","value":{"ok":true,"type":"stars","accepts":true}},"already_subscribed":{"summary":"Premium: the recipient already has Premium","value":{"ok":true,"type":"premium","accepts":false,"reason":"already_subscribed"}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"400":{"description":"username_required, invalid_username, unsupported_type, invalid_premium_duration.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"invalid_username","message":"The recipient username is invalid. Use an @handle of 5-32 characters (letters, digits, _; not starting or ending with _) for stars/premium/gram, or a Steam account login without @ (letters, digits, _ and -) for steam.","type":"stars"}},"examples":{"invalid_username":{"summary":"invalid_username","value":{"ok":false,"error":"invalid_username","message":"The recipient username is invalid. Use an @handle of 5-32 characters (letters, digits, _; not starting or ending with _) for stars/premium/gram, or a Steam account login without @ (letters, digits, _ and -) for steam.","type":"stars"}},"username_required":{"summary":"username_required","value":{"ok":false,"error":"username_required","message":"A recipient username is required."}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"404":{"description":"not_found: no such user, or it is a channel or a bot.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"not_found","message":"Not found.","type":"stars"}},"example":{"ok":false,"error":"not_found","message":"Not found.","type":"stars"}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"502":{"description":"fetch_failed: Fragment or the Steam gateway did not answer or answered unclearly. This says nothing about the recipient, retry.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"fetch_failed","message":"Temporary failure checking the recipient. Please retry.","type":"stars"}},"example":{"ok":false,"error":"fetch_failed","message":"Temporary failure checking the recipient. Please retry.","type":"stars"}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"default":{"$ref":"#/components/responses/Error"}}}},"/balance":{"get":{"tags":["Balance and deposits"],"operationId":"getBalance","summary":"Balance","description":"Available balance in USDT dollars. The movement history lives in the cabinet.","responses":{"200":{"description":"Balance","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Balance"}],"example":{"ok":true,"balance":42.5}},"example":{"ok":true,"balance":42.5}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"default":{"$ref":"#/components/responses/Error"}}}},"/deposit":{"get":{"tags":["Balance and deposits"],"operationId":"getDeposit","summary":"Deposit details","description":"Where to send money. We accept only **USDT on TON**: send at least 100 USDT to `address` and **always put `memo`** in the transfer comment. The address is shared, the memo belongs to your account. We usually credit within a minute; check `GET /balance`. Forgot the memo: send the transaction hash to support and we credit it by hand.","responses":{"200":{"description":"Deposit details","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"asset":{"type":"string","example":"USDT"},"network":{"type":"string","example":"TON"},"address":{"type":"string","description":"Our wallet."},"memo":{"type":"string","description":"Transfer comment. Without it we cannot tell whose transfer it is."},"minRecommendedUsdt":{"type":"number","description":"Minimum top-up amount in USDT."},"note":{"type":"string","description":"Short hint, always in Russian."}},"example":{"ok":true,"asset":"USDT","network":"TON","address":"EQ…","memo":"d1a2b3c4d5e6f7081","minRecommendedUsdt":100,"note":"Отправьте от 100 USDT (TON) на address с указанным memo. Без memo средства зачислить нельзя."}},"example":{"ok":true,"asset":"USDT","network":"TON","address":"EQ…","memo":"d1a2b3c4d5e6f7081","minRecommendedUsdt":100,"note":"Отправьте от 100 USDT (TON) на address с указанным memo. Без memo средства зачислить нельзя."}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"403":{"description":"sandbox_no_deposit: the sandbox has no deposits, get live access first.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"sandbox_no_deposit","message":"Deposits are not available in sandbox. Request live access first."}},"example":{"ok":false,"error":"sandbox_no_deposit","message":"Deposits are not available in sandbox. Request live access first."}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"default":{"$ref":"#/components/responses/Error"}}}},"/webhook":{"get":{"tags":["Webhooks"],"operationId":"getWebhook","summary":"Current webhook","description":"The webhook URL and the `secret` for signature checks. A test key gets `secret: null`.","responses":{"200":{"description":"Webhook settings","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"url":{"type":"string","nullable":true},"secret":{"type":"string","nullable":true}},"example":{"ok":true,"url":"https://shop.example.com/webhooks/krab","secret":"whsec_4f1c…"}},"example":{"ok":true,"url":"https://shop.example.com/webhooks/krab","secret":"whsec_4f1c…"}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"tags":["Webhooks"],"operationId":"setWebhook","summary":"Set webhook","description":"Sets the URL we POST to when an order outcome changes. `url: null` turns the webhook off. The response carries `secret`: it appears with the first URL and stays the same when you change the URL. The \"Rotate secret\" button in the cabinet issues a new one, and the old one stops working at once.\n\n**Event.** JSON with `event`, `orderId`, `status`, `type`, `qty`, `username`, `settlement`, `sequence` and `occurredAt` (unix time in seconds when we recorded this state), plus `reason` and `retryable` when a delivery attempt failed. An event arrives when the order outcome changes: `order.completed` (delivered), `order.failed` (not delivered, or closed without delivery) or `order.updated` (same outcome, but `settlement`, `reason` or `retryable` changed, for example the reserve was returned after `fail_hold`). `sequence` grows by 1 with each event of an order: if an event has a lower number than one you already processed, skip it. The \"Send test\" button in the cabinet sends `{\"event\":\"ping\",\"at\":<unix time>}` with `X-Webhook-Id: test`.\n\n**Signature.** We send `X-Webhook-Id`, `X-Webhook-Timestamp` (unix time in seconds) and `X-Webhook-Signature: v1,<signature>`, where signature = base64(HMAC-SHA256(`id.timestamp.body`, secret)) and `body` is the raw request body. Reject deliveries older than a few minutes.\n\n```js\nconst expected = Buffer.from(crypto.createHmac('sha256', secret).update(`${id}.${ts}.${rawBody}`).digest('base64'));\nconst got = Buffer.from(signature.replace(/^v1,/, ''));\nconst ok = got.length === expected.length && crypto.timingSafeEqual(expected, got);\n```\n\n**Delivery.** Answer 2xx within 10 seconds with a body of at most 64 KB, otherwise the attempt fails. We do not follow redirects: a 3xx answer is a failure too. We retry after 1m→5m→30m→2h, 5 attempts in total, then notify you in the cabinet. One event can arrive twice: `X-Webhook-Id` stays the same across retries, so drop duplicates by it. Delivery order is not guaranteed, so compare `sequence`. An event not delivered in 5 attempts or missed while the webhook was off may never arrive (after you turn the webhook on, events for orders changed in the last day may arrive): always reconcile the final status via `GET /orders/{id}`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","nullable":true,"example":"https://shop.example.com/webhooks/krab","description":"Public https or http URL. `null` turns the webhook off. If the host name resolves to a private network, deliveries to it fail."}}},"example":{"url":"https://shop.example.com/webhooks/krab"}}}},"responses":{"200":{"description":"Saved","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"url":{"type":"string","nullable":true},"secret":{"type":"string","nullable":true}},"example":{"ok":true,"url":"https://shop.example.com/webhooks/krab","secret":"whsec_4f1c…"}},"example":{"ok":true,"url":"https://shop.example.com/webhooks/krab","secret":"whsec_4f1c…"}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"400":{"description":"url_required, invalid_url, unsupported_protocol, private_address_not_allowed.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"private_address_not_allowed","message":"The webhook URL must be a public address."}},"examples":{"private_address_not_allowed":{"summary":"private_address_not_allowed","value":{"ok":false,"error":"private_address_not_allowed","message":"The webhook URL must be a public address."}},"unsupported_protocol":{"summary":"unsupported_protocol","value":{"ok":false,"error":"unsupported_protocol","message":"The webhook URL must use http or https."}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"403":{"description":"test_key_readonly: a test key cannot change settings, use a live key.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"test_key_readonly","message":"A test key (sk_test_) cannot change configuration. Use a live key (sk_live_)."}},"example":{"ok":false,"error":"test_key_readonly","message":"A test key (sk_test_) cannot change configuration. Use a live key (sk_live_)."}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"default":{"$ref":"#/components/responses/Error"}}}},"/orders":{"post":{"tags":["Orders"],"operationId":"createOrder","summary":"Create order","description":"Creates the order and sends the product right away. We usually deliver Stars, Premium and GRAM within seconds, and the response already says `status: completed`. Steam usually answers `processing`: the outcome arrives by webhook or via `GET /orders/{id}`. If delivery fails at once, you get `status: failed` with the cause in `reason`. If the Steam gateway runs out of funds and the order never reached it, the order waits up to two hours, then ends `failed` with `reason: temporary` and the money returns to your balance. If the transfer may have gone through, an operator decides.\n\nRepeating a request with the same `orderId` is safe: we return the same order and do not charge again. After `503 order_create_failed`, retry the same `orderId`: a failed new creation rolls back its reserve, so it does not leave funds held without an order. A legacy reservation without recorded order terms returns `409 hold_requires_resolution`; contact support to reconcile or release it. A closed reservation still returns `hold_settled`, and one with a different amount returns `hold_amount_mismatch`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"},"examples":{"stars":{"summary":"100 Stars","value":{"type":"stars","qty":100,"username":"@johnsmith","orderId":"order-123"}},"premium":{"summary":"Premium for 3 months","value":{"type":"premium","qty":3,"username":"@johnsmith","orderId":"order-124"}},"gram":{"summary":"GRAM: 5 TON","value":{"type":"gram","qty":5,"username":"@johnsmith","orderId":"order-127"}},"steam":{"summary":"Steam: $10.50 to a login","value":{"type":"steam","qty":10.5,"username":"john_doe","orderId":"order-125"}},"sandbox":{"summary":"Sandbox","value":{"type":"stars","qty":100,"username":"@johnsmith","orderId":"order-126","dryRun":true}},"stars_giveaway":{"summary":"Stars giveaway in a channel: 2500 stars for 5 winners (500 each)","value":{"type":"stars_giveaway","qty":2500,"winners":5,"username":"@mychannel","orderId":"gw-stars-1001"}},"premium_giveaway":{"summary":"Premium giveaway: 10 subscriptions of 3 months","value":{"type":"premium_giveaway","qty":3,"winners":10,"username":"@mychannel","orderId":"gw-prem-1001"}},"ads_recharge":{"summary":"Top up a Telegram Ads account with 50 GRAM","value":{"type":"ads_recharge","qty":50,"username":"Qm7Tz2VxK9pLc4Nw8RbF3sYd6HgJ1uEa5WkZ0oXnC2tMv7PqB4yLr9SiD8fGh3Ue","orderId":"ads-1001"}}}}}},"responses":{"200":{"description":"The order. In the sandbox (test key or `dryRun: true`) no order is created: `status` and `settlement` are absent, and `dryRun: true` and `mode` come instead.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Order"}],"example":{"ok":true,"orderId":"order-123","status":"completed","type":"stars","qty":100,"username":"@johnsmith","price":1.515,"settlement":"charged"}},"examples":{"stars":{"summary":"Stars delivered","value":{"ok":true,"orderId":"order-123","status":"completed","type":"stars","qty":100,"username":"@johnsmith","price":1.515,"settlement":"charged"}},"steam":{"summary":"Steam: delivery running","value":{"ok":true,"orderId":"order-125","status":"processing","type":"steam","qty":10.5,"username":"john_doe","price":10.5,"settlement":"pending"}},"sandbox":{"summary":"Sandbox","value":{"ok":true,"orderId":"order-126","type":"stars","qty":100,"username":"@johnsmith","price":1.515,"dryRun":true,"mode":"test"}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"400":{"description":"Fix the request: username_required, invalid_username, order_id_required, invalid_order_id, unsupported_type, invalid_quantity, min_quantity, max_quantity, invalid_premium_duration, invalid_dry_run. hold_settled, hold_amount_mismatch: this orderId is taken, use a new one.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"order_id_required","message":"An orderId is required."}},"examples":{"order_id_required":{"summary":"order_id_required","value":{"ok":false,"error":"order_id_required","message":"An orderId is required."}},"min_quantity":{"summary":"min_quantity","value":{"ok":false,"error":"min_quantity","message":"Quantity is below the allowed minimum (see min).","min":50}},"hold_settled":{"summary":"hold_settled","value":{"ok":false,"error":"hold_settled","message":"This orderId was already settled (charged or refunded). Use a new orderId."}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"402":{"description":"insufficient_funds: not enough money. The response has `balance` and `required` in dollars.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"insufficient_funds","message":"Insufficient balance for this order (see balance and required).","balance":0.5,"required":1.515}},"example":{"ok":false,"error":"insufficient_funds","message":"Insufficient balance for this order (see balance and required).","balance":0.5,"required":1.515}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"409":{"description":"in_progress: a parallel request is processing this orderId; retry in a second, no duplicate appears. order_id_conflict: this orderId already belongs to an order with a different `type`, `qty` or `username`; use a new one. hold_requires_resolution: an existing reservation has no matching recorded terms; contact support to reconcile or release it. No new order or delivery is started.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"order_id_conflict","message":"This orderId already belongs to a different order (type, quantity or recipient). Use a new orderId."}},"examples":{"order_id_conflict":{"summary":"order_id_conflict","value":{"ok":false,"error":"order_id_conflict","message":"This orderId already belongs to a different order (type, quantity or recipient). Use a new orderId."}},"in_progress":{"summary":"in_progress","value":{"ok":false,"error":"in_progress","message":"This orderId is already being processed. Retry shortly.","retryable":true}},"hold_requires_resolution":{"summary":"hold_requires_resolution","value":{"ok":false,"error":"hold_requires_resolution","message":"An existing reservation cannot be matched to this order. Contact support to reconcile or release it."}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"503":{"description":"Retry later: category_disabled, plan_unavailable, pricing_unavailable, rate_unavailable, order_create_failed, hold_failed.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"order_create_failed","message":"Could not create the order. Please retry."}},"examples":{"order_create_failed":{"summary":"order_create_failed","value":{"ok":false,"error":"order_create_failed","message":"Could not create the order. Please retry."}},"rate_unavailable":{"summary":"rate_unavailable","value":{"ok":false,"error":"rate_unavailable","message":"The exchange rate is temporarily unavailable. Please retry.","retryable":true}}}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"default":{"$ref":"#/components/responses/Error"}}}},"/orders/{id}":{"get":{"tags":["Orders"],"operationId":"getOrder","summary":"Order status","description":"The order by your `orderId`: `status`, `settlement`, plus `reason` and `retryable` after a failed delivery. No price here; it comes in the `POST /orders` response.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"order-123","description":"Your orderId."}],"responses":{"200":{"description":"Order","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Order"}],"example":{"ok":true,"orderId":"order-123","status":"completed","type":"stars","qty":100,"username":"@johnsmith","settlement":"charged"}},"example":{"ok":true,"orderId":"order-123","status":"completed","type":"stars","qty":100,"username":"@johnsmith","settlement":"charged"}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"400":{"description":"invalid_order_id: malformed orderId.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"invalid_order_id","message":"The orderId format is invalid. Use up to 128 characters from A-Z, a-z, 0-9, dot, underscore, colon or hyphen."}},"example":{"ok":false,"error":"invalid_order_id","message":"The orderId format is invalid. Use up to 128 characters from A-Z, a-z, 0-9, dot, underscore, colon or hyphen."}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"404":{"description":"not_found: no such order. Sandbox orders are not stored.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"example":{"ok":false,"error":"not_found","message":"Not found."}},"example":{"ok":false,"error":"not_found","message":"Not found."}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}}},"default":{"$ref":"#/components/responses/Error"}}}}}}