Spark REST API — reference
This HTTPS JSON API allows shops, resellers, and automated tools to manage PUBG Mobile UC: redeem codes from uploaded stock, verify voucher status, fetch character names for a Player ID before charging UC, and read wallet balance and daily request limits. Midasbuy calls run on Spark servers; the integration only sends and receives JSON. Messages returned by this API are provided in English.
X-API-Key
Bot @SparkUCBot
When viewing this page on the API host, ${BASE} and __ORIGIN__ in samples resolve to that server’s URL automatically.
For local scripts, replace them with the production base URL (for example https://api.pubgredeemerbot.com) and paste the API key from the Telegram bot.
The Quick start section includes ready-to-run examples in several languages.
Introduction
Integrations use two complementary patterns:
- Synchronous calls — one HTTP request returns the final JSON immediately: service health, account and quota snapshot, stock counts, redemption history, and player name lookup (useful to show the nickname before submitting a redeem).
- Asynchronous jobs — redeem and voucher-check operations contact Midas and may take several seconds. The API accepts a
POST, returns ajob_idimmediately, and executes work in a server-side queue. Integrations should pollGET /v1/jobs/{job_id}until the job reaches a terminal state (doneorfailed). This avoids dropped connections on slow upstream calls.
API keys are created in the Telegram bot (@SparkUCBot) under API → API key.
Access requires an active subscription plan plus the HTTP API add-on. The website root / redirects to the bot; programmatic access uses /health, /v1/*, and (for the order screenshot utility) /api/v1/* as documented below.
Quick start
Step 1: confirm the API host answers. Step 2: send a real API key on a protected route. Replace pk_YOUR_KEY_HERE with the key copied from the bot.
1) Health (no key)
curl -sS -o /dev/null -w "%{http_code}" "${BASE}/health"
# expect: 200 and body {"ok":true}
import requests
BASE = "__ORIGIN__" # replaced when viewing /docs on your API host
r = requests.get(f"{BASE}/health", timeout=30)
r.raise_for_status()
print(r.json()) # {"ok": true}
const BASE = "__ORIGIN__";
const r = await fetch(`${BASE}/health`, { method: "GET" });
console.log(r.status, await r.json());
<?php $base = "__ORIGIN__"; echo file_get_contents($base . "/health");
// go run: needs package main, import "net/http", "io", "log"
resp, err := http.Get("__ORIGIN__/health")
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
println(string(body))
2) Authenticated ping — GET /v1/quota
curl -sS "${BASE}/v1/quota" \
-H "X-API-Key: pk_YOUR_KEY_HERE" \
-H "Accept: application/json"
import requests
# Same pattern used for every authenticated GET under /v1/
BASE = "__ORIGIN__"
API_KEY = "pk_YOUR_KEY_HERE"
r = requests.get(
f"{BASE}/v1/quota",
headers={"X-API-Key": API_KEY, "Accept": "application/json"},
timeout=60,
)
r.raise_for_status()
data = r.json()
# Example fields: trial_remaining, daily_limit_combined, usage_today,
# daily_left_subscription, total_requests_left_approx
print(data)
const BASE = "__ORIGIN__";
const API_KEY = "pk_YOUR_KEY_HERE";
const r = await fetch(`${BASE}/v1/quota`, {
headers: { "X-API-Key": API_KEY, Accept: "application/json" },
});
if (!r.ok) throw new Error(await r.text());
console.log(await r.json());
<?php $base = "__ORIGIN__"; $key = "pk_YOUR_KEY_HERE"; $ctx = stream_context_create([ "http" => ["header" => "X-API-Key: $key\r\nAccept: application/json\r\n"], ]); echo file_get_contents($base . "/v1/quota", false, $ctx);
// Quick check with net/http — set apiKey constant
req, _ := http.NewRequest("GET", "__ORIGIN__/v1/quota", nil)
req.Header.Set("X-API-Key", "pk_YOUR_KEY_HERE")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
// defer resp.Body.Close(); io.ReadAll(resp.Body)
Conventions
| Voucher codes | PUBG UC codes are exactly 18 characters: letters and digits only (A–Z, a–z, 0–9). Case is not required — upper, lower, or mixed is fine (validation is case-insensitive). |
|---|---|
| Format | Request and response bodies use application/json unless noted. The exception is POST /api/v1/order/screenshot on success: the body is raw WebP bytes (image/webp). Use UTF-8 for JSON. |
| Timestamps | Job and history objects may include Unix seconds (_stamp suffix). Daily quota resets on UTC calendar days. |
| IDs | job_id is a MongoDB ObjectId string. player_id is the numeric PUBG player id (string of digits). |
| Versioning | Prefix /v1/ allows future non-breaking additions. Breaking changes would ship under a new prefix. |
| Idempotency | Each POST to a job endpoint creates a new job record. Submitting the same payload twice intentionally queues two separate tasks; polling uses the distinct job_id returned each time. |
| OpenAPI | Machine-readable schema: /openapi.json · interactive UI: /swagger. |
Authentication
All /v1/* and /api/v1/* routes listed in this reference require the header below, except where explicitly marked public (for example GET /health).
Each key is tied to the same Telegram account wallet and subscriptions used inside the bot.
| Header | X-API-Key: <key> — keys normally begin with pk_. |
|---|---|
| Eligibility | An active base plan (p1 … p6) and an active HTTP API add-on subscription. If either is missing, the API responds with 403 Forbidden. |
| Invalid key | 401 Unauthorized — missing header, wrong value, or revoked key. |
| Security | Store keys in server-side configuration or a secrets manager; avoid exposing them in public web pages or mobile apps. |
GET /health — no key.Protected: all
/v1/* and /api/v1/* routes listed in this document (unless stated otherwise).
Quota & billing (requests)
Usage is counted in requests. Most redeem and check-code operations consume 1 request per voucher; player lookup consumes 0.25 when it succeeds. The service draws free-trial requests first, then the combined daily allowance from active plans. Several overlapping subscriptions add together (for example, multiple rows can increase the daily cap beyond the Titan tier’s 10000 requests per UTC day).
| Daily reset | Subscription usage counters reset at the UTC day boundary (aligned with server usage_today). |
|---|---|
GET /v1/quota | Returns trial left, combined daily cap, usage today, and total_requests_left_approx (trial + daily remaining). |
| Job: check-code | Consumes 1 request per voucher in the job when the worker runs (after queue), not at POST time — same rule as Telegram batch checks (up to 10 codes per job). |
| Job: manual-redeem | Consumes 1 request per voucher code in the job (same rule if you send one code or many in codes). |
| Job: stock-redeem | Consumes 1 request per voucher code redeemed in the batch (equal to number of codes pulled from your stock). |
| Player lookup | GET /v1/player/lookup costs 0.25 requests, charged only if Midas returns a successful name resolution. Upstream failure → 502, no deduction. |
| Fractional totals | Internal accounting supports fractional requests (e.g. several lookups + one redeem). |
| Per-minute throttles | Optional env API_RATE_LIMIT_PER_MINUTE applies to audited /v1/* and /api/v1/* routes (0 = off). POST /api/v1/order/screenshot has an additional cap: API_ORDER_SCREENSHOT_RATE_LIMIT_PER_MINUTE (default 15/min per API key; set to 0 to disable only this extra cap). Does not consume daily redeem quota — it only limits abuse / buggy loops. |
HTTP status reference
Error responses follow FastAPI conventions: a JSON object with a detail field (string or structured object). Logging the HTTP status line together with the response body simplifies troubleshooting. Full outcome codes live under Response catalog.
| Code | When | Typical fix |
|---|---|---|
200 | OK — JSON parsed successfully. For jobs, HTTP 200 only means the document was read; always inspect status inside the JSON (done vs failed). For POST /api/v1/order/screenshot, 200 means the response body is binary WebP, not JSON. | Continue parsing; branch on job state or Content-Type. |
400 | Malformed JSON, unknown plan_key, missing required fields such as player_id, invalid voucher format, or same plan re-purchase without same_plan_action (see SAME_PLAN_CHOICE_REQUIRED in the structured detail object). | Compare the payload with this guide and /openapi.json. |
401 | X-API-Key missing or not recognized. | Re-copy the key from the bot; verify the header name spelling. |
403 | No active API-eligible subscription, or player banned on lookup (PLAYER_BANNED). | Renew plan + API add-on, or block that Player ID. |
404 | Unknown job_id, resource outside the authenticated account, or PLAYER_NOT_FOUND on lookup. | Confirm the id string; list jobs if necessary. |
409 | Job still running (JOB_NOT_READY) — used by POST /api/v1/order/screenshot when status is pending or running. | Poll GET /v1/jobs/{job_id} until status is done, then retry the screenshot. |
429 | Too many requests — (1) per-minute API throttle (RATE_LIMIT / SCREENSHOT_RATE_LIMIT on heavy screenshot route), or (2) daily/trial quota exhausted (quota_exhausted) on redeem/lookup paths. | Respect Retry-After: 60 for throttles; call GET /v1/quota for subscription limits. |
500 | Unexpected server-side failure (including some screenshot render errors). | Retry; if the JSON detail.error is ORDER_SCREENSHOT_FAILED, include the message when contacting support. |
501 | WEBP_ENCODE_FAILED — Pillow cannot encode WebP (missing libwebp / empty encoder output). | Use JSON order_details, or ask the operator to install libwebp for Pillow. |
502 | Upstream failure during player lookup (LOOKUP_FAILED). | Retry later; confirm Player ID format; contact support if persistent. |
503 | SERVICE_UNAVAILABLE — temporary capacity / account path on lookup. | Retry in a few minutes. |
Response catalog (success, failure, err_code)
Integrations must treat three layers separately. A request can return HTTP 200 with job
status: "done" while individual vouchers still failed (success: false + err_code).
| HTTP layer | Status line + optional JSON detail — auth, validation, rate limits, resource existence. |
|---|---|
| Job layer | GET /v1/jobs/{id} fields status / error — queue lifecycle. |
| Outcome layer | result.success, result.results[].success, err_code, check-code ok — per-voucher business result. |
status: "done" means the worker finished — not that UC was credited.
Always inspect every redeem row’s success and err_code. Partial batches are normal.
Structured detail.error codes (non-2xx)
| error | HTTP | Endpoints | Meaning |
|---|---|---|---|
INVALID_PLAYER_ID | 400 | lookup, redeem | Player id failed format validation. |
MISSING_CODES | 400 | check-code, manual-redeem | All codes empty after trim. |
TOO_MANY_CODES | 400 | check-code, manual-redeem | More than 10 vouchers in one job. |
INVALID_CODE | 400 | check-code, manual-redeem | Voucher at index not 18 alphanumeric characters. |
SAME_PLAN_CHOICE_REQUIRED | 400 | subscription/purchase | Re-buying an active plan requires same_plan_action: extend or stack. |
PLAYER_NOT_FOUND | 404 | player/lookup | No character for that Player ID. |
PLAYER_BANNED | 403 | player/lookup | Account restricted; UC cannot be sent. |
SERVICE_UNAVAILABLE | 503 | player/lookup | Temporary upstream/account shortage. |
LOOKUP_FAILED | 502 | player/lookup | Generic upstream failure (no quota charged). |
RATE_LIMIT | 429 | audited API routes | Per-minute throttle; response includes Retry-After: 60. |
SCREENSHOT_RATE_LIMIT | 429 | order/screenshot | Extra screenshot-only throttle. |
JOB_NOT_FOUND | 404 | order/screenshot | No job with this id for the API key. |
JOB_NOT_READY | 409 | order/screenshot | Job still pending or running. |
JOB_NOT_SUCCESSFUL | 400 | order/screenshot | Job is not done (e.g. failed). |
NO_ORDER_DETAILS | 400 | order/screenshot | No successful redeem row (or check-code job). |
WEBP_ENCODE_FAILED | 501 | order/screenshot | Server cannot encode WebP. |
ORDER_SCREENSHOT_FAILED | 500 | order/screenshot | Unexpected render/encode failure. |
Auth failures often use a plain string detail (Missing X-API-Key header, Invalid or revoked API key, subscription text). Sync lookup quota may return detail: "quota_exhausted".
Job lifecycle
Create endpoints return {"job_id":"…","status":"pending"}. Poll until terminal state.
status | pending · running · done · failed |
|---|---|
result | Present when the worker stored an outcome (typically done). Sanitized — no Midas emails/sessions. |
error | String when status is failed. |
Job error (status failed) | Meaning | Client action |
|---|---|---|
quota_exhausted / quota_exhausted:… | Daily/trial requests insufficient when the worker ran. | Call GET /v1/quota; wait for UTC reset or upgrade. |
invalid_payload | Empty codes / bad picks after queue. | Fix request body. |
invalid_player_id | Player id rejected by worker. | Validate digits before POST. |
no_codes / inventory messages | Stock redeem could not reserve requested denominations. | GET /v1/stock/summary; lower picks. |
unknown_job_type:… | Internal mismatch (rare). | Contact support. |
Redeem err_code (job done)
Used on manual_redeem / stock_redeem. Inspect result.results[] (batch) or top-level success / msg / err_code (single).
| err_code | success | Meaning | Recommended UX |
|---|---|---|---|
| (empty / omitted) | true | UC credited; row includes order_details. | Show success + optional screenshot via job_id. |
REDEEM_CODE_ALREADY_USED | false | Voucher already redeemed elsewhere. | Mark code dead; do not retry the same code. |
CODE_EXPIRED | false | Voucher past expiry. | Mark expired; do not retry. |
PLAYER_NOT_FOUND | false | No PUBG character for Player ID (public English msg). | Ask buyer to re-check Player ID; run lookup first. |
PLAYER_BANNED | false | Account restricted in-game; UC cannot be sent. | Refuse order / apply refund policy. |
PLAYER_RISK_LIMITED | false | Player ID blocked from receiving UC (risk controls). | Advise buyer to contact PUBG Mobile support. |
ROLE_INACTIVE_LONG_TIME | false | Wrong ID or account inactive for a very long time. | Ask player to log into PUBG Mobile, then retry. |
REDEEM_FAILED | false | Neutral public failure (internal capacity/routing mapped away). | Retry later; verify Player ID; contact support if repeated. |
CDKEY_STATUS_INVALID | false | Upstream rejected voucher state. | Run check-code; do not assume unused. |
MISSING_PRODUCT_INFO | false | Could not map voucher to a product SKU. | Retry once; escalate if persistent. |
| other / empty on failure | false | Transport or transient upstream issues. | Show msg; retry with backoff. |
Check-code outcomes
| Signal | Meaning |
|---|---|
ok: true + cdkey_status: 1 | Unused / available voucher (typical “safe to sell”). |
ok: true + cdkey_status: 2 | Already used (treat as dead inventory). |
err_code: INVALID_OR_EXPIRED_CODE | No usable code info from upstream. |
err_code: CODE_EXPIRED | Confirmed expired; may include expired_confirmed: true. |
err_code: PLAYER_NOT_FOUND | Player context missing / not found during query path. |
err_code: ROLE_INACTIVE_LONG_TIME | Inactive / stale role for linked player context. |
err_code: SERVICE_UNAVAILABLE | Temporary service/account shortage (neutral message). |
cdkey_expires_at_unix / cdkey_expires_at_utc | Present when Midas returns parseable expiry. |
batch: true + results[] | Multi-code job; evaluate each row independently. |
Example — redeem job done with a failed voucher
Example — job failed (quota)
Hub mirror for crawlers / GitHub visitors: responses.html.
GET /health
Liveness probe for load balancers and uptime monitors. No authentication. Does not touch Midas or quota.
| Response | 200 OK · JSON {"ok": true} |
|---|---|
| Use case | Kubernetes / Cloudflare health checks; quick TLS verification. |
curl -sS "${BASE}/health"
import requests
print(requests.get("__ORIGIN__/health", timeout=15).json())
const r = await fetch("__ORIGIN__/health");
console.log(await r.json());
<?php echo file_get_contents("__ORIGIN__/health");
resp, err := http.Get("__ORIGIN__/health")
if err != nil { panic(err) }
defer resp.Body.Close()
GET /v1/player/lookup
Returns the linked in-game character name for a numeric PUBG player_id using the same Midas preparation flow as redeem operations.
Typical use: display the nickname on a checkout screen before charging UC so customers can confirm they typed the correct Player ID.
| Query | player_id — required. Numeric string (typically 5–24 digits). |
|---|---|
| Quota | Default 0.25 requests charged only on HTTP 200 success (quota_requests_charged). Self-hosted: override with env PLAYER_LOOKUP_QUOTA_COST. |
| Errors | 400 missing id · 429 insufficient quota at request time · 502 Midas/account path failed (no quota deduction). |
Success — HTTP 200
Typical error — HTTP 502 (upstream)
Returned when Midas or account initialization fails. No quota is deducted for lookup in this case.
curl -sS "${BASE}/v1/player/lookup?player_id=5123456789" \
-H "X-API-Key: pk_YOUR_KEY_HERE" \
-H "Accept: application/json"
import requests
BASE = "__ORIGIN__"
API_KEY = "pk_YOUR_KEY_HERE"
r = requests.get(
f"{BASE}/v1/player/lookup",
params={"player_id": "5123456789"},
headers={"X-API-Key": API_KEY, "Accept": "application/json"},
timeout=120,
)
print(r.status_code, r.json())
const BASE = "__ORIGIN__";
const API_KEY = "pk_YOUR_KEY_HERE";
const u = new URL(`${BASE}/v1/player/lookup`);
u.searchParams.set("player_id", "5123456789");
const r = await fetch(u, {
headers: { "X-API-Key": API_KEY, Accept: "application/json" },
});
console.log(r.status, await r.json());
<?php
$key = "pk_YOUR_KEY_HERE";
$q = http_build_query(["player_id" => "5123456789"]);
$ctx = stream_context_create([
"http" => ["header" => "X-API-Key: $key\r\nAccept: application/json\r\n"],
]);
echo file_get_contents("__ORIGIN__/v1/player/lookup?" . $q, false, $ctx);
req, _ := http.NewRequest("GET", "__ORIGIN__/v1/player/lookup?player_id=5123456789", nil)
req.Header.Set("X-API-Key", "pk_YOUR_KEY_HERE")
req.Header.Set("Accept", "application/json")
// resp, err := http.DefaultClient.Do(req)
GET /v1/me
Snapshot of the authenticated customer account: USDT wallet balance and a normalized subscription object (aligned with the Telegram bot’s “My account” / subscription area).
balance_usdt | Available wallet balance for purchases. |
|---|---|
subscription | trial_remaining, daily_limit, usage_today, daily_left, subscriptions[] with plan_key, api_enabled, ends_at_stamp. |
has_api_subscription | Always true when this endpoint returns 200 (API key path requires +API). |
Example — HTTP 200
curl -sS "${BASE}/v1/me" \
-H "X-API-Key: pk_YOUR_KEY_HERE" \
-H "Accept: application/json"
import requests
print(requests.get("__ORIGIN__/v1/me", headers={"X-API-Key": "pk_YOUR_KEY_HERE"}, timeout=60).json())
const r = await fetch("__ORIGIN__/v1/me", {
headers: { "X-API-Key": "pk_YOUR_KEY_HERE", Accept: "application/json" },
});
console.log(await r.json());
<?php
$ctx = stream_context_create(["http" => ["header" => "X-API-Key: pk_YOUR_KEY_HERE\r\nAccept: application/json\r\n"]]);
echo file_get_contents("__ORIGIN__/v1/me", false, $ctx);
req, _ := http.NewRequest("GET", "__ORIGIN__/v1/me", nil)
req.Header.Set("X-API-Key", "pk_YOUR_KEY_HERE")
GET /v1/quota
Single-call summary of remaining trial requests, today’s daily cap, and how much of that cap has been used (UTC day).
trial_remaining | Promotional / trial requests remaining. |
|---|---|
daily_limit_combined | Sum of per-day limits across active subscription rows. |
usage_today | Requests consumed today (UTC) against subscription allowance. |
daily_left_subscription | Remaining subscription requests today (≥ 0). |
total_requests_left_approx | Trial remaining + daily_left — estimate before enqueueing costly jobs. |
curl -sS "${BASE}/v1/quota" \
-H "X-API-Key: pk_YOUR_KEY_HERE" \
-H "Accept: application/json"
import requests
print(requests.get("__ORIGIN__/v1/quota", headers={"X-API-Key": "pk_YOUR_KEY_HERE"}).json())
const r = await fetch("__ORIGIN__/v1/quota", {
headers: { "X-API-Key": "pk_YOUR_KEY_HERE" },
});
console.log(await r.json());
<?php
$ctx = stream_context_create(["http" => ["header" => "X-API-Key: pk_YOUR_KEY_HERE\r\n"]]);
echo file_get_contents("__ORIGIN__/v1/quota", false, $ctx);
req, _ := http.NewRequest("GET", "__ORIGIN__/v1/quota", nil)
req.Header.Set("X-API-Key", "pk_YOUR_KEY_HERE")
GET /v1/stock/summary
Counts of voucher codes that are still available in the account’s uploaded inventory, grouped by UC pack size (denomination). Use this to show “how many 60 / 325 / … codes are left” before building a stock-redeem request.
by_denomination_uc | Object keyed by denomination string ("60", "325", …) → integer count ready to redeem. |
|---|---|
denominations | Array of supported denomination values for UI selectors. |
Example — HTTP 200
curl -sS "${BASE}/v1/stock/summary" \
-H "X-API-Key: pk_YOUR_KEY_HERE"
import requests
print(requests.get("__ORIGIN__/v1/stock/summary", headers={"X-API-Key": "pk_YOUR_KEY_HERE"}).json())
const r = await fetch("__ORIGIN__/v1/stock/summary", {
headers: { "X-API-Key": "pk_YOUR_KEY_HERE" },
});
console.log(await r.json());
<?php
$ctx = stream_context_create(["http" => ["header" => "X-API-Key: pk_YOUR_KEY_HERE\r\n"]]);
echo file_get_contents("__ORIGIN__/v1/stock/summary", false, $ctx);
req, _ := http.NewRequest("GET", "__ORIGIN__/v1/stock/summary", nil)
req.Header.Set("X-API-Key", "pk_YOUR_KEY_HERE")
GET /v1/history
Unified redemption activity for the API account (newest events first): manual redeems, stock batch redeems, and related log sources. Suitable for receipts, accounting exports, and support. Use limit and offset to page through long histories.
| Query | limit — optional. Default 30, maximum 100 per request. |
|---|---|
| Query | offset — optional. Default 0. Skip this many rows from the newest (e.g. offset=200 with limit=100 returns the next 100 rows after the 200 most recent entries). |
| Response | items (array of row objects), limit, offset, and total (number of rows that match, used for pagination UI). |
| Items | Each item may include fields such as code, denomination, player_id, status, timestamps, and charac_name depending on the source of the row. |
Example — HTTP 200
curl -sS "${BASE}/v1/history?limit=20&offset=0" \
-H "X-API-Key: pk_YOUR_KEY_HERE"
import requests
print(requests.get(
"__ORIGIN__/v1/history",
params={"limit": 20, "offset": 0},
headers={"X-API-Key": "pk_YOUR_KEY_HERE"},
).json())
const u = new URL("__ORIGIN__/v1/history");
u.searchParams.set("limit", "20");
u.searchParams.set("offset", "0");
const r = await fetch(u, { headers: { "X-API-Key": "pk_YOUR_KEY_HERE" } });
console.log(await r.json());
<?php
$ctx = stream_context_create(["http" => ["header" => "X-API-Key: pk_YOUR_KEY_HERE\r\n"]]);
echo file_get_contents("__ORIGIN__/v1/history?limit=20&offset=0", false, $ctx);
req, _ := http.NewRequest("GET", "__ORIGIN__/v1/history?limit=20&offset=0", nil)
req.Header.Set("X-API-Key", "pk_YOUR_KEY_HERE")
POST /v1/subscription/purchase
Purchases a 30-day subscription tier using the account’s USDT wallet balance inside the Telegram ecosystem. When multiple subscription rows overlap, their daily request limits add together. If you already have the same plan_key active, you must send same_plan_action: extend (+30 days on the farthest row; combined daily cap unchanged) or stack (new parallel row; limits sum).
| Body | plan_key — required: p1 … p6. api_enabled is deprecated and must be false; HTTP API access is now sold separately via POST /v1/subscription/purchase-api-addon. Optional same_plan_action: extend or stack — required when re-buying a tier you already have active (otherwise the API returns 400 with detail.error: "SAME_PLAN_CHOICE_REQUIRED" and preview numbers). |
|---|---|
| Pricing | Base prices are listed below. The HTTP API add-on is a standalone purchase: $15 USDT / 30 days. It requires an active normal plan to buy and to stay active (API access is suspended automatically when no normal plan is active). |
| Success | {"ok": true, "result": { … }} — result.mode may be new, stacked, or extended. |
| Failure | 400 — insufficient balance, invalid plan, or missing same_plan_action when required. |
| plan_key | Daily requests | Base USDT |
|---|---|---|
| p1 | 25 | 5 |
| p2 | 200 | 10 |
| p3 | 1000 | 25 |
| p4 | 2500 | 50 |
| p5 | 5000 | 85 |
| p6 | 10000 | 135 |
Example — success response (HTTP 200)
The result object is documented in the OpenAPI schema; mode describes whether a row was created fresh, stacked, or extended.
curl -sS -X POST "${BASE}/v1/subscription/purchase" \
-H "X-API-Key: pk_YOUR_KEY_HERE" -H "Content-Type: application/json" \
-d "{\"plan_key\":\"p2\",\"api_enabled\":false}"
# Re-buy same tier you already have: choose extend or stack
curl -sS -X POST "${BASE}/v1/subscription/purchase" \
-H "X-API-Key: pk_YOUR_KEY_HERE" -H "Content-Type: application/json" \
-d "{\"plan_key\":\"p2\",\"api_enabled\":false,\"same_plan_action\":\"extend\"}"
curl -sS -X POST "${BASE}/v1/subscription/purchase-api-addon" \
-H "X-API-Key: pk_YOUR_KEY_HERE"
import requests
r = requests.post(
"__ORIGIN__/v1/subscription/purchase",
headers={"X-API-Key": "pk_YOUR_KEY_HERE", "Content-Type": "application/json"},
json={"plan_key": "p2", "api_enabled": False},
timeout=60,
)
print(r.status_code, r.json())
r2 = requests.post(
"__ORIGIN__/v1/subscription/purchase",
headers={"X-API-Key": "pk_YOUR_KEY_HERE", "Content-Type": "application/json"},
json={"plan_key": "p2", "api_enabled": False, "same_plan_action": "stack"},
timeout=60,
)
print(r2.status_code, r2.json())
# Standalone HTTP API add-on:
r = requests.post(
"__ORIGIN__/v1/subscription/purchase-api-addon",
headers={"X-API-Key": "pk_YOUR_KEY_HERE"},
timeout=60,
)
print(r.status_code, r.json())
const r = await fetch("__ORIGIN__/v1/subscription/purchase", {
method: "POST",
headers: { "X-API-Key": "pk_YOUR_KEY_HERE", "Content-Type": "application/json" },
body: JSON.stringify({ plan_key: "p2", api_enabled: false }),
});
console.log(await r.json());
// Standalone HTTP API add-on:
const r2 = await fetch("__ORIGIN__/v1/subscription/purchase-api-addon", {
method: "POST",
headers: { "X-API-Key": "pk_YOUR_KEY_HERE" },
});
console.log(await r2.json());
<?php
$body = json_encode(["plan_key" => "p2", "api_enabled" => false]);
$ctx = stream_context_create(["http" => [
"method" => "POST",
"header" => "X-API-Key: pk_YOUR_KEY_HERE\r\nContent-Type: application/json\r\n",
"content" => $body,
]]);
echo file_get_contents("__ORIGIN__/v1/subscription/purchase", false, $ctx);
body := strings.NewReader(`{"plan_key":"p2","api_enabled":false}`)
req, _ := http.NewRequest("POST", "__ORIGIN__/v1/subscription/purchase", body)
req.Header.Set("X-API-Key", "pk_YOUR_KEY_HERE")
req.Header.Set("Content-Type", "application/json")
Asynchronous jobs
Operations that redeem UC or query Midas for voucher details run as background jobs. Each relevant POST responds immediately with a small JSON envelope containing job_id and status: "pending".
The integration then calls GET /v1/jobs/{job_id} on a short interval until the job finishes. This pattern keeps HTTP connections short and works reliably behind proxies and mobile networks.
- Create — Send the JSON body for check-code, manual-redeem, or stock-redeem. The HTTP response body includes
job_id(MongoDB ObjectId string) and initialstatus. - Poll — Request
GET /v1/jobs/{job_id}every 1–3 seconds (with backoff if desired). Typical lifecycle:pending→running→doneorfailed. - Read outcome — When
statusisdone, read theresultobject (shape depends on job type; examples appear under each endpoint). Whenstatusisfailed, readerror(human-readable string). - Quota — Billable requests are consumed when the worker executes work, not when the job is created. Some failures still consume quota depending on how far processing progressed;
GET /v1/quotareflects the live balance.
Immediate response after POST (all job types)
HTTP 200 OK. Store job_id for polling.
Polling responses — job still running
status may be pending (queued) or running. The example omits large fields; live responses include timestamps.
Polling responses — job finished successfully
status is done; result holds the sanitized outcome (exact fields depend on type — see sections below).
Polling responses — job failed
status is failed; error explains the failure; result is usually null.
Example: poll loop (Python)
Response privacy: Job result objects are filtered for public API use. Session tokens, Midas login email, raw upstream payloads, and similar internal fields are removed.
Product titles, voucher status, timestamps, optional voucher expiry (cdkey_expires_at_unix / cdkey_expires_at_utc on check-code when Midas includes redemptionDetail.cdkey_endtime), and safe verification fields (for example matched_player_id / matched_charac_name on check-code) remain available when applicable.
Successful manual-redeem and stock-redeem rows may include an order_details block (normalized price, rewards, VIP points, order ids, display time) suitable for receipts — see the sections for each job type.
POST /v1/jobs/check-code
Queues one or more voucher inspections against Midas (query redeem gift info): whether each code is valid, already used, expired, and which UC product it maps to.
The HTTP response only contains job_id; the outcome arrives after polling GET /v1/jobs/{job_id}.
Each voucher is exactly 18 letters or digits (any mixture of upper and lower case). Up to 10 codes per job (same cap as manual redeem batch). Billing: 1 request per code when the worker runs (see Quota).
When redemptionDetail.cdkey_endtime is present, the public API includes normalized expiry fields (see examples below); the Telegram bot may also show that date for unused codes using the user’s timezone preference.
Request body (v1.6): send only "codes": [ ... ] — a JSON array of voucher strings.
For one code, the array has one element (this is the normal shape for every check; there is no separate code string field).
For several codes, append more strings in the same array (max 10). Order is preserved; quota counts one request per non-empty element after trimming.
| JSON body | Required key: codes — non-empty array of 1–10 strings (each a full 18-char UC token, any case). Example single check: {"codes":["a1B2c3D4e5F6g7H8i9"]}. Example batch: {"codes":["a1B2c3D4e5F6g7H8i9","x9Y8z7W6v5U4t3S2r1"]}. |
|---|---|
| Immediate HTTP response | 200 OK with job_id and status: "pending" — see Asynchronous jobs. |
Final result (when status is done) | One code: flat sanitized object (ok, checked_code, cdkey_status, cdkey_name, optional voucher expiry cdkey_expires_at_unix / cdkey_expires_at_utc derived from Midas redemptionDetail.cdkey_endtime when present, other optional times, matched_player_id / matched_charac_name when applicable). Multiple codes: batch: true and results[] — one sanitized object per code (same field ideas as single). |
Example — request JSON (single voucher)
One code is still a list of length 1 — this keeps the contract identical for scripts and storefronts.
Example — request JSON (three vouchers in one job)
Example — POST response body
Example — completed job result (success path)
Exact keys vary with voucher state; unused codes typically include product metadata. cdkey_expires_at_unix / cdkey_expires_at_utc appear only when redemptionDetail.cdkey_endtime is present and parseable.
Example — completed job result (multi-code, public API)
POST body for this job was e.g. {"codes":["a1B2c3D4e5F6g7H8i9","x9Y8z7W6v5U4t3S2r1"]} (always an array). Internal session fields are stripped in API responses.
Use any language that can send HTTPS + JSON. Define BASE (the API origin), copy API_KEY from Telegram, set headers X-API-Key and Content-Type: application/json, POST the body, read job_id, then poll as described under Asynchronous jobs.
# Single voucher (still use `codes` as a JSON array)
curl -sS -X POST "${BASE}/v1/jobs/check-code" \
-H "X-API-Key: pk_YOUR_KEY_HERE" -H "Content-Type: application/json" \
-d "{\"codes\":[\"a1B2c3D4e5F6g7H8i9\"]}"
# Up to 10 codes — quota: 1 request per array element when the worker runs
curl -sS -X POST "${BASE}/v1/jobs/check-code" \
-H "X-API-Key: pk_YOUR_KEY_HERE" -H "Content-Type: application/json" \
-d "{\"codes\":[\"a1B2c3D4e5F6g7H8i9\",\"x9Y8z7W6v5U4t3S2r1\",\"b2C3d4E5f6G7h8I9j0\"]}"
import requests
BASE = "__ORIGIN__"
API_KEY = "pk_YOUR_KEY_HERE"
# Always pass `codes` as a list — one element for a single code, many for a batch (max 10).
body_single = {"codes": ["a1B2c3D4e5F6g7H8i9"]}
body_batch = {
"codes": [
"a1B2c3D4e5F6g7H8i9",
"x9Y8z7W6v5U4t3S2r1",
"b2C3d4E5f6G7h8I9j0",
]
}
body = body_single
resp = requests.post(
f"{BASE}/v1/jobs/check-code",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
"Accept": "application/json",
},
json=body,
timeout=60,
)
resp.raise_for_status()
pending = resp.json()
job_id = pending["job_id"]
print("Queued:", pending)
const body = { codes: ["a1B2c3D4e5F6g7H8i9"] };
const r = await fetch(BASE + "/v1/jobs/check-code", {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
console.log(await r.json());
<?php $body = json_encode(["codes" => ["a1B2c3D4e5F6g7H8i9"]]); $ctx = stream_context_create(["http" => ["method"=>"POST","header"=>"X-API-Key: $key\r\nContent-Type: application/json\r\n","content"=>$body]]); echo file_get_contents(BASE . "/v1/jobs/check-code", false, $ctx);
body := strings.NewReader(`{"codes":["a1B2c3D4e5F6g7H8i9"]}`)
req, _ := http.NewRequest("POST", BASE+"/v1/jobs/check-code", body)
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
POST /v1/jobs/manual-redeem
Queues redemption of voucher codes that are not pulled from uploaded stock (manual / external codes).
All codes in one request share a single Midas session for the given player_id.
Billing: 1 request per voucher when the worker runs (same rule as check-code).
Each code must be 18 letters or digits; letter case may be mixed.
Request body: send player_id and codes — a JSON array of voucher strings (same contract as check-code: use a one-element array for a single voucher).
| JSON body | Required keys: player_id (numeric PUBG id string) and codes — non-empty array of 1–10 strings (each a full 18-char UC token, any case). Example one voucher: {"player_id":"5123456789","codes":["a1B2c3D4e5F6g7H8i9"]}. Example batch: {"player_id":"5123456789","codes":["a1B2c3D4e5F6g7H8i9","x9Y8z7W6v5U4t3S2r1"]}. |
|---|---|
| Immediate HTTP response | 200 OK with job_id and status: "pending" — see Asynchronous jobs. |
Replace placeholder codes with real 18-character vouchers. Poll GET /v1/jobs/{job_id} for redemption outcomes (see Asynchronous jobs).
curl -sS -X POST "${BASE}/v1/jobs/manual-redeem" \
-H "X-API-Key: pk_YOUR_KEY_HERE" -H "Content-Type: application/json" \
-d "{\"player_id\":\"5123456789\",\"codes\":[\"a1B2c3D4e5F6g7H8i9\",\"x9Y8z7W6v5U4t3S2r1\"]}"
import requests
BASE = "__ORIGIN__"
API_KEY = "pk_YOUR_KEY_HERE"
payload = {
"player_id": "5123456789",
"codes": [
"a1B2c3D4e5F6g7H8i9",
"x9Y8z7W6v5U4t3S2r1",
],
}
resp = requests.post(
f"{BASE}/v1/jobs/manual-redeem",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
"Accept": "application/json",
},
json=payload,
timeout=60,
)
print(resp.status_code, resp.json())
const r = await fetch(BASE + "/v1/jobs/manual-redeem", {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
player_id: "5123456789",
codes: ["a1B2c3D4e5F6g7H8i9", "x9Y8z7W6v5U4t3S2r1"],
}),
});
console.log(await r.json());
<?php $b = json_encode(["player_id"=>"5123456789","codes"=>["a1B2c3D4e5F6g7H8i9","x9Y8z7W6v5U4t3S2r1"]]); $ctx = stream_context_create(["http" => ["method"=>"POST","header"=>"X-API-Key: $key\r\nContent-Type: application/json\r\n","content"=>$b]]); echo file_get_contents(BASE . "/v1/jobs/manual-redeem", false, $ctx);
body := strings.NewReader(`{"player_id":"5123456789","codes":["a1B2c3D4e5F6g7H8i9","x9Y8z7W6v5U4t3S2r1"]}`)
req, _ := http.NewRequest("POST", BASE+"/v1/jobs/manual-redeem", body)
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
Successful result shape
For one voucher (codes had one element), the job result is often a flat object with per-code fields plus order_details when redemption succeeded.
For two or more vouchers, expect results[] (one object per code), ok_count, fail_count, and top-level success when every code succeeded.
Each successful row may include order_details as with stock redeems.
{
"player_id": "5867494047",
"ok_count": 2,
"fail_count": 0,
"success": true,
"results": [
{
"success": true,
"code": "XXXXXXXXXXXXXXXXXX",
"denomination": 660,
"cdkey_name": "600 UC",
"order_details": {
"cart_order_number": "-CART1001-20250605-NuR39Ocy7WEF",
"price": "600",
"rewards": "UC* 60",
"vip_points": 100,
"product": "UC",
"payment": "redeem",
"status": "Success"
}
}
]
}
POST /v1/jobs/stock-redeem
Queues redemption using codes from the account’s uploaded inventory. The picks object maps each UC denomination (string keys such as "60", "325") to how many codes to consume.
Total cost equals the number of codes redeemed (one request per code). If the inventory does not contain enough codes for a denomination, the job may fail with an inventory-related error.
player_id | Target player. |
|---|---|
picks | e.g. {"60": 1, "325": 2} — keys are denomination strings, values positive integers. |
| Stock | If the request asks for more codes than are available for a denomination, the job may end in failed with an inventory-related error string. |
Example — immediate POST response
Confirm denominations exist using GET /v1/stock/summary. Poll GET /v1/jobs/{job_id} for final redemption rows (Asynchronous jobs).
curl -sS -X POST "${BASE}/v1/jobs/stock-redeem" \
-H "X-API-Key: pk_YOUR_KEY_HERE" -H "Content-Type: application/json" \
-d "{\"player_id\":\"5123456789\",\"picks\":{\"60\":1,\"325\":1}}"
import requests
BASE = "__ORIGIN__"
API_KEY = "pk_YOUR_KEY_HERE"
# Redeem one 60 UC code and one 325 UC code from uploaded stock
body = {
"player_id": "5123456789",
"picks": {"60": 1, "325": 1},
}
resp = requests.post(
f"{BASE}/v1/jobs/stock-redeem",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
"Accept": "application/json",
},
json=body,
timeout=60,
)
print(resp.status_code, resp.json())
const r = await fetch(BASE + "/v1/jobs/stock-redeem", {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ player_id: "5123456789", picks: { 60: 1, 325: 1 } }),
});
console.log(await r.json());
<?php $b = json_encode(["player_id"=>"5123456789","picks"=>["60"=>1,"325"=>1]]); $ctx = stream_context_create(["http" => ["method"=>"POST","header"=>"X-API-Key: $key\r\nContent-Type: application/json\r\n","content"=>$b]]); echo file_get_contents(BASE . "/v1/jobs/stock-redeem", false, $ctx);
body := strings.NewReader(`{"player_id":"5123456789","picks":{"60":1,"325":1}}`)
req, _ := http.NewRequest("POST", BASE+"/v1/jobs/stock-redeem", body)
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
Successful result shape
When status: "done" and type: "stock_redeem", result includes player_id and a results array—one object per voucher.
Each element that redeemed successfully includes the same order_details structure as manual-redeem (sanitized row fields apply per item).
{
"player_id": "5565630418",
"results": [
{
"success": true,
"code": "XXXXXXXXXXXXXXXXXX",
"denomination": 24300,
"msg": "",
"product_id": "24300_coins_redeem_vip",
"cdkey_name": "18000 UC",
"charac_name": "R͜͡๛』Mafia",
"order_number": "-CART1001-20250706-2rD39OckhUbD",
"order_details": {
"cart_order_number": "-CART1001-20250706-2rD39OckhUbD",
"order_number": "-CART1001-20250706-2rD39OckhUbD",
"order_time_unix": 1750381002,
"order_time_display": "7/5/2025, 9:53:13 PM",
"payment": "redeem",
"status": "Success",
"product": "UC",
"rewards": "UC* 6300",
"vip_points": 3000,
"price": "18000",
"player_id": "5565630418",
"charac_name": "R͜͡๛』Mafia",
"cdkey_name": "18000 UC",
"product_id": "24300_coins_redeem_vip"
}
}
]
}
GET /v1/jobs/{job_id}
Returns one job document belonging to the authenticated API account. The path parameter is the job_id string returned when the job was created.
Poll this endpoint until status becomes done or failed. The JSON shape is stable across job types; only payload, result, and error vary.
status | pending · running · done · failed (and transitional states during execution). |
|---|---|
result | Present when finished successfully — sanitized per job type. Redeem jobs include order_details on success (see manual-redeem / stock-redeem sections for field list and examples). |
error | Human-readable failure reason when status is failed. |
| HTTP | 404 when the id is unknown or belongs to a different API account. |
Example — job succeeded (done)
Example — job failed
curl -sS "${BASE}/v1/jobs/JOB_ID" -H "X-API-Key: pk_YOUR_KEY_HERE"
import requests
BASE = "__ORIGIN__"
API_KEY = "pk_YOUR_KEY_HERE"
job_id = "674a1b2c3d4e5f60718293a4"
r = requests.get(
f"{BASE}/v1/jobs/{job_id}",
headers={"X-API-Key": API_KEY, "Accept": "application/json"},
timeout=60,
)
print(r.status_code, r.json())
const jid = "JOB_ID";
const r = await fetch(BASE + "/v1/jobs/" + jid, { headers: { "X-API-Key": API_KEY } });
console.log(await r.json());
<?php $id = "JOB_ID"; $ctx = stream_context_create(["http" => ["header" => "X-API-Key: $key\r\n"]]); echo file_get_contents(BASE . "/v1/jobs/" . $id, false, $ctx);
req, _ := http.NewRequest("GET", BASE+"/v1/jobs/"+jobID, nil)
req.Header.Set("X-API-Key", apiKey)
GET /v1/jobs
Returns a paginated list of jobs for the authenticated account, newest first. Useful for dashboards, audit trails, and locating past job_id values.
| Query | limit — optional, default 20, max 100. |
|---|---|
| Query | offset — optional, default 0. Skip this many jobs from the newest (for example offset=200 with limit=100 loads the next page after the first 200 rows). |
| Response | jobs (array of job documents), limit, offset, and total (total job count for this API key). |
Example — HTTP 200 body
curl -sS "${BASE}/v1/jobs?limit=10&offset=0" -H "X-API-Key: pk_YOUR_KEY_HERE"
import requests
print(requests.get(BASE + "/v1/jobs", params={"limit": 10, "offset": 0}, headers={"X-API-Key": API_KEY}).json())
const r = await fetch(BASE + "/v1/jobs?limit=10&offset=0", { headers: { "X-API-Key": API_KEY } });
console.log(await r.json());
<?php $ctx = stream_context_create(["http" => ["header" => "X-API-Key: $key\r\n"]]); echo file_get_contents(BASE . "/v1/jobs?limit=10&offset=0", false, $ctx);
req, _ := http.NewRequest("GET", BASE+"/v1/jobs?limit=10&offset=0", nil)
req.Header.Set("X-API-Key", apiKey)
POST /api/v1/order/screenshot
Renders the same ORDER DETAILS image as the bot (Midasbuy-style layout) using data stored on the server.
You send only the job_id of an API job you own (the same id returned by POST /v1/jobs/manual-redeem, …/stock-redeem, etc.).
The service loads the job from the database, reads order_details from the finished result (same logic as GET /v1/jobs/{job_id}), draws the card in memory, and returns raw WebP bytes — no disk file.
| Auth | X-API-Key — same eligibility as other subscriber routes (plan + API add-on). |
|---|---|
| Request | Content-Type: application/json. Required: job_id (string). Optional: result_index (non-negative integer) for batch redeems with result.results[] — pick which row’s order_details to render; if omitted, the first successful row with order_details is used. Optional: webp_quality (1–100, default 88). |
| Success (HTTP 200) | Content-Type: image/webp — binary body (not JSON). |
404 | No job with that id for your API key. |
409 | Job still pending or running — wait until done. |
400 | Job finished but not usable for a screenshot (failed job, check-code job, or no order_details on the chosen row). |
429 | Too many screenshot requests per minute for this API key (SCREENSHOT_RATE_LIMIT) or global per-minute API throttle (RATE_LIMIT). Response includes Retry-After: 60. |
| Other errors | 501 WEBP_ENCODE_FAILED if WebP encoding is unavailable; JSON detail on errors. |
Example (curl — save to file)
curl -sS -X POST "${BASE}/api/v1/order/screenshot" \
-H "X-API-Key: pk_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-H "Accept: image/webp" \
-d '{"job_id":"674a1b2c3d4e5f60718293a4"}' \
-o order.webp
import pathlib
import requests
BASE = "__ORIGIN__"
API_KEY = "pk_YOUR_KEY_HERE"
JOB_ID = "674a1b2c3d4e5f60718293a4"
r = requests.post(
f"{BASE}/api/v1/order/screenshot",
headers={"X-API-Key": API_KEY, "Accept": "image/webp"},
json={"job_id": JOB_ID, "webp_quality": 88},
timeout=60,
)
if r.status_code == 200 and r.headers.get("Content-Type", "").startswith("image/webp"):
pathlib.Path("order.webp").write_bytes(r.content)
print("saved order.webp", len(r.content), "bytes")
else:
print(r.status_code, r.text)
const r = await fetch(BASE + "/api/v1/order/screenshot", {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
Accept: "image/webp",
},
body: JSON.stringify({ job_id: "674a1b2c3d4e5f60718293a4" }),
});
if (!r.ok) {
console.log(await r.text());
} else {
const buf = await r.arrayBuffer();
console.log("webp bytes", buf.byteLength);
}
<?php
$payload = json_encode(["job_id" => "674a1b2c3d4e5f60718293a4"]);
$ctx = stream_context_create([
"http" => [
"method" => "POST",
"header" => "X-API-Key: $key\r\nContent-Type: application/json\r\nAccept: image/webp\r\n",
"content" => $payload,
],
]);
$raw = file_get_contents(BASE . "/api/v1/order/screenshot", false, $ctx);
file_put_contents("order.webp", $raw);
payload := strings.NewReader(`{"job_id":"674a1b2c3d4e5f60718293a4"}`)
req, _ := http.NewRequest("POST", BASE+"/api/v1/order/screenshot", payload)
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "image/webp")
resp, err := http.DefaultClient.Do(req)
// defer resp.Body.Close(); io.Copy(outFile, resp.Body)
Works for successful manual-redeem and stock-redeem jobs that expose order_details after GET /v1/jobs/{job_id}. Does not apply to check-code jobs (no order receipt).
Swagger — interactive testing
Authorize with X-API-Key, then execute endpoints directly from the browser.
For the clearest layout, open Swagger in a full tab rather than only the embedded frame below.
Open Swagger in new tab Same origin · full window · no cramped box