Skip to main content

Bybit Pay — AI Integration Skill

Single-file reference for AI-assisted merchant integration of Bybit Pay QR Payment and Recurring Payments (Auto-Deduction) APIs.


Products Overview

ProductUse CaseKey Flow
QR PaymentOne-time scan-to-payCreate order → Display QR → Webhook notify
Recurring PaymentsAuto-deduction (utilities, subscriptions, ride-hailing)Sign agreement → Deduct → Webhook notify

Base URL:

  • Mainnet: https://api2.bybit.com or https://api.bytick.com
  • Testnet: https://api2-testnet.bybit.com

Authentication (Both Products)

Both QR Payment and Recurring Payments use identical Bybit standard API authentication.

Required Headers

X-BAPI-API-KEY:     {your_api_key}
X-BAPI-TIMESTAMP: {unix_ms} # milliseconds, e.g. 1736233200000
X-BAPI-SIGN: {signature}
X-BAPI-RECV-WINDOW: 5000 # default 5000ms, max 10000ms
Content-Type: application/json

QR Payment only: also add Version: 5.00 header.

Signature Construction

Step 1 — Build the plain string:

# POST request
plain = timestamp + api_key + recv_window + raw_json_body

# GET request
plain = timestamp + api_key + recv_window + raw_query_string
# raw_query_string must be unescaped: name=foo&age=18 ✓ name%3Dfoo ✗

Step 2 — Sign:

# HMAC_SHA256 (system-generated API key) → hex string
X-BAPI-SIGN = HEX( HMAC_SHA256(plain, api_secret) )

# RSA_SHA256 (self-generated API key) → base64 string
X-BAPI-SIGN = Base64( RSA_SHA256_Sign(plain, merchant_private_key) )

Example (POST):

plain = "1736233200000<api_key>5000{"merchant_id":"M123456789",...}"

Timestamp constraint: server_time - recv_window ≤ timestamp < server_time + 1000

Reference implementations: https://github.com/bybit-exchange/api-usage-examples

Common Response Envelope

{
"retCode": 100000,
"retMsg": "success",
"result": { },
"retExtInfo": {},
"time": 1736233200000
}

retCode=100000 (QR Payment) or retCode=20000 (Recurring Payments) indicates success.


When to Use — Business Scenarios & API Flow

Scenario 1: E-Commerce Checkout (QR Payment)

When: User checks out on web/app, pays once by scanning a QR code.

1. POST /v5/bybitpay/create_pay       → get qrContent (base64 image) + checkoutLink
2. Display QR to user
3. POST {webhookUrl} ← Bybit notifies payment result (PAY_SUCCESS / PAY_FAILED)
4. GET /v5/bybitpay/pay_result → poll if webhook not received (fallback)
5. POST /v5/bybitpay/refund → if refund needed (supports partial & batch)

Key fields: merchantTradeNo (idempotency key) · orderAmount decimal string e.g. "23.50" · env.ip real user IP (required) · orderExpireTime Unix seconds max +1h See full field reference: Create Payment API · Refund API


Scenario 2: Subscription / Membership Auto-Renewal (Recurring — CYCLE)

When: Monthly/yearly fixed-cycle deduction (video membership, cloud service, gym card).

1. POST /v5/bybitpay/agreement/sign   → get qr_code / sign_url for user to authorize
2. Display QR to user (user verifies with SMS/Face/Password)
3. POST {notify_url} ← Bybit notifies SIGNED status (agreement_no returned)
4. [Each billing cycle]
POST /v5/bybitpay/agreement/pay → deduct using agreement_no
5. POST {notify_url} ← Bybit notifies deduction result
6. GET /v5/bybitpay/agreement/pay/query → query if webhook not received
7. POST /v5/bybitpay/agreement/refund → refund if needed (see [Refund API](recurring-payments/refund))
8. POST /v5/bybitpay/agreement/unsign → terminate when user cancels (see [Unsign API](recurring-payments/unsign))

Step 1 sign key fields: agreement_type · merchant_user_id · external_agreement_no (idempotency) · scene_code · single_limit · notify_url Response returns result.qr_code / result.sign_url to show user, and result.agreement_no. See full field reference: Sign Request API

