Play Cards API
A simple HTTP API to check your balance, browse products, place orders, and track their status — built for resellers and automation.
Introduction #
The API speaks JSON over HTTPS. If you already integrate with a standard reseller API in this market, you migrate by changing only the base URL and your token — the paths, envelopes, and error codes match.
order_uuid.Authentication #
Every request must include your API token in the api-token header. You can generate and manage your token, allowed IPs, and callback URL from your account.
api-token: YOUR_API_TOKEN
Two additional header forms are accepted for convenience: x-api-key: YOUR_API_TOKEN and Authorization: Bearer YOUR_API_TOKEN.
123. Leave the list empty (or enable “allow all IPs”) to accept requests from anywhere.Rate limits #
API requests are rate-limited per token. Stay under the limits below and you will never see an HTTP 429.
| Scope | Limit | When exceeded |
|---|---|---|
| newOrder | 120 requests per minute per token | HTTP 429 |
products (catalog sync: plain or price=1) | 1 request per minute per token — the full catalog in one response; syncing every 1–2 minutes is the intended pace | HTTP 429 with a Retry-After header (seconds) |
products with base=1 (product-linking list) | 6 requests per minute, up to 60 per hour per token — sized for a human linking products, not for automated polling | HTTP 429 with a Retry-After header (seconds) |
| orders (reconciliation list) | 60 requests per minute per token — pull a window and reconcile, rather than polling in a tight loop | HTTP 429 |
| other endpoints | 1200 requests per minute per token (content, check, profile, changeStateHistory) | HTTP 429 |
| checkId | 60 lookups per minute per token by default; cached repeats are free | HTTP 429 · code 135 |
These are counted per token, not per IP, so spreading requests across servers does not raise the limit. On HTTP 429, pause and retry after a few seconds — the response message includes how long to wait.
/client/api/products?price=1 — it returns a lightweight payload for every product. Do not call the API once per product: a large catalog synced product-by-product burns through the read limit for no benefit.Webhooks #
Instead of polling /check, set a callback URL on your account and we’ll POST to it whenever one of your orders reaches a final state. This is optional and complements the polling endpoint.
Headers we send
| Header | Description |
|---|---|
| X-PlayCore-Event | order_completed or order_rejected |
| X-PlayCore-Callback-Id | Unique id of this callback attempt (delivery is retried on failure) |
| X-PlayCore-Timestamp | Unix time in milliseconds when we signed the request — sent only when a signing secret is set. See Signature verification below. |
| X-PlayCore-Signature | sha256=<hmac> — sent only when a signing secret is set. |
{
"event": "order_completed",
"order": {
"id": 10231,
"publicId": "ID_9fffb0d849a45215",
"orderUuid": "ecbdd545-e616-4aee-8770-7eefa977bcd0",
"apiClientReference": null,
"status": "completed",
"totalPrice": "1.26048",
"currencyCode": "USD",
"inputs": { "playerId": "test" },
"completedAt": "2026-07-06T13:55:48.000Z",
"failedAt": null,
"createdAt": "2026-07-06T13:55:40.000Z",
"updatedAt": "2026-07-06T13:55:48.000Z"
}
}
/client/api/check?orders=[publicId] before acting on it.Signature verification (optional)
Generate a signing secret from your account (API settings → Webhook signature). Once set, every callback also carries the X-PlayCore-Timestamp and X-PlayCore-Signature headers, letting you confirm a request truly came from us before acting on it. Leaving the secret unset changes nothing — callbacks are simply sent unsigned.
The signature is sha256= followed by the hex HMAC-SHA256 of <timestamp>.<raw body>, keyed with your secret. Recompute it on your side and compare:
const crypto = require("crypto");
const SECRET = process.env.PLAYCORE_WEBHOOK_SECRET;
// Verify against the RAW request body (string/Buffer) — not the parsed JSON.
function isFromPlayCore(rawBody, headers) {
const ts = headers["x-playcore-timestamp"];
const got = headers["x-playcore-signature"] || "";
const mine = "sha256=" + crypto
.createHmac("sha256", SECRET)
.update(ts + "." + rawBody)
.digest("hex");
return got.length === mine.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(mine));
}
timingSafeEqual) and reject anything that fails.Errors #
Every error returns the same envelope with a stable numeric code. Build your integration against the code, not the message text.
{
"status": "error",
"code": 100,
"message": "Insufficient balance"
}
Authentication codes
| Code | Meaning |
|---|---|
| 120 | Api Token is required |
| 121 | Token error (invalid token) |
| 122 | Not allowed to use the API (disabled / inactive account) |
| 123 | IP not allowed |
Order codes
| Code | Meaning |
|---|---|
| 100 | Insufficient balance |
| 106 | Quantity not allowed |
| 107 | Missing required field |
| 108 | Invalid field value / option |
| 109 | Product deleted or not found |
| 110 | Product not available now |
| 111 | Too many orders for the same ID within a minute |
| 112 | Quantity is too small |
| 113 | Quantity is too large |
| 114 | Bad request / unknown order error |
| 115 | Site under maintenance |
| 116 | Purchase not allowed for this user |
| 500 | Unknown internal error |
Player ID verification codes
| Code | Meaning |
|---|---|
| 130 | No active subscription — subscribe from the API page in your account (HTTP 403) |
| 131 | This product does not support ID verification |
| 132 | Product id missing or not found |
| 133 | Player id is required |
| 134 | The player id is not valid — check the value, and the server/charname if the game needs one |
| 135 | Too many lookups — slow down and retry (HTTP 429) |
| 136 | Service busy — retry shortly |
| 137 | Service unavailable (not enabled) |