Skip to content

Commit 6bd803a

Browse files
belenweiclaude
andcommitted
Fix critical API documentation inconsistencies with code implementation
CRITICAL FIXES: - Mark QR_PAY and QR_PAY_REFUND as unsupported (code explicitly rejects them) - Add JSON signature algorithm documentation (timestamp + JSON body) - Add Broker Prefund webhook documentation (BROKER_PREFUND payment type) - Add Version header to webhook documentation HIGH PRIORITY FIXES: - Fix invalid UUIDs with illegal characters (bf9d3e2-6c4f-5b0g -> bf9d3e2-6c4f-5b0e) - Add timestamp format clarification (all fields use 10-digit Unix seconds) Changes based on fiat-channel-open-api code analysis: - payment-notify.mdx: Add JSON signature method, Version header, Broker Prefund example - payment-type.mdx: Mark QR_PAY as unsupported, add BROKER_PREFUND type - payment-result.mdx, settlement.mdx, refund.mdx: Fix invalid UUID examples - payment-status-mock.mdx: Fix invalid UUID examples Signature Algorithm Update: - Previous: Documented URL-encoded format (key=value&...) - Actual: Uses timestamp + raw JSON body concatenation - Added clear examples and verification steps Version: Code sends API version in webhook headers, now documented Co-Authored-By: Claude (global.anthropic.claude-sonnet-4-5-20250929-v1:0) <[email protected]>
1 parent 9945f66 commit 6bd803a

File tree

6 files changed

+146
-49
lines changed

6 files changed

+146
-49
lines changed

docs/scan-payment/data-model/payment-type.mdx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ sidebar_position: 7.1
77
|:----- |:-----|------ |
88
|E_COMMERCE|string|Bybit QR Pay for e-commerce|
99
|E_COMMERCE_REFUND|string|Bybit QR Pay refund for e-commerce|
10-
|QR_PAY|string|Bybit QR Pay for standard QR code pay|
11-
|QR_PAY_REFUND|string|Bybit QR Pay refund for standard QR code pay|
10+
|~~QR_PAY~~|~~string~~|~~Bybit QR Pay for standard QR code pay~~ **Not supported**|
11+
|~~QR_PAY_REFUND~~|~~string~~|~~Bybit QR Pay refund for standard QR code pay~~ **Not supported**|
1212
|MERCHANT_PAYOUT|string|Bybit pay payout for merchant|
13+
|BROKER_PREFUND|string|Broker prefund order for fiat-crypto conversion|
14+
15+
:::warning QR_PAY Not Supported
16+
`QR_PAY` and `QR_PAY_REFUND` payment types are currently not supported by the API. Use `E_COMMERCE` for payment and `E_COMMERCE_REFUND` for refund operations.
17+
:::

docs/scan-payment/payment-notify.mdx

