Sell top-up cards and game recharges from your own system. This page covers authentication, token scopes, IP restrictions, signed order webhooks, errors and rate limits. Base URL https://kaziyh.online.
API access is granted per account and is not on by default. Two things must be true: your account is on the api tier, and an administrator has enabled API access for it. Until both hold, every token request returns 403.
Once enabled, you create and revoke your own keys from the developer page in the reseller dashboard. We never see your key after it is generated, and it cannot be retrieved later — only replaced.
A bearer token on every request. Tokens are prefixed kzh_ so they are recognisable in logs and secret scanners.
curl "https://kaziyh.online/api/agent/catalog?page=1" \ -H "Authorization: Bearer kzh_..." \ -H "Accept: application/json"
You may hold up to 10 tokens. Use a separate one per system, so revoking a leaked key does not take down everything else you run. Tokens are created and revoked from the dashboard only — never over the API, so a leaked token cannot mint another or widen its own permissions.
Every token carries an explicit list of scopes and can do nothing outside them. Grant the least your integration needs: a service that only reads the catalogue should not be able to spend your wallet.
| Scope | Grants |
|---|---|
catalog:read | Browse the catalogue, read a product, request a price quote. |
orders:read | List your orders and read a single order's status. |
orders:write | Create an order.Spends your wallet balance. |
codes:read | Reveal the delivered code for an order.The code is the goods — grant this only to something that must sell it. |
wallet:read | Read your wallet balance, deposits and payment methods. |
storefront:read | Read your storefront config, listed products and analytics. |
A request outside a token's scopes returns 403 naming the scope it needed, so you can fix the call without guessing.
Tokens created before scopes existed carry every permission and keep working. They are marked legacy in the dashboard; replacing one with a narrowly scoped token is worth doing when convenient.
A token can be given a lifetime of 30, 90 or 365 days, or none. The default is none, so nothing expires unless you choose it.
An expired token returns 401 and is shown as expired in the dashboard, so a failing integration is diagnosable rather than mysterious. Expiry cannot be extended — create a replacement and revoke the old one.
A token can be restricted to the addresses it may be used from — up to 20 entries, as exact IPv4/IPv6 addresses or CIDR ranges. A token with no list works from anywhere, which is the default.
203.0.113.7 # one server 203.0.113.0/24 # a range, for a dynamic address 2001:db8::/32 # IPv6 works too
Set your server's address, not the machine you are browsing from — those are usually different. The dashboard shows your current address as a convenience for when they are the same.
A refused call returns 403 naming the address that was rejected, which is the address you need to add.
Register one https endpoint and we will call it when an order reaches a terminal state — order.success or order.failed. Intermediate states are not sent.
Worth knowing before you build one: most orders fulfil within seconds and the result already comes back in the response to your create call. Webhooks earn their keep on orders that go through manual review, where the alternative is polling on a timer.
POST https://your-system.example/kaziyh-hook
X-Kaziyh-Event: order.success
X-Kaziyh-Timestamp: 1785408225
X-Kaziyh-Signature: sha256=9f2b…
{
"event": "order.success",
"order": {
"id": 987,
"order_number": "KZ-…",
"status": "success",
"previous_status": "processing",
"quantity": 1,
"customer_identifier": "P12345",
"created_at": "2026-07-30T10:15:00+00:00"
},
"occurred_at": "2026-07-30T10:15:04+00:00"
}Answer with any 2xx. A non-2xx or a timeout is retried up to four times with a widening gap (10s, 60s, 5m). Every attempt, including the response status and any error, is logged in your dashboard — so a missed callback is something you can diagnose yourself.
The URL must be https and must resolve to a public address. Private, loopback and link-local addresses are rejected, and this is re-checked immediately before each send rather than only when you save it. Redirects are not followed, so point us at the final URL.
Always verify. An unverified endpoint will accept a forged call from anyone who learns its URL, and this one tells your system that money moved.
The signature is HMAC-SHA256 over {timestamp}.{raw body} using your webhook secret. Sign the raw body — re-serialising parsed JSON changes the bytes and the signature will not match.
// Node / Express — note express.raw(), not express.json()
app.post("/kaziyh-hook", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("X-Kaziyh-Timestamp");
const sig = req.header("X-Kaziyh-Signature");
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.KAZIYH_WEBHOOK_SECRET)
.update(ts + "." + req.body) // req.body is a Buffer here
.digest("hex");
// timingSafeEqual, never === : a plain compare leaks the signature byte by byte
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.sendStatus(400);
// Reject anything older than ~5 minutes, or a captured call can be replayed
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
res.sendStatus(200); // acknowledge fast, then process out of band
});# PHP
$expected = 'sha256=' . hash_hmac(
'sha256',
$timestamp . '.' . $rawBody,
getenv('KAZIYH_WEBHOOK_SECRET')
);
if (! hash_equals($expected, $signature)) { http_response_code(400); exit; }# Python
expected = "sha256=" + hmac.new(
os.environ["KAZIYH_WEBHOOK_SECRET"].encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature):
return Response(status=400)Your secret is shown in the dashboard and starts whsec_. Changing the URL does not rotate it, so fixing a typo will not break your verifier. Rotate it deliberately if you believe it has leaked.
Errors are JSON with a message. Validation failures add errors, keyed by field. Send Accept: application/json or you may get an HTML error page.
401No token, an unknown token, or an expired one.{ "message": "Unauthenticated." }403Authenticated, but not allowed. Three distinct causes, and the message says which.{ "message": "API access is not enabled for this account." }
{ "message": "This API token is missing the required scope: wallet:read." }
{ "message": "This API token is not allowed from 198.51.100.9." }404The record does not exist, or is not yours. We do not distinguish the two.{ "message": "Not found." }409The product cannot be supplied right now (restocking).{ "message": "…", "code": "restocking" }422Validation failed. `errors` is keyed by field.{ "message": "The product id field is required.",
"errors": { "product_id": ["The product id field is required."] } }429Rate limited. Back off and retry — see Rate limits.{ "message": "Too Many Requests" }Limits are counted per token, not per account — so one busy integration cannot exhaust the budget of another, or of the dashboard.
| Limit | API token | Dashboard | Applies to |
|---|---|---|---|
| General | 300 / min | 120 / min | Every /api/agent endpoint. |
| Order creation | 30 / min | 60 / min | POST /api/agent/orders |
| Code reveal | 20 / min | 20 / min | GET /api/agent/orders/{id}/code |
Exceeding a limit returns 429 with a Retry-After header. Wait that long rather than retrying immediately — a tight retry loop just keeps you limited.
Code reveals are capped hardest on purpose. Each one decrypts a bearer instrument, so the low ceiling bounds how much a leaked token could extract before you revoke it.
order.success and order.failed, with a delivery log.Not today. There is no separate test environment, so an integration is exercised against real products and a real wallet. The practical approach is to test with the smallest denomination of a cheap product, which keeps the cost of a full end-to-end run trivial. We would rather say this plainly than let you discover it after building.
No. The reseller dashboard places orders, manages the wallet and shows the full history without a line of code, and you can run a branded storefront from it. The API is for connecting a system you already operate.
Send an Idempotency-Key header and a repeat of the same request returns the original order instead of creating a second or charging twice. This is the protection that matters when your network drops mid-request, and it is scoped to your account.
Open a reseller account, then ask us to enable API access. Your keys, webhook secret and delivery log all live in the dashboard.
Apply for a reseller account