Reference · REST + webhook
Payment API Reference
Create UZS invoices, ingest bank and Telegram events, settle pending invoices, expire stale ones,
and deliver signed merchant callbacks. Everything a merchant integration needs is on this page.
Amount range
1,000–1e9
Body limit
64 KiB
Event window
−10m / +30s
Worker batch
20 events
Invoice lifetime
5 min
Poll interval
5 s
Callback attempts
10
Overload
503 + retry
02
Authentication
Use separate credentials per surface. Send them only in headers, only over HTTPS, and never in query
strings, logs, or browser bundles.
Merchant API Authorization: Bearer API_KEY
Used by /api/create/ and /api/getter/. One key per merchant; rotate by disabling the old row after clients switch.
Payment webhook X-Secret-Key: PAYMENT_WEBHOOK_SECRET
Used by /api/status/ for trusted payment events.
Cron trigger X-Cron-Key: CRON_SECRET
Used by /api/cron/ to expire invoices and run maintenance.
Telegram webhook X-Telegram-Bot-Api-Secret-Token: TELEGRAM_WEBHOOK_SECRET
Used by /telegram.php for Telegram Business updates.
03
Endpoints
All examples use https://payments.example.com. Browser cross-origin access is not enabled.
5 endpoints documented
Creates a pending invoice for the authenticated merchant.
Request body
Field Type Required Notes
amount
integer or digit string
yes
Whole UZS. Range 1,000 to 1,000,000,000. No floats or scientific notation.
Response 200
Copy
the Response 200 example
{
"status": "success",
"message": "invoice_created",
"data": {
"order_id": "pay_3f9a1c0e7b2d4a58b6c1e0d94f7a2b13",
"amount": 50137,
"currency": "UZS",
"card_number": "8600000000001234",
"card_holder": "CARD HOLDER",
"status": "pending",
"created_at": "2026-10-25 09:00:00",
"end_time": "2026-10-25 09:05:00",
"wait_time": 5
}
}
Ask the customer to pay the returned amount, not the requested one: up to 999 UZS are added so concurrent invoices stay distinguishable. Times are UTC. A payment is matched by amount and card suffix, so a different amount will not settle the invoice.
cURL
JavaScript
Python
PHP
Copy
the selected example
cURL
curl --fail-with-body --tlsv1.2 \
-H 'Authorization: Bearer API_KEY' \
-H 'Content-Type: application/json' \
--data '{"amount":50000}' \
https://payments.example.com/api/create/
JavaScript
const response = await fetch("https://payments.example.com/api/create/", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYMENT_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ amount: 50000 })
});
if (!response.ok) throw new Error(`Payment API HTTP ${response.status}`);
const result = await response.json();
if (result.status !== "success") throw new Error(result.message);
console.log(result.data.order_id, result.data.amount);
Python
import os, requests
response = requests.post(
"https://payments.example.com/api/create/",
headers={"Authorization": f"Bearer {os.environ['PAYMENT_API_KEY']}"},
json={"amount": 50000},
timeout=(3, 10),
)
response.raise_for_status()
result = response.json()
if result.get("status") != "success":
raise RuntimeError(result.get("message", "payment API error"))
print(result["data"]["order_id"])
PHP
$curl = curl_init("https://payments.example.com/api/create/");
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(["amount" => 50000], JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("PAYMENT_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($body === false || $status < 200 || $status >= 300) {
throw new RuntimeException("Payment API request failed");
}
$result = json_decode($body, true, 32, JSON_THROW_ON_ERROR);
GET
/api/getter/?order_id=pay_ORDER_ID
#
Returns one invoice owned by the authenticated merchant. Use it to poll for payment status.
Query parameters
Field Type Required Notes
order_id
string
yes
Query parameter returned by invoice creation. Format pay_ plus 8–64 letters or digits.
Response 200
Copy
the Response 200 example
{
"status": "success",
"message": "order_found",
"data": {
"order_id": "pay_3f9a1c0e7b2d4a58b6c1e0d94f7a2b13",
"amount": 50137,
"card_number": "8600000000001234",
"status": "success",
"created_at": "2026-10-25 09:00:00",
"end_time": "2026-10-25 09:05:00",
"currency": "UZS"
}
}
Invoice status
Status Meaning Client action
pending
Waiting for payment until end_time.
Keep polling every wait_time seconds.
success
Paid. A paid invoice never becomes expired.
Stop polling and fulfil the order.
expired
end_time passed without a matching payment.
Stop polling. Create a new invoice if the customer still wants to pay.
Polling rules. Poll from your server, never a browser, since it needs the API key. Poll every 5 seconds, about 60 requests per invoice at most. On 429 or 503 wait for Retry-After and continue; do not poll faster to catch up. Prefer signed callbacks and keep polling as the fallback. Fetch the final state once more after end_time.
cURL
Copy
the cURL example
curl --fail-with-body --tlsv1.2 \
-H 'Authorization: Bearer API_KEY' \
'https://payments.example.com/api/getter/?order_id=pay_ORDER_ID'
Records a trusted payment event and settles a matching pending invoice.
Send with X-Secret-Key: PAYMENT_WEBHOOK_SECRET and Content-Type: application/json.
Request body
Field Type Required Notes
amountinteger or digit string yes Whole UZS amount.
card_last_4string yes Four digit card suffix.
event_idstring yes Stable source transaction ID, unchanged across retries.
occurred_atUnix seconds yes Within ten minutes past and thirty seconds future.
JSON body
Copy
the JSON body
{
"amount": 50000,
"card_last_4": "1234",
"event_id": "bank:transaction-123456",
"occurred_at": 1792100000
}
Response 200
Copy
the Response 200 example
{
"status": "success",
"message": "event_recorded",
"data": {
"status": "success",
"order_id": "pay_3f9a1c0e7b2d4a58b6c1e0d94f7a2b13",
"duplicate": false
}
}
Matching requires a pending, unexpired invoice that existed when the event occurred. A duplicate
returns the recorded outcome with duplicate: true.
Expires old pending invoices and performs service maintenance. No request body is required.
Schedule with X-Cron-Key: CRON_SECRET, at least once a minute.
cURL
Copy
the cURL example
curl --fail-with-body --tlsv1.2 \
-H 'X-Cron-Key: CRON_SECRET' \
-X POST \
https://payments.example.com/api/cron/
Receives and validates Telegram Business updates.
Register the webhook with Telegram's secret_token set to
TELEGRAM_WEBHOOK_SECRET. Empty allowlists ignore every notification.
cURL
Copy
the cURL example
curl --fail-with-body --tlsv1.2 \
-H 'X-Telegram-Bot-Api-Secret-Token: TELEGRAM_WEBHOOK_SECRET' \
-H 'Content-Type: application/json' \
--data @update.json \
https://payments.example.com/telegram.php
No endpoints match “ ”
Filtering covers endpoint paths, request fields, headers, and status codes. Clear the
filter to see all five endpoints again.
Clear search
04
Merchant callbacks
Callback URLs must use operator-allowlisted HTTPS hostnames on port 443 with publicly routable IPv4
addresses. The checked DNS result is pinned for the connection; redirects, proxies, and private or
reserved addresses are rejected.
Delivery headers
Header Meaning
X-Callback-Timestamp
Unix seconds at delivery.
X-Callback-Signature
sha256= plus HMAC-SHA256 of timestamp + "." + raw_request_body.
Delivery is at least once, including after a worker crash. Receivers must be idempotent and return 2xx
only after durably recording the event.
05
Callback verification
Verify the raw request body before parsing JSON, compare with a constant-time function, and reject
timestamps outside a short tolerance.
Python
Copy
the verification example
import hmac, hashlib, time
def verify_callback(raw_body, timestamp, signature, secret):
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(secret, (timestamp + ".").encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + expected, signature)
Deduplicate by event_id or order_id before applying business effects. A repeated
delivery that passes signature checks is still a repeated delivery.
06
Status codes and duplicate behavior
200
success / not_found Both matched and unmatched payment events return HTTP 200 when accepted. Inspect data.status.
409
conflict Reusing an event ID with a different amount, card suffix, or timestamp.
400
validation Invalid JSON, missing fields, bad timestamp window, or unsupported amount.
401
auth Missing or invalid credential header.
413
large body JSON request body exceeds 64 KiB.
415
media type Unsupported content type.
404
not found Unknown endpoint, or order_not_found when the order does not exist or belongs to another merchant.
405
method Wrong HTTP method. The Allow header names the accepted one.
429
rate limited Too many requests from your IP. Back off, then continue.
503
high_load / no_active_cards Temporary overload or no card available. Retry after Retry-After seconds; reuse the same event_id for payment events.
500
server error internal_server_error carries no details. Retry with backoff and contact the operator if it persists.
A duplicate event returns the recorded outcome with duplicate: true.
A duplicate never searches for another invoice.
Errors always look like {"status":"error","message":"…"}; branch on the HTTP status and message, never on free text.
Never trust the top-level HTTP success alone as proof of settlement; inspect data.status.
07
Security requirements
01
Transport HTTPS only. No credentials in query strings, logs, or browser bundles.
02
Amounts Whole UZS integers only. Range and uniqueness constraints are enforced in the database.
03
Idempotency Stable event IDs, conflicts on mismatched reuse, duplicates never re-match an invoice.
04
Callbacks Exact host allowlisting, DNS pinning, no redirects, no proxies, TLS verification.
05
Ingestion Telegram allowlists for senders and business connections. Forwards and edits are rejected.
06
Secrets Loaded from the environment or .env. Never printed, logged, or returned.
08
Client examples
cURL
Shell
Copy
the cURL example
curl --fail-with-body --tlsv1.2 \
-H 'Authorization: Bearer API_KEY' \
-H 'Content-Type: application/json' \
--data '{"amount":50000}' \
https://payments.example.com/api/create/
JavaScript (Node.js)
Node.js 18+
Copy
the Node.js example
const response = await fetch("https://payments.example.com/api/create/", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYMENT_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ amount: 50000 })
});
if (!response.ok) throw new Error(`Payment API HTTP ${response.status}`);
const result = await response.json();
if (result.status !== "success") throw new Error(result.message);
console.log(result.data.order_id, result.data.amount);
Python
Python 3
Copy
the Python example
import os, requests
response = requests.post(
"https://payments.example.com/api/create/",
headers={"Authorization": f"Bearer {os.environ['PAYMENT_API_KEY']}"},
json={"amount": 50000},
timeout=(3, 10),
)
response.raise_for_status()
result = response.json()
if result.get("status") != "success":
raise RuntimeError(result.get("message", "payment API error"))
print(result["data"]["order_id"])
PHP
PHP 8.2+
Copy
the PHP example
$curl = curl_init("https://payments.example.com/api/create/");
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(["amount" => 50000], JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("PAYMENT_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($body === false || $status < 200 || $status >= 300) {
throw new RuntimeException("Payment API request failed");
}