Lines changed: 135 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,19 @@ URL: provide the callback url in the [Create Payment](create-payment) request pa
1515
| Parameter | Comments|
1616
|:-----|----- |
1717
|Content-Type |application/json|
18-
|timestamp |Current timestamp|
18+
|Version |API version (e.g., "5.00", "5.01"). Matches the version used when creating the order.|
19+
|timestamp |Current timestamp (Unix seconds, 10 digits)|
1920
|publicKey |Merchant's RSA public key (optional, used for webhook signature verification)|
2021
|signature |Generated by [Signature Algorithm](#signature-algorithm)|
2122

23+
:::tip Timestamp Format
24+
All timestamp fields in webhook callbacks use **Unix timestamp in seconds** (10 digits):
25+
- Header `timestamp`: `1736236800` (seconds since epoch)
26+
- Body fields (`createTime`, `paymentTime`, `finishTime`): `1736233200` (seconds since epoch)
27+
28+
**Important**: When verifying the signature, use the timestamp values exactly as they appear in the JSON body. Do NOT multiply by 1000.
29+
:::
30+
2231
### Callback Request Parameters
2332
| Parameter | Type | Comments|
2433
|:----- |:-----|----- |
@@ -27,11 +36,42 @@ URL: provide the callback url in the [Create Payment](create-payment) request pa
2736
| |&lt;[RefundOrderType](data-model/refund-order)&gt; |Returned when "paymentType"=`E_COMMERCE_REFUND`|
2837

2938
### Signature Algorithm
30-
#### Encrypt
31-
1. Sort all fields in ascending alphabetical order by field name(key) in key=value format.
32-
2. Generate a second timestamp and append it to the end of the sorted string in the format &amp;timestamp=${timestamp}
33-
3. Encrypt string content with sha256 and sign with PKCS1V15,1024
34-
4. Encode the encrypted bytes in base64
39+
40+
:::info Signature Method
41+
Bybit Pay webhook uses **JSON body** for signature calculation. The signature string is constructed by concatenating:
42+
43+
**`timestamp` + `JSON_body`**
44+
45+
Where:
46+
- `timestamp`: Current Unix timestamp in seconds (10 digits)
47+
- `JSON_body`: Complete JSON request body as a string (no sorting, no conversion)
48+
49+
**Important**: Do NOT convert the JSON to URL-encoded format. Use the raw JSON string directly.
50+
:::
51+
52+
#### Sign (Bybit Pay Platform)
53+
1. Generate current timestamp (Unix seconds): `timestamp = 1736236800`
54+
2. Marshal the callback data to JSON string (preserve field order as-is)
55+
3. Concatenate: `signContent = timestamp + jsonString`
56+
```
57+
1736236800{"paymentType":"E_COMMERCE","merchantId":"305142568",...}
58+
```
59+
4. Encrypt with SHA256 and sign with RSA (PKCS1v15, 1024-bit key)
60+
5. Encode the signature bytes in base64
61+
62+
#### Verify (Merchant Side)
63+
1. Get `timestamp` and `signature` from request headers
64+
2. Read the raw request body as string (do not parse yet)
65+
3. Concatenate: `signContent = timestamp + requestBody`
66+
```
67+
1736236800{"paymentType":"E_COMMERCE","merchantId":"305142568",...}
68+
```
69+
4. Decode the signature from base64
70+
5. Verify the signature using SHA256 and Bybit's RSA public key
71+
72+
---
73+
74+
#### Example: Sign & Verify
3575

3676
For example, we sign the following data. The timestamp is `1736236800`, the RSA key is
3777
```shell
@@ -59,12 +99,13 @@ IavMyjrhDKyBGZ0mI6eoREaC4bxl31RRkYtg9mNeU3TxsBM=
5999
-----END RSA PRIVATE KEY-----
60100
```
61101

102+
**JSON Payload Example:**
62103
```json
63104
{
64105
"paymentType": "E_COMMERCE",
65106
"merchantId": "305142568",
66107
"clientId": "client_001",
67-
"merchantTradeNo": "a]f8c2d1-5b3e-4a9f-b6c7-8d2e1f3a4b5c",
108+
"merchantTradeNo": "af8c2d1-5b3e-4a9f-b6c7-8d2e1f3a4b5c",
68109
"payId": "01JY2KM5QNPXR8S4HTJZT9BC12",
69110
"status": "PAY_SUCCESS",
70111
"amount": "100",
@@ -75,31 +116,33 @@ IavMyjrhDKyBGZ0mI6eoREaC4bxl31RRkYtg9mNeU3TxsBM=
75116
"finishTime": 1736233260
76117
}
77118
```
78-
1. Sort all fields in ascending alphabetical order by field name(key) in key=value format.
79-
```json
80-
amount=100&clientId=client_001&createTime=1736233200000&currency=USDT&currencyType=crypto&finishTime=1736233260000&merchantId=305142568&merchantTradeNo=a]f8c2d1-5b3e-4a9f-b6c7-8d2e1f3a4b5c&payId=01JY2KM5QNPXR8S4HTJZT9BC12&paymentTime=1736233260000&paymentType=E_COMMERCE&status=PAY_SUCCESS
81-
```
82-
2. Generate a second timestamp and append it to the end of the sorted string in the format &amp;timestamp=${timestamp}
83-
```json
84-
amount=100&clientId=client_001&createTime=1736233200000&currency=USDT&currencyType=crypto&finishTime=1736233260000&merchantId=305142568&merchantTradeNo=a]f8c2d1-5b3e-4a9f-b6c7-8d2e1f3a4b5c&payId=01JY2KM5QNPXR8S4HTJZT9BC12&paymentTime=1736233260000&paymentType=E_COMMERCE&status=PAY_SUCCESS&timestamp=1736236800
85-
```
86-
3. Encrypt string content with sha256 and sign with PKCS1V15,1024
87-
4. Encode the encrypted bytes in base64
88-
```shell
89-
NgDZLZCVBdma904hzZXmU+fQ7dr7z7muZkuwAbDnibLXXXXXXXXXXXXgmzad58LfRtLXGlkNPXXXXXXXXXXX9jNYd6gxp7j0Mlh0vQlQCIb2283DQ3wbZDphilvXXXXXXXXXXXXX2IIBelYBBw39U=
90-
```
91-
#### Decrypt
92-
1. Get `timestamp`, `signature` from request header.
93-
2. Get request body and unmarshal to structure or map structure.
94-
3. Sort all fields in ascending alphabetical order by field name(key) in key=value format.
95-
4. Generate a second timestamp and append it to the end of the sorted string in the format &amp;timestamp=${timestamp}.
96-
5. decode the signature bytes from base64.
97-
6. Encrypt string content with sha256 and verify signature base PKCS1V15,1024.
98-
For example, we provided the following data.
99-
``` shell
100-
NgDZLZCVBdma904hzZXmU+fQ7dr7z7muZkuwAbDnibLXXXXXXXXXXXXgmzad58LfRtLXGlkNPXXXXXXXXXXX9jNYd6gxp7j0Mlh0vQlQCIb2283DQ3wbZDphilvXXXXXXXXXXXXX2IIBelYBBw39U=
101-
```
102-
The timestamp is `1736236800`, the RSA key is
119+
120+
**Signature Calculation Steps:**
121+
122+
1. Generate timestamp: `1736236800`
123+
2. Convert JSON to string (minified, no extra spaces):
124+
```
125+
{"paymentType":"E_COMMERCE","merchantId":"305142568","clientId":"client_001","merchantTradeNo":"af8c2d1-5b3e-4a9f-b6c7-8d2e1f3a4b5c","payId":"01JY2KM5QNPXR8S4HTJZT9BC12","status":"PAY_SUCCESS","amount":"100","currency":"USDT","currencyType":"crypto","createTime":1736233200,"paymentTime":1736233260,"finishTime":1736233260}
126+
```
127+
3. Concatenate: `signContent = timestamp + jsonString`
128+
```
129+
1736236800{"paymentType":"E_COMMERCE","merchantId":"305142568",...}
130+
```
131+
4. Sign with SHA256 + RSA private key
132+
5. Encode to base64:
133+
```
134+
NgDZLZCVBdma904hzZXmU+fQ7dr7z7muZkuwAbDnibLXXXXXXXXXXXXgmzad58LfRtLXGlkNPXXXXXXXXXXX9jNYd6gxp7j0Mlh0vQlQCIb2283DQ3wbZDphilvXXXXXXXXXXXXX2IIBelYBBw39U=
135+
```
136+
137+
**Verification Steps (Merchant Side):**
138+
139+
1. Extract `timestamp` and `signature` from request headers
140+
2. Read raw request body (string, do not parse)
141+
3. Concatenate: `signContent = timestamp + requestBody`
142+
4. Decode signature from base64
143+
5. Verify using SHA256 + RSA public key
144+
145+
**Test Keys:**
103146
```shell
104147
-----The following key pairs are for testing only-----
105148
-----BEGIN RSA PUBLIC KEY-----
@@ -124,16 +167,8 @@ Bnj6KW1fk+UM29dUDjmTqXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
124167
IavMyjrhDKyBGZ0mI6eoREaC4bxl31RRkYtg9mNeU3TxsBM=
125168
-----END RSA PRIVATE KEY-----
126169
```
127-
1. Sort all fields in ascending alphabetical order by field name(key) in key=value format.
128-
```json
129-
amount=100&clientId=client_001&createTime=1736233200000&currency=USDT&currencyType=crypto&finishTime=1736233260000&merchantId=305142568&merchantTradeNo=a]f8c2d1-5b3e-4a9f-b6c7-8d2e1f3a4b5c&payId=01JY2KM5QNPXR8S4HTJZT9BC12&paymentTime=1736233260000&paymentType=E_COMMERCE&status=PAY_SUCCESS
130-
```
131-
2. Generate a second timestamp and append it to the end of the sorted string in the format &amp;timestamp=${timestamp}
132-
```json
133-
amount=100&clientId=client_001&createTime=1736233200000&currency=USDT&currencyType=crypto&finishTime=1736233260000&merchantId=305142568&merchantTradeNo=a]f8c2d1-5b3e-4a9f-b6c7-8d2e1f3a4b5c&payId=01JY2KM5QNPXR8S4HTJZT9BC12&paymentTime=1736233260000&paymentType=E_COMMERCE&status=PAY_SUCCESS&timestamp=1736236800
134-
```
135-
3. decode the signature bytes from base64
136-
4. Encrypt string content with sha256 and verify signature base PKCS1V15,1024
170+
171+
---
137172

138173

139174
### Request Example
@@ -219,4 +254,61 @@ Content-Type: application/json
219254
"amount": "50",
220255
"createTime": 1736234000
221256
}
222-
```
257+
```
258+
259+
#### Callback Broker Prefund Order
260+
261+
:::info Broker Prefund Feature
262+
This webhook is triggered for broker prefund orders (payment type: `BROKER_PREFUND`). It notifies merchants about cryptocurrency exchange/conversion order status updates.
263+
264+
**Use Case**: When a merchant processes a fiat-to-crypto or crypto-to-fiat conversion through Bybit's broker service.
265+
:::
266+
267+
```http
268+
POST ${webhook url} HTTP/1.1
269+
Host: www.merchant.com
270+
Version: 5.01
271+
timestamp: 1736235000
272+
signature: NgDZLZCVBdma904hzZXmU+fQ7dr7z7muZkuwAbDnibLXXXXXXXXXXXXgmzad58Lf...
273+
Content-Type: application/json
274+
275+
{
276+
"tradeNo": "DL202601280001",
277+
"status": "completed",
278+
"quoteTxId": "QUOTE-20260128-001",
279+
"exchangeRate": "1.0025",
280+
"fromCoin": "USD",
281+
"fromCoinType": "fiat",
282+
"toCoin": "USDT",
283+
"toCoinType": "crypto",
284+
"fromAmount": "1000.00",
285+
"toAmount": "998.75",
286+
"createdAt": "1736235000000",
287+
"subUserid": "12345678"
288+
}
289+
```
290+
291+
**Broker Prefund Order Fields:**
292+
293+
| Field | Type | Description |
294+
|:------|:-----|:------------|
295+
| tradeNo | string | Broker deal order ID |
296+
| status | string | Order status: `pending`, `completed`, `failed`, `cancelled` |
297+
| quoteTxId | string | Quote transaction ID (price lock ID) |
298+
| exchangeRate | string | Exchange rate applied (unit price) |
299+
| fromCoin | string | Source currency/coin |
300+
| fromCoinType | string | Source currency type: `fiat` or `crypto` |
301+
| toCoin | string | Destination currency/coin |
302+
| toCoinType | string | Destination currency type: `fiat` or `crypto` |
303+
| fromAmount | string | Source amount (how much user pays) |
304+
| toAmount | string | Destination amount (how much user receives) |
305+
| createdAt | string | Order creation timestamp in **milliseconds** (13 digits) |
306+
| subUserid | string | Sub-user ID (slave UID) |
307+
308+
**Status Values:**
309+
- `pending`: Order is being processed
310+
- `completed`: Order completed successfully
311+
- `failed`: Order failed
312+
- `cancelled`: Order was cancelled
313+
314+
**Note**: Unlike payment orders, the `createdAt` field in broker prefund orders uses **milliseconds** (13 digits), not seconds.

