Integration documentation for partners.
https://api.jetbot.pwhttps://dev.jetbot.pw (for testing)
To work with the API, follow these steps:
Important: Your merchant API balance is directly linked to your Telegram account balance in the bot. To top up your balance, you need to perform a standard deposit operation via the Telegram bot: @jetpaycryptobot.
Important: Sandbox uses separate credentials, tokens, and URLs. Request access to the Sandbox separately for safe integration testing.
All API requests must contain the Authorization HTTP header:
Authorization: Bearer YOUR_TOKEN
Methods that change state (e.g., creating a payout) require a mandatory signature parameter to protect against request forgery.
signature field from this dictionary if present.METHOD_PATH?key1=value1&key2=value2.
/api/payout?amount=1000&order_id=123&recipient=4444555566667777
signature parameter in the request body.from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
import base64
def sign_request(method_path, data, private_key_path):
# 1. Sorting and assembling the string
sorted_keys = sorted([k for k in data.keys() if k != 'signature'])
params = [f"{k}={data[k]}" for k in sorted_keys]
data_string = f"{method_path}?{'&'.join(params)}"
# 2. Loading the key
with open(private_key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(), password=None
)
# 3. Signing
signature = private_key.sign(
data_string.encode(),
padding.PKCS1v15(),
hashes.SHA512()
)
return base64.b64encode(signature).decode()
Get the current balance of your merchant account in USDT.
{
"balance": 5400.50,
"currency": "USDT"
}
Get the current USDT/RUB exchange rate for your merchant account. No signature required.
{
"rate": "71.82"
}
The rate field is a string with two decimal places (dot as separator), rubles per 1 USDT.
Create a new payout request to a bank card (in RUB).
| Parameter | Type | Required | Description |
|---|---|---|---|
amount |
Float/Int | Yes | Payout amount in Rubles (RUB). Currently: 110 to 400,000 RUB. |
recipient |
String | Yes | Recipient's bank card number (digits only, no spaces, 16 digits). |
order_id |
String | Yes | Unique order ID in your system. A repeated POST with the same order_id for this merchant returns 400 Duplicate order_id and does not create a second payout. Also used in GET status. |
phone |
String | No | Recipient's phone number in format 79xxxxxxxxx (11 digits). If not provided, a placeholder is used. |
name |
String | No | Recipient's name (Latin or Cyrillic). Useful for some payment gateways. |
signature |
String | Yes | RSA-SHA512 signature of the request (see "Signature Generation" section). |
{
"status": "pending",
"id": "provider_abc123", // Transaction ID in the processing gateway
"order_id": "my_order_001",
"payout_id": 42, // Internal payout ID in JetBot
"debited_amount_usdt": 20.88554616, // Amount debited from the USDT balance
"rate_used": 71.82 // Applied RUB/USDT rate
}
debited_amount_usdt is the exact USDT amount debited when the payout is created. Formula: amount / rate_used. These fields are also returned by the payout status endpoint.
Create a new payout via SBP (Faster Payments System / Система быстрых платежей) — by recipient phone number and bank, without a card number. This is a separate endpoint; /api/payout and its behavior remain unchanged.
SBP payout specifics:
created.Signature: generated the same way as for /api/payout, but the method path is /api/payout/sbp (example string: /api/payout/sbp?amount=5000&bank=Sberbank&name=Ivan Ivanov&order_id=123&phone=79001234567).
| Parameter | Type | Required | Description |
|---|---|---|---|
amount |
Float/Int | Yes | Payout amount in rubles (RUB). Currently: 110 to 20,000 RUB. |
bank |
String | Yes | Recipient's bank name (e.g., "Sberbank", "Tinkoff"). 2–50 characters. |
name |
String | Yes | Recipient name (2–100 characters after trim). - or empty string → Invalid recipient name. |
phone |
String | Yes | Recipient's phone number (Russian mobile, format 79xxxxxxxxx; input starting with 8 or +7 is normalized automatically). |
order_id |
String | Yes | Unique order identifier. Repeating the same value → 400 Duplicate order_id. |
signature |
String | Yes | RSA-SHA512 signature of the request (method path — /api/payout/sbp). |
{
"status": "created", // SBP payout accepted for processing
"id": "19E3BABE0796332", // Public ID (used for status checks and receipts)
"order_id": "my_order_001",
"payout_id": 42, // Internal payout ID in JetBot
"debited_amount_usdt": 69.61849067,
"rate_used": 71.82
}
Get the current status of a payout. You can use the following as <ID>:
payout_id - internal JetBot ID (returned upon creation).order_id - your unique order ID (merchant_order_id).id - gateway ID.POST /api/payout or POST /api/payout/sbp returned HTTP 400 / 401 / 402 / 403, the payout was not created (exception: SBP daily phone request limit exceeded — see the table). A follow-up GET /api/payout/<order_id> will be 404 Payout not found. That is not “pending” and not a hung payout — do not poll 404. Fix the request body and create again with a new order_id (or the same one if nothing was created).
{
"id": 42,
"order_id": "my_order_001",
"amount": 1500.00,
"status": "confirmed", // Possible statuses: confirmed, pending, failed, cancelled
"jetbot_status": "success", // Detailed gateway status: success, process, error, etc.
"payout_type": "card", // Payout type: "card" or "sbp"
"recipient": "4276....1234", // For SBP — 3-line details: name, bank, phone
"created_at": "2023-10-27 14:30:00",
"debited_amount_usdt": 20.88554616, // Amount debited from the USDT balance
"rate_used": 71.82 // Applied RUB/USDT rate
}
Download a PDF receipt for a successful payout. Use the receipt ID (id field returned when creating a payout) as <public_id>. Internal payout number (payout_id), order_id, and other identifiers cannot be used to download receipts. No signature required.
The response is a PDF file (Content-Type: application/pdf). The time on the receipt is in Moscow time (MSK, UTC+3).
curl -H "Authorization: Bearer <TOKEN>" \
-o receipt.pdf \
https://api.jetbot.pw/api/payout/358472DC4B20E6A/receipt
409 + Receipt not ready, payout is still processing — not successful yet (still processing).409 + Receipt is only available for successful payouts — payout failed / cancelled.404 + Payout not found — no payout with that public id.403 + Access denied — payout belongs to another merchant.500 + Failed to generate receipt — PDF generation failed.JSON error body (all methods except a successful PDF receipt):
{
"error": "exact string from the tables below",
"code": 400
}
The HTTP status matches code. Create errors may also include order_id. Successful HTTP 200 responses do not include code.
The error strings below are the exact API texts. Match them by full string equality.
| HTTP | When | Retry? |
|---|---|---|
| 400 | Body validation, duplicate order_id, SBP daily phone request limit | No until you fix the payload. Duplicate — do not create again; use payout_id/id from the body |
| 401 | Missing/invalid Bearer token | No until you fix the header |
| 402 | Not enough USDT on the merchant balance | After topping up, with a new order_id if nothing was created |
| 403 | IP not whitelisted, invalid RSA signature, SBP disabled, another merchant’s payout | No until you fix access/signature |
| 404 | Payout not found (including after a failed POST — nothing was created) | Do not poll “until it appears”. Recreate only if POST was not 200 |
| 409 | Receipt: payout is not successful | Receipt only after successful |
| 500 | Internal error on create / receipt | Cautious retry; GET by order_id first to see if a payout exists |
| 503 | Nginx: more than 10 req/s per IP (burst 20) | Yes, exponential backoff |
Checked before parsing the body. Signed bodies are not processed on 401/403.
| HTTP | error | When |
|---|---|---|
| 401 | Missing or invalid Authorization header | No header or not Bearer <token> |
| 401 | Invalid token | Unknown / disabled token |
| 403 | Access denied | Client IP not on the merchant whitelist (same text for GET of another merchant’s payout) |
POST /api/payout (card)Required before business logic: amount, recipient, order_id, signature.
| HTTP | error | When |
|---|---|---|
| 400 | No data provided | Empty body (no JSON and no form) |
| 400 | Missing field: amount / recipient / order_id / signature | Required field missing. Empty order_id is also Missing field: order_id |
| 400 | Duplicate order_id | This merchant already has a payout with that order_id. Extra fields: order_id, payout_id, id (public ID), status. No new payout is created — reuse those IDs |
| 400 | Invalid amount | amount is not a number |
| 400 | Amount must be at least 110 RUB. Provided: … | Amount < 110 |
| 400 | Amount must not exceed 400000 RUB. Provided: … | Amount > 400000 |
| 400 | Card number must contain exactly 16 digits. Provided: N digits | Not 16 digits after stripping non-digits |
| 400 | Invalid card number (failed Luhn check) | 16 digits, Luhn fails |
| 402 | Insufficient balance | Not enough USDT. Extra: required, available. Payout not created |
| 403 | Invalid signature | RSA-SHA512 mismatch (path /api/payout) |
| 500 | Internal server error | Unhandled exception during create |
POST /api/payout/sbpRequired: amount, bank, name, phone, order_id, signature.
| HTTP | error | When |
|---|---|---|
| 400 | No data provided | Empty body |
| 400 | Missing field: … | Required field missing (including empty order_id) |
| 400 | Duplicate order_id | Same as card. Response includes payout_id, id, status |
| 400 | Invalid amount | amount is not a number |
| 400 | Amount must be at least 110 RUB. Provided: … | Amount < 110 |
| 400 | SBP amount must not exceed 20000 RUB. Provided: … | Amount > 20000 |
| 400 | Bank name must be 2-50 characters | Bank name after trim shorter than 2 or longer than 50 (after normalize up to 80) |
| 400 | Invalid recipient name | Name after trim shorter than 2 or longer than 100. Typical: name: "-" or whitespace. Payout is not created. GET by order_id = 404. Do not poll. |
| 400 | Invalid phone number (expected Russian mobile, e.g. 79001234567) | Not a Russian mobile (after normalize not 11 digits 79…) |
| 400 | SBP daily phone request limit exceeded | Phone is on the daily SBP request blacklist. A payout is created and immediately cancelled with a refund. Response includes payout_id and id. GET finds it as failure. Retry with the same order_id → Duplicate |
| 402 | Insufficient balance | Same as card |
| 403 | Invalid signature | Signature path /api/payout/sbp |
| 403 | SBP payouts are disabled for this account | SBP disabled for the merchant |
| 500 | Internal server error | Unhandled exception |
POST /api/payout/batchRequest-level errors (no payout is parsed):
| HTTP | error |
|---|---|
| 400 | No data provided |
| 400 | Missing field: batch_id / payouts / signature |
| 400 | batch_id must be non-empty |
| 400 | payouts must be a JSON array |
| 400 | payouts must be a non-empty array |
| 400 | Too many payouts (max 50) |
| 403 | Invalid signature (path /api/payout/batch) |
If some items are valid: HTTP 200, status: "partial"; failed items have status: "error" and the same error/code as §§4.3–4.4, plus type must be 'card' or 'sbp', payouts[N] must be an object.
If every item is rejected: HTTP 400 (or 402 if all are balance errors; or 403 if all are SBP-disabled), status: "rejected".
| Method | HTTP | error | When |
|---|---|---|---|
| GET /api/payout/<ID> | 404 | Payout not found | No payout with that internal id / public id / this merchant’s order_id |
| GET /api/payout/<ID> | 403 | Access denied | Payout exists for another user_id |
| GET …/receipt | 404 | Payout not found | Lookup by public receipt id only |
| GET …/receipt | 403 | Access denied | Another merchant’s payout |
| GET …/receipt | 409 | Receipt not ready, payout is still processing | Not success yet |
| GET …/receipt | 409 | Receipt is only available for successful payouts | Terminal failure |
| GET …/receipt | 500 | Failed to generate receipt | PDF generation failed |
POST /api/payout/batch/status: No data provided, Missing field: signature, ids must be a JSON array / ids must be an array, Provide ids and/or batch_id, Too many ids (max 50), Invalid signature. Per-item: Empty id, Payout not found (code 404), Access denied.
HTTP/1.1 400 Bad Request
{
"error": "Duplicate order_id",
"code": 400,
"order_id": "my_order_001",
"payout_id": 42,
"id": "19E3BABE0796332",
"status": "in_progress"
}
To ensure API stability, the following limits apply:
503 response code. Please implement a retry mechanism with exponential backoff.Instead of continuously polling GET /api/payout/<ID>, you can receive push notifications on every payout status change. JetBot sends a POST request to your configured URL. Configuration (the URL and signing secret) is done on the JetBot side — provide them to your administrator.
failure notification is sent only after the payout has definitively failed and funds have been refunded. If one payment gateway declines the payout and it is automatically re-routed to another gateway, failure is not sent. You can safely build business logic (e.g. refunding your customer) on this status.
A notification is sent whenever the client-facing payout status changes. Duplicate notifications for the same status are not sent.
| status | Meaning |
|---|---|
created | Payout accepted and created. |
in_progress | Payout is being processed by a gateway (including gateway retries/switching). |
successful | Payout completed successfully (terminal status). |
failure | Payout definitively declined, funds refunded to balance (terminal status). |
Method POST, JSON body (Content-Type: application/json). Headers:
| Header | Description |
|---|---|
X-Webhook-Event | Event type, always payout.status_updated. |
X-Webhook-Id | Unique event ID (event_id). Use it for idempotency. |
X-Signature | Optional. Body signature in the form sha256=<hex> (HMAC-SHA256). Absent when signing is not configured. |
{
"event": "payout.status_updated",
"event_id": "9f1c2b7e4a8d4f6e9b0c1d2e3f4a5b6c",
"status": "successful", // created | in_progress | successful | failure
"payout_id": "358472DC4B20E6A", // payout ID (as on the receipt / the id field on creation)
"order_id": "my_order_001", // your merchant order_id
"amount": 1500.00,
"currency": "RUB",
"payout_type": "card",
"created_at": "2023-10-27 14:30:00",
"timestamp": "2023-10-27T11:31:05Z" // UTC, when the notification was sent
}
By default webhooks are sent without a signature. Sender authenticity is verified by IP address: all notifications originate from the static address 185.207.14.225. Add it to your allowlist and only accept webhooks from it.
remote_addr), not the X-Forwarded-For / X-Real-IP headers — those can be spoofed. If your server is behind a reverse proxy (nginx, Cloudflare, etc.), take the IP from a trusted proxy source. Only accept over HTTPS.
If a secret is configured for your account, each notification is additionally signed. The signature is computed as HMAC-SHA256 over the raw request body (exactly as received, without re-serializing the JSON) using your secret. Compare the result with the value in the X-Signature header (strip the sha256= prefix). Always use a constant-time comparison. If signing is not configured, the X-Signature header is not sent.
import hmac, hashlib
from flask import request, abort
WEBHOOK_SECRET = b"your_webhook_secret"
@app.route("/jetbot/webhook", methods=["POST"])
def jetbot_webhook():
raw = request.get_data() # the raw body, before JSON parsing
received = request.headers.get("X-Signature", "").removeprefix("sha256=")
expected = hmac.new(WEBHOOK_SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected):
abort(403)
event = request.get_json()
# Idempotency: skip event_ids you have already processed
# if already_processed(event["event_id"]): return "", 200
# ... handle event["status"] ...
return "", 200 # return 2xx to acknowledge receipt
2xx status code to acknowledge receipt. Any other code or a timeout is treated as a failed delivery.1 min → 5 min → 30 min → 2 h → 6 h → 12 h → 24 h, up to 8 attempts.event_id (X-Webhook-Id).