跳至主要内容

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://api.bybit.com or https://api.bytick.com
  • Testnet: https://api-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

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/pay/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/pay/agreement/deduction → deduct using agreement_no
5. POST {notify_url} ← Bybit notifies deduction result
6. GET /v5/pay/agreement/transaction/query → query if webhook not received
7. POST /v5/pay/agreement/refund → refund if needed (see [Refund API](recurring-payments/refund))
8. POST /v5/pay/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/pay/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/pay/agreement/deduction → 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/pay/agreement/sign        (agreement_type: SINGLE)
2. User signs
3. POST /v5/pay/agreement/deduction → 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/pay/agreement/signCreate sign request (get QR for user)
POST/v5/pay/agreement/unsignTerminate agreement
POST/v5/pay/agreement/deductionExecute deduction
POST/v5/pay/agreement/pay-with-signSign + deduct in one step — use when user is present to authorize and pay immediately (NON_CYCLE / SINGLE)
POST/v5/pay/agreement/refundRefund deduction
GET/v5/pay/agreement/queryQuery single agreement (check SIGNED status)
GET/v5/pay/agreement/listList agreements (paginated)
GET/v5/pay/agreement/transaction/queryQuery single transaction/refund
GET/v5/pay/agreement/transaction/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/pay/agreement/transaction/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
50000–50002System errorRetry with exponential backoff

Retryable errors: 50000, 50001, 50002, 139002005, 139003003 Non-retryable: All 139001xxx, 139002001–139002004, 139003001–139003002


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/pay/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