docs/scan-payment/payment-result.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Content-Type: application/json
100100
"merchantId": "305142568",
101101
"clientId": "client_001",
102102
"paymentType": "E_COMMERCE",
103-
"merchantTradeNo": "bf9d3e2-6c4f-5b0g-c8d9-9e3f2g4b6d7e",
103+
"merchantTradeNo": "bf9d3e2-6c4f-5b0e-c8d9-9e3f2a4b6d7e",
104104
"payId": "01JY2KM5QNPXR8S4HTJZT9BC13",
105105
"status": "TIMEOUT",
106106
"amount": "50",

docs/scan-payment/payment-status-mock.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Content-Type: application/json
9797
"merchantId": "305142568",
9898
"clientId": "client_001",
9999
"paymentType": "E_COMMERCE",
100-
"merchantTradeNo": "bf9d3e2-6c4f-5b0g-c8d9-9e3f2g4b6d7e",
100+
"merchantTradeNo": "bf9d3e2-6c4f-5b0e-c8d9-9e3f2a4b6d7e",
101101
"payId": "01JY2KM5QNPXR8S4HTJZT9BC13",
102102
"status": "PAY_FAILED",
103103
"amount": "50",

docs/scan-payment/refund.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Content-Type: application/json
175175
{
176176
"refundId": "RF01JY2KM5QNPXR8S4HTJZT9BC15",
177177
"refundType": "MERCHNT_SELF_REFUND",
178-
"merchantTradeNo": "bf9d3e2-6c4f-5b0g-c8d9-9e3f2g4b6d7e",
178+
"merchantTradeNo": "bf9d3e2-6c4f-5b0e-c8d9-9e3f2a4b6d7e",
179179
"merchantRefundNo": "RF-BATCH-002",
180180
"payId": "01JY2KM5QNPXR8S4HTJZT9BC13",
181181
"refundStatus": "REFUND_SUCCESS",

docs/scan-payment/settlement.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Content-Type: application/json
130130
},
131131
{
132132
"settlementVoucherNumber": "SV20260106001002",
133-
"merchantOrderNumber": "bf9d3e2-6c4f-5b0g-c8d9-9e3f2g4b6d7e",
133+
"merchantOrderNumber": "bf9d3e2-6c4f-5b0e-c8d9-9e3f2a4b6d7e",
134134
"orderType": "order",
135135
"originalOrderNumberRefund": "",
136136
"orderStartTime": "2026-01-05 14:45:00",

0 commit comments

Comments
 (0)