Step 4 deduction key fields: agreement_no · out_trade_no (idempotency) · amount.total minimum unit integer string e.g. "2350" = 23.50 USDT · scene_code · notify_url scene_code values: SUBSCRIPTION · UTILITY_BILL · TRANSPORTATION · FOOD_DELIVERY · LIFESTYLE Verify agreement status == SIGNED via query API before deducting. See full field reference: Deduction API


Scenario 3: On-Demand Consumption (Recurring — NON_CYCLE)

When: Irregular deductions triggered by actual usage (ride-hailing, parking, food delivery).

1. POST /v5/bybitpay/agreement/sign   → user authorizes once (agreement_type: NON_CYCLE)
2. User scans QR / opens sign_url; Bybit notifies SIGNED webhook → store agreement_no
3. [Each consumption event]
POST /v5/bybitpay/agreement/pay → deduct; include scene_info.device_ip & location
4. POST {notify_url} ← async result notification

Difference from CYCLE: No fixed schedule; merchant initiates deduction whenever a transaction occurs.


Scenario 4: One-Time Pre-Authorization (Recurring — SINGLE)

When: Hotel deposit, car rental deposit — one authorization, one deduction, auto-expires.

1. POST /v5/bybitpay/agreement/sign   (agreement_type: SINGLE)
2. User signs
3. POST /v5/bybitpay/agreement/pay → one deduction only
Agreement automatically becomes UNSIGNED after deduction

Scenario 5: Merchant Payout to User

When: Merchant sends crypto to a Bybit user (rewards, cashback, refund to wallet).

1. POST /v5/bybitpay/payout           → paymentType: MERCHANT_PAYOUT
2. GET /v5/bybitpay/pay_result → query status (or receive webhook)

Note: Requires payee.uid (Bybit user UID) and mccCode.


Scenario 6: FX Conversion Before Payment

When: Merchant wants to quote exchange rate before creating an order in a different settlement currency.

1. POST /v5/bybitpay/fx/convert       → get quotationId + exchange rate
2. POST /v5/bybitpay/create_pay → include quotationId to lock the rate

API Reference Summary

QR Payment APIs

MethodEndpointPurpose
POST/v5/bybitpay/create_payCreate order, get QR code
GET/v5/bybitpay/pay_resultQuery payment/refund status
POST/v5/bybitpay/refundRefund (single, partial, or batch)
POST/v5/bybitpay/payoutPayout to Bybit user
POST/v5/bybitpay/fx/convertGet FX quote
POST{webhookUrl} (inbound)Receive payment/refund result
POST/v5/bybitpay/paystatus/mockMock status in sandbox only

Recurring Payments APIs

MethodEndpointPurpose
POST/v5/bybitpay/agreement/signCreate sign request (get QR for user)
POST/v5/bybitpay/agreement/unsignTerminate agreement
POST/v5/bybitpay/agreement/payExecute deduction
POST/v5/bybitpay/agreement/pay-with-signSign + deduct in one step — use when user is present to authorize and pay immediately (NON_CYCLE / SINGLE)
POST/v5/bybitpay/agreement/refundRefund deduction
GET/v5/bybitpay/agreement/queryQuery single agreement (check SIGNED status)
GET/v5/bybitpay/agreement/listList agreements (paginated)
GET/v5/bybitpay/agreement/pay/queryQuery single transaction/refund
GET/v5/bybitpay/agreement/pay/listList transactions (paginated)

Best Practices

1. Signature Verification (Webhook Authentication)

Bybit signs every webhook it sends to you. Always verify before processing.

QR Payment Webhook Verification

Headers from Bybit: timestamp (Unix seconds, 10 digits), signature

# Verify incoming QR Payment webhook
def verify_qr_webhook(timestamp: str, signature: str, raw_body: str, bybit_public_key) -> bool:
content = timestamp + raw_body # timestamp is seconds (10 digits)
sig_bytes = base64.b64decode(signature)
# Verify: SHA256 + RSA PKCS1v15 (1024-bit key)
try:
bybit_public_key.verify(sig_bytes, content.encode(), padding.PKCS1v15(), hashes.SHA256())
return True
except Exception:
return False

Critical: Use the raw request body string — do NOT re-serialize the parsed JSON.

Recurring Payments Webhook Verification

Headers from Bybit: X-Timestamp (ms), X-Signature, X-Nonce, X-Sign-Type: RSA2

