Русская версия 🇷🇺

JetBot API v1.0

Integration documentation for partners.

Production Base URL: https://api.jetbot.pw
Sandbox Base URL: https://dev.jetbot.pw (for testing)

1. Connection and Authentication

To work with the API, follow these steps:

  1. Get Bearer Token: Contact the JetBot administrator to create an API account and receive an access token.
  2. Generate RSA Keys: You need an RSA key pair (4096 bit) to sign requests.
    • The Private Key remains with you and is used to generate the signature.
    • The Public Key must be provided to the administrator to verify your requests.
  3. Whitelist IP: Provide the static IP address of your server from which requests will be sent. Access to the API is allowed only from trusted IPs.

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.

Request Headers

All API requests must contain the Authorization HTTP header:

Authorization: Bearer YOUR_TOKEN

2. Signature Generation

Methods that change state (e.g., creating a payout) require a mandatory signature parameter to protect against request forgery.

RSA-SHA512 Signature Algorithm:

  1. Collect all parameters from the request body (JSON or Form-data) into a dictionary.
  2. Exclude the signature field from this dictionary if present.
  3. Sort the parameters by keys (parameter names) alphabetically (A-Z).
  4. Assemble the string for signing in the format: METHOD_PATH?key1=value1&key2=value2.
    Example string: /api/payout?amount=1000&order_id=123&recipient=4444555566667777
    Note: parameter values must be exactly the same as in the sent request.
  5. Sign the resulting string with your Private Key using the SHA512 hashing algorithm.
  6. Encode the resulting binary signature into a Base64 string.
  7. Pass this string in the signature parameter in the request body.

Python Code Example:

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()

3. API Methods

GET /api/balance

Get the current balance of your merchant account in USDT.

Example Response:

{
  "balance": 5400.50,
  "currency": "USDT"
}
GET /api/rate

Get the current USDT/RUB exchange rate for your merchant account. No signature required.

Example Response:

{
  "rate": "71.82"
}

The rate field is a string with two decimal places (dot as separator), rubles per 1 USDT.

POST /api/payout

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).

Example Success Response:

{
  "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.

POST /api/payout/sbp

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:

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).

Example Success Response:

{
  "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 /api/payout/<ID>

Get the current status of a payout. You can use the following as <ID>:

GET 404 vs create errors: if 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).

Example Response:

{
  "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
}
GET /api/payout/<public_id>/receipt

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).

Example Request (cURL):

curl -H "Authorization: Bearer <TOKEN>" \
     -o receipt.pdf \
     https://api.jetbot.pw/api/payout/358472DC4B20E6A/receipt

Possible Errors:

4. Error Codes

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.

4.1. HTTP codes (summary)

HTTPWhenRetry?
400Body validation, duplicate order_id, SBP daily phone request limitNo until you fix the payload. Duplicate — do not create again; use payout_id/id from the body
401Missing/invalid Bearer tokenNo until you fix the header
402Not enough USDT on the merchant balanceAfter topping up, with a new order_id if nothing was created
403IP not whitelisted, invalid RSA signature, SBP disabled, another merchant’s payoutNo until you fix access/signature
404Payout not found (including after a failed POST — nothing was created)Do not poll “until it appears”. Recreate only if POST was not 200
409Receipt: payout is not successfulReceipt only after successful
500Internal error on create / receiptCautious retry; GET by order_id first to see if a payout exists
503Nginx: more than 10 req/s per IP (burst 20)Yes, exponential backoff

4.2. All methods — auth and IP

Checked before parsing the body. Signed bodies are not processed on 401/403.

HTTPerrorWhen
401Missing or invalid Authorization headerNo header or not Bearer <token>
401Invalid tokenUnknown / disabled token
403Access deniedClient IP not on the merchant whitelist (same text for GET of another merchant’s payout)

4.3. POST /api/payout (card)

Required before business logic: amount, recipient, order_id, signature.

HTTPerrorWhen
400No data providedEmpty body (no JSON and no form)
400Missing field: amount / recipient / order_id / signatureRequired field missing. Empty order_id is also Missing field: order_id
400Duplicate order_idThis 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
400Invalid amountamount is not a number
400Amount must be at least 110 RUB. Provided: …Amount < 110
400Amount must not exceed 400000 RUB. Provided: …Amount > 400000
400Card number must contain exactly 16 digits. Provided: N digitsNot 16 digits after stripping non-digits
400Invalid card number (failed Luhn check)16 digits, Luhn fails
402Insufficient balanceNot enough USDT. Extra: required, available. Payout not created
403Invalid signatureRSA-SHA512 mismatch (path /api/payout)
500Internal server errorUnhandled exception during create

4.4. POST /api/payout/sbp

Required: amount, bank, name, phone, order_id, signature.

HTTPerrorWhen
400No data providedEmpty body
400Missing field: …Required field missing (including empty order_id)
400Duplicate order_idSame as card. Response includes payout_id, id, status
400Invalid amountamount is not a number
400Amount must be at least 110 RUB. Provided: …Amount < 110
400SBP amount must not exceed 20000 RUB. Provided: …Amount > 20000
400Bank name must be 2-50 charactersBank name after trim shorter than 2 or longer than 50 (after normalize up to 80)
400Invalid recipient nameName 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.
400Invalid phone number (expected Russian mobile, e.g. 79001234567)Not a Russian mobile (after normalize not 11 digits 79…)
400SBP daily phone request limit exceededPhone 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
402Insufficient balanceSame as card
403Invalid signatureSignature path /api/payout/sbp
403SBP payouts are disabled for this accountSBP disabled for the merchant
500Internal server errorUnhandled exception

4.5. POST /api/payout/batch

Request-level errors (no payout is parsed):

HTTPerror
400No data provided
400Missing field: batch_id / payouts / signature
400batch_id must be non-empty
400payouts must be a JSON array
400payouts must be a non-empty array
400Too many payouts (max 50)
403Invalid 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".

4.6. GET status and receipt

MethodHTTPerrorWhen
GET /api/payout/<ID>404Payout not foundNo payout with that internal id / public id / this merchant’s order_id
GET /api/payout/<ID>403Access deniedPayout exists for another user_id
GET …/receipt404Payout not foundLookup by public receipt id only
GET …/receipt403Access deniedAnother merchant’s payout
GET …/receipt409Receipt not ready, payout is still processingNot success yet
GET …/receipt409Receipt is only available for successful payoutsTerminal failure
GET …/receipt500Failed to generate receiptPDF 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.

4.7. Example: duplicate order_id

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"
}

5. Rate Limits

To ensure API stability, the following limits apply:

6. Webhooks (status notifications)

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.

Terminal status guarantee: a 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.

When it is sent

A notification is sent whenever the client-facing payout status changes. Duplicate notifications for the same status are not sent.

statusMeaning
createdPayout accepted and created.
in_progressPayout is being processed by a gateway (including gateway retries/switching).
successfulPayout completed successfully (terminal status).
failurePayout definitively declined, funds refunded to balance (terminal status).

Request format

Method POST, JSON body (Content-Type: application/json). Headers:

HeaderDescription
X-Webhook-EventEvent type, always payout.status_updated.
X-Webhook-IdUnique event ID (event_id). Use it for idempotency.
X-SignatureOptional. Body signature in the form sha256=<hex> (HMAC-SHA256). Absent when signing is not configured.

Example body:

{
  "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
}

IP authentication (default)

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.

Important: verify the real TCP connection IP (socket / 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.

Signature verification (optional)

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.

Example verification in Python (Flask):

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

Acknowledgement and retries