API documentation
REST + JSON. Create an invoice, send the customer to the payment page, receive the result via a signed webhook.
Getting started
The API key and secret are created in the merchant dashboard under API; the secret is shown only once. The webhook URL and webhook secret are managed on the same page. To open a merchant account, message @mistnetwork on Telegram.
Base URL: https://mistket.com/api/v1
Authentication
Send three headers with every request:
| Header | Value |
|---|---|
| X-Api-Key | Key from the dashboard (mp_live_…) |
| X-Api-Timestamp | Unix timestamp (seconds); may differ from server time by at most ±300 s |
| X-Api-Signature | hex( HMAC-SHA256( "{timestamp}.{raw body}", API_SECRET ) ) |
The signed string is the timestamp + a dot + the raw request body (send the JSON exactly as you signed it). For requests without a body (GET and body-less POST) the body is the empty string ""; query parameters are not part of the signature.
Replay protection: for state-changing requests (POST etc.) the same signature is not accepted twice within ±5 minutes. Take a fresh timestamp and recompute the signature for every request.
Signature version 2 (recommended): send X-Api-Signature-Version: 2 and a fresh X-Api-Nonce (8–64 chars [A-Za-z0-9_-]) on every request; the signature is hex( HMAC-SHA256( "{timestamp}.{nonce}.{METHOD}.{path?query}.{raw body}", API_SECRET ) ). A nonce cannot be reused within ±5 minutes. Version 1 keeps working.
If you set an IP allowlist in the dashboard, only requests from those addresses are accepted.
Example client
function mistket(string $method, string $path, array $body = []): array
{
$key = getenv('MISTKET_KEY');
$secret = getenv('MISTKET_SECRET');
$payload = $body ? json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : '';
$timestamp = (string) time();
$signature = hash_hmac('sha256', $timestamp.'.'.$payload, $secret);
$ch = curl_init('https://mistket.com/api/v1'.$path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $payload !== '' ? $payload : null,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Api-Key: '.$key,
'X-Api-Timestamp: '.$timestamp,
'X-Api-Signature: '.$signature,
],
]);
return json_decode(curl_exec($ch), true);
}
$invoice = mistket('POST', '/invoices', [
'amount' => '49.90',
'currency' => 'USD',
'order_id' => 'ORDER-1001',
'callback_url' => 'https://example.com/webhook/mistket',
'success_url' => 'https://example.com/thanks',
]);
header('Location: '.$invoice['data']['checkout_url']);
import crypto from 'node:crypto';
async function mistket(method, path, body) {
const payload = body ? JSON.stringify(body) : '';
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha256', process.env.MISTKET_SECRET)
.update(`${ts}.${payload}`).digest('hex');
const res = await fetch(`https://mistket.com/api/v1${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': process.env.MISTKET_KEY,
'X-Api-Timestamp': ts,
'X-Api-Signature': sig,
},
body: payload || undefined,
});
return res.json();
}
const inv = await mistket('POST', '/invoices', { amount: '49.90', currency: 'USD', order_id: 'ORDER-1001' });
console.log(inv.data.checkout_url);
import hashlib, hmac, json, os, time, urllib.request
def mistket(method, path, body=None):
payload = json.dumps(body, separators=(",", ":")) if body else ""
ts = str(int(time.time()))
sig = hmac.new(os.environ["MISTKET_SECRET"].encode(),
f"{ts}.{payload}".encode(), hashlib.sha256).hexdigest()
req = urllib.request.Request(
"https://mistket.com/api/v1" + path,
data=payload.encode() if payload else None,
method=method,
headers={
"Content-Type": "application/json",
"X-Api-Key": os.environ["MISTKET_KEY"],
"X-Api-Timestamp": ts,
"X-Api-Signature": sig,
},
)
with urllib.request.urlopen(req) as r:
return json.load(r)
inv = mistket("POST", "/invoices", {"amount": "49.90", "currency": "USD", "order_id": "ORDER-1001"})
print(inv["data"]["checkout_url"])
Endpoints
| POST /invoices | Create an invoice |
| GET /invoices/{uuid} | Get invoice status |
| GET /invoices | List invoices |
| POST /invoices/{uuid}/cancel | Cancel an open invoice |
| GET /assets | Supported coins / networks |
| GET /rate | Rate and amount to pay |
| GET /health | Service health (no authentication) |
POST/invoices — Create an invoice
| Field | Type | Required | Description |
|---|---|---|---|
| amount | string | ✓ | Amount, e.g. "49.90" (greater than zero) |
| currency | string | ✓ | Price currency: USD, EUR, TRY, RUB, GBP, AED or a crypto symbol directly (e.g. USDT; 1:1 when it matches the paid asset) |
| order_id | string | Your own order number. Idempotency key: calling again with the same order_id does not create a new invoice; the existing one is returned with "idempotent": true. | |
| pay_currency network | string | To fix the network up front, e.g. USDT + TRC20. If omitted the customer picks the network on the payment page (recommended). | |
| asset_id | int | Alternative: the id from /assets | |
| callback_url | url | Webhook URL for this invoice (defaults to the store's webhook URL) | |
| success_url cancel_url | url | Where to send the customer after paying / cancelling | |
| ttl_minutes | int | 5–1440 minutes; defaults to the store setting (60 by default) | |
| description customer_email meta | Free-form fields (description ≤191 chars, meta object) |
Success returns 201; redirect the customer to data.checkout_url.
{
"success": true,
"data": {
"uuid": "9b1c…",
"order_id": "ORDER-1001",
"status": "new",
"price_amount": "49.90",
"price_currency": "USD",
"pay_amount": null,
"pay_currency": null,
"network": null,
"pay_address": null,
"checkout_url": "https://mistket.com/pay/9b1c…",
"expires_at": "2026-09-12T22:51:00+00:00",
"seconds_remaining": 3600
}
}
GET/invoices/{uuid} — Get invoice status
{
"success": true,
"data": {
"uuid": "9b1c…",
"status": "confirmed",
"pay_amount": "49.912345",
"paid_amount": "49.912345",
"remaining_amount": "0",
"pay_currency": "USDT",
"network": "TRC20",
"chain": "TRON",
"pay_address": "T…",
"confirmations": 21,
"required_confirmations": 19,
"is_underpaid": false,
"can_pay_remaining": false,
"under_review": false,
"checkout_url": "https://mistket.com/pay/9b1c…",
"transactions": [
{
"tx_hash": "f3a…",
"amount": "49.912345",
"confirmations": 21,
"status": "confirmed",
"explorer_url": "https://tronscan.org/#/transaction/f3a…"
}
]
}
}
GET/invoices — List invoices
Query parameters: status, order_id, per_page (max 100), page. The response includes meta.current_page, meta.last_page, meta.total.
POST/invoices/{uuid}/cancel — Cancel an open invoice
Closes an open invoice and returns its payment address to the pool; an invoice.cancelled webhook is sent.
GET/assets — Supported coins / networks
Each item: id, symbol, name, network, chain, contract, decimals, min_amount, confirmations, available. available: false → no free address on that network right now.
GET/rate?asset_id=1¤cy=USD&amount=100
Shows the rate and crypto amount without creating an invoice. Parameters: asset_id (required), currency (required), amount.
GET/health — Service health (no authentication)
Invoice object
All invoice responses and the webhook data share the same shape. Key fields:
| status status_label | Status (see table below) and its label in the customer's language |
| *_amount | pay_amount: crypto amount to pay · paid_amount / received_amount: total received at the address · remaining_amount: what is left |
| pay_currency network chain pay_address | Payment asset, network, chain code and address (null until a network is chosen) |
| confirmations required_confirmations | Current / required block confirmations |
| is_underpaid … | is_underpaid, can_pay_remaining (customer may send the rest to the same address), under_review (expired with a partial payment; manual review), expiry_extensions |
| transactions[] | Transactions seen on chain: tx_hash, amount, confirmations, status, explorer_url, detected_at |
Invoice statuses
| new | Created; the customer has not chosen a network yet |
| pending | Network chosen, address and amount locked; waiting for payment |
| underpaid | Less than expected arrived; the rest can be sent to the same address (expiry extends automatically while open) |
| paid | Amount seen, waiting for confirmations — do not fulfil the order yet |
| confirmed | Paid and confirmed |
| overpaid | Paid more than expected and confirmed |
| expired | Expired without a completed payment |
| cancelled | Cancelled |
Webhooks
Every time an invoice status changes, a JSON POST is sent to the invoice's callback_url (or the store's webhook URL).
Headers
Content-Type: application/json
User-Agent: mistket-webhook/1.0
X-Mistket-Event: invoice.confirmed
X-Mistket-Timestamp: 1789456123
X-Mistket-Signature: sha256=<hex>
X-Mistket-Delivery: 4412
Body
{
"event": "invoice.confirmed",
"sent_at": "2026-09-12T21:55:03+00:00",
"data": { "uuid": "9b1c…", "order_id": "ORDER-1001", "status": "confirmed", "…": "…" }
}
Events
invoice.pending, invoice.underpaid, invoice.paid, invoice.confirmed, invoice.overpaid, invoice.expired, invoice.cancelled
Verifying the signature
The signature is HMAC-SHA256( "{X-Mistket-Timestamp}.{raw body}" ) with your webhook secret, sent with a sha256= prefix. Verify the raw body before decoding JSON, compare in constant time and reject stale timestamps.
$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_MISTKET_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_MISTKET_SIGNATURE'] ?? '';
$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$payload, WEBHOOK_SECRET);
if (! hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit('invalid signature');
}
$event = json_decode($payload, true);
if (in_array($event['event'], ['invoice.confirmed', 'invoice.overpaid'], true)) {
// fulfil the order: check data.order_id and data.paid_amount
}
http_response_code(200);
echo 'ok';
Fulfil the order only on invoice.confirmed or invoice.overpaid; invoice.paid means “transaction seen, waiting for confirmations”. The same event may arrive more than once; make your handler idempotent (use X-Mistket-Delivery and uuid).
Retries
Non-2xx responses (or a 15 s timeout) are retried with increasing delays: 10 s · 30 s · 2 min · 10 min · 30 min · 1 h · 2 h · 3 h — after that the delivery is marked failed. The delivery history of each invoice is visible in the dashboard.
Errors and limits
Errors return success: false with an error message. Validation errors also include per-field details in errors.
{ "success": false, "error": "…", "errors": null }
| Code | Meaning |
|---|---|
| 401 | Missing/invalid key, bad signature or timestamp outside the window |
| 403 | Request IP is not allowed |
| 404 | Invoice not found (another store's invoice also returns 404) |
| 422 | Validation error, unsupported coin/network or no free address |
| 429 | Rate limit: 120 requests / 1 min per API key |
| 503 | Rate provider unreachable; retry in a few seconds |
Ready to integrate?
Message us on Telegram to open a merchant account or ask questions.
Message on Telegram Merchant login