// Verify incoming Recurring Payments webhook
public boolean verifyRecurringWebhook(String timestamp, String nonce,
String signature, String rawBody,
PublicKey platformPublicKey) throws Exception {
// Timestamp must be within 5 minutes
if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) > 5 * 60 * 1000) {
return false;
}
// Sign content = timestamp + nonce + rawBody
String content = timestamp + nonce + rawBody;
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(platformPublicKey);
sig.update(content.getBytes("UTF-8"));
return sig.verify(Base64.getDecoder().decode(signature));
}

Webhook response: Always return HTTP 200 with plain text body success. Bybit retries up to 5 times (15s → 30s → 1min → 5min → 30min).

Platform public key: Download from Bybit Merchant Portal → API Management → Platform Public Key. Required for webhook signature verification.

QR Payment Webhook Key Fields

status (PAY_SUCCESS / PAY_FAILED / REFUND_SUCCESS / TIMEOUT) · payId (dedup + polling key) · merchantTradeNo See full structure: Payment Notify

Recurring Payments Webhook Key Fields

notifyId (dedup) · notifyType (AGREEMENT_STATUS / TRANSACTION_RESULT / AGREEMENT_TIMEOUT / ORDER_TIMEOUT) · data.eventType (SIGNED / PAY / REFUND) · data.status (result) Route by notifyType → then data.eventType → then read data.status. See full structure: Webhooks Overview


2. Order Result Query — Webhook vs Polling

Primary strategy: Webhook (push)

  • Register webhookUrl (QR Payment) or notify_url (Recurring Payments) when creating orders
  • Process results asynchronously; respond success immediately
  • Use notifyId / payId for deduplication

Fallback strategy: Active polling

# QR Payment polling (when webhook is delayed)
Recommended interval: every 2–3 seconds
Max wait: up to order expiry (max 1 hour)
Stop on: PAY_SUCCESS, PAY_FAILED, TIMEOUT, REFUND_SUCCESS

GET /v5/bybitpay/pay_result?merchantId=...&paymentType=E_COMMERCE&payId={payId}

# Recurring Payments polling (after PROCESSING status or request timeout)
Recommended interval: every 3–5 seconds
Max attempts: 10 times
Stop on: SUCCESS, FAILED, TIMEOUT

GET /v5/bybitpay/agreement/pay/query?merchant_id=...&trade_no={trade_no}

Decision logic:

On API call timeout (30s) → query once immediately → if PROCESSING → poll
On webhook not received within N seconds → trigger active poll

3. Idempotency — Ensuring Transaction Uniqueness

ScenarioIdempotency KeyBehavior
QR Payment createmerchantTradeNoSame merchantTradeNo returns same order
QR Payment refundmerchantRefundNoSame merchantRefundNo returns same refund
Recurring signexternal_agreement_noSame value returns existing sign request
Recurring deductionout_trade_noSame out_trade_no returns first result
Recurring refundout_refund_noSame out_refund_no returns first result

Rules:

  • Generate idempotency keys before sending the request; store them persistently
  • After the first request fails: use a new key to retry (different from original failure)
  • After a request times out: query first — if already succeeded, do NOT retry with the same key
  • Never reuse order numbers across different orders
# Safe retry pattern for deduction
def safe_deduct(agreement_no, amount, existing_trade_no=None):
trade_no = existing_trade_no or generate_unique_trade_no()
try:
result = call_deduction_api(agreement_no, amount, trade_no)
if result.status == "PROCESSING":
return poll_until_final(trade_no)
return result
except TimeoutError:
# Query first before deciding to retry
existing = query_transaction(trade_no)
if existing:
return existing # already submitted, do NOT create new
# Only retry with new trade_no if truly not found
return safe_deduct(agreement_no, amount, generate_unique_trade_no())

4. Risk & Security — Device and IP Information

Provide risk context in every payment/deduction request. This helps pass Bybit's risk control and reduces false rejections.

{
"env": {
"terminalType": "WEB", // APP | WEB | WAP | MINIAPP | OTHERS
"device": "Mozilla/5.0 ...", // device UA or device model (e.g. iPhone15,2)
"browserVersion": "Chrome/133.0.0.0",
"ip": "203.0.113.50" // real user IP, not server IP
},
"riskInfo": {
"terminalType": "WEB"
}
}
{
"scene_info": {
"device_id": "device-fingerprint-abc123",
"device_ip": "203.0.113.50",
"location": {
"latitude": "39.9042",
"longitude": "116.4074",
"address": "Beijing, China"
}
},
"risk_info": {
"user_ip": "203.0.113.50",
"device_fingerprint": "fp_abc123xyz",
"user_agent": "Mozilla/5.0 ..."
}
}

Key rules:

  • Always pass the real end-user IP, not your backend server IP
  • For mobile apps: use device model as device (e.g., iPhone15,2, Pixel 8)
  • For ride-hailing/parking: include GPS location in scene_info
  • If risk control rejects (RISK_REJECT / 139005001): guide user to complete active payment with identity verification

5. Common Error Handling

QR Payment Error Codes

CodeMeaningAction
100000Success
400000Invalid parametersCheck request fields
400002Signature failedVerify sign algorithm & key
400003Timestamp timeoutSync server clock
400620Duplicate order numberUse unique merchantTradeNo
500008Merchant not foundCheck merchantId
500100QR code expiredCreate a new order
500104Refund balance unavailableTop up KYB funding account
500105Order not paidCannot refund unpaid order
500000Bybit internal errorRetry with exponential backoff

Recurring Payments Error Codes

CodeMeaningAction
20000Success
139001001Agreement not foundCheck agreement number
139001002Agreement expiredRe-sign
139001003Agreement unsignedRe-sign
139001012Sign URL expiredRe-call sign request API
139002002Quota exceededNotify user; wait for reset
139002003Insufficient balanceNotify user to top up
139002005Trade processingWait; do NOT retry with same out_trade_no
139004005Exceeds single limitLower amount or adjust limit
139005001Risk rejectedGuide to active payment
139005002Invalid signatureCheck sign algorithm
139005003Timestamp invalidMust be within 5 minutes of server time
5000050002System errorRetry with exponential backoff

Retryable errors: 50000, 50001, 50002, 139002005, 139003003 Non-retryable: All 139001xxx, 139002001139002004, 139003001139003002


Agreement Type Quick Reference

TypeDeduction FrequencyAfter First DeductionUse Case
CYCLEPeriodic (fixed schedule)Remains SIGNEDSubscriptions, monthly bills
NON_CYCLEIrregular (any time)Remains SIGNEDRide-hailing, parking, food delivery
SINGLEOne-time onlyAuto UNSIGNEDDeposits, one-time pre-auth

Limit support: CYCLE/NON_CYCLE support single_limit + period_limits (DAY/WEEK/MONTH/YEAR). SINGLE supports single_limit only.


Order Status Reference

QR Payment Order Status

INIT → PAY_SUCCESS → REFUND_SUCCESS
↘ PAY_FAILED
↘ TIMEOUT

Recurring Payments — Agreement Status

INIT → PENDING → SIGNED ⇄ SUSPENDED
↘ FAILED
↘ TIMEOUT
SIGNED → UNSIGNED (final)
SIGNED → EXPIRED (final)

Recurring Payments — Transaction Status

PROCESSING → SUCCESS
→ FAILED
→ TIMEOUT

Quick Start Checklist

Before You Start

Configure your server IP whitelist in Bybit Merchant Portal → API Management, otherwise all API calls return 403 FORBIDDEN.

QR Payment:

  • Generate API key + whitelist server IP in Merchant Portal (testnet first, then mainnet)
  • Implement HMAC_SHA256 or RSA_SHA256 request signing
  • Build POST /v5/bybitpay/create_pay with env.ip = real user IP
  • Host a public webhookUrl endpoint; verify Bybit's RSA signature
  • Store merchantTradeNo before calling API (idempotency)
  • Implement polling fallback using GET /v5/bybitpay/pay_result

Recurring Payments:

  • Call POST /v5/bybitpay/agreement/sign; display QR to user
  • Receive SIGNED webhook; store agreement_no
  • Verify webhook using X-Timestamp + X-Nonce + rawBody with platform RSA public key
  • Use unique out_trade_no per deduction; store before calling API
  • Handle PROCESSING status: wait for webhook, then poll pay/query
  • Implement unsign flow for user cancellation