Verification & Handling
When an important status change occurs in the GStable system (such as Payment Session Created, Payment Succeeded, Settlement Completed), we will send an HTTP POST request to your configured Webhook URL.
This chapter describes the data structure of the notification and how to verify the legitimacy of the request.
Event Object Structure
All Webhook events pushed by GStable follow a unified JSON envelope structure.
Common Fields
| Field | Type | Description |
|---|---|---|
eventId | String | Global unique identifier for the event (e.g., evt_i4NWz...). Recommended for idempotency handling (deduplication). |
eventType | String | Specific event type (e.g., session.created, session.paid). |
businessType | String | Business category type. Currently includes: session, payment_link, product, account. |
occurrence | String | Business time when the event occurred (Format: YYYY-MM-DD HH:mm:ss). |
isSubscribable | Boolean | Whether this event supports subscription. |
payload | Object | Business data payload. The internal structure changes depending on businessType. |
Payload Structure Example
The payload field wraps specific business object data. For example, for session type events, data is located in payload.sessionData.
{
"eventId": "evt_i4NWz4J3QkWugyq1",
"eventType": "session.created",
"businessType": "session",
"occurrence": "2026-01-04 02:52:38",
"isSubscribable": true,
"payload": {
// Business data wrapped in corresponding key, like sessionData, productData etc.
"sessionData": {
"sessionId": "sess_example_payment_02",
"status": "initialized",
"amount": 20000,
"currency": "polygon::usdc",
"createAt": "2026-01-04 02:52:38",
"metadata": {
"orderId": "12345"
},
// ... other session detail fields
},
// If on-chain transaction is involved, transaction object will be included
"transaction": null
}
}
Security Verification
To ensure fund security, you must verify the signature of all Webhook requests on your server to prevent man-in-the-middle attacks or forged requests.
1. Get Secret Key
After creating a Webhook in the Dashboard, you will receive a Signing Secret starting with wkk_.
2. Extract Headers
GStable requests contain the following two key Headers:
x-gstable-timestamp: The timestamp when the request was sent.x-gstable-signature: The hexadecimal string of the signature.
3. Verification Steps
Step 1: Construct String to Sign
Concatenate the value of x-gstable-timestamp, the character : (colon), and the Raw Request Body.
SignedPayload = Timestamp + ":" + RawBody
Please note that signature concatenation uses a colon (:), not a dot or other characters.
Step 2: Calculate Signature
Use your Signing Secret as the Key to perform HMAC-SHA256 calculation on the above string.
Step 3: Compare
Compare the calculated Hex string with the x-gstable-signature in the Header.
Code Example (Node.js)
Here is a complete example using native Node.js http and crypto modules.
const http = require('http');
const crypto = require('crypto');
// Your Webhook Signing Secret (Get from Dashboard)
const WEBHOOK_SECRET = "wkk_example_secret_key_000";
const server = http.createServer((req, res) => {
if (req.method !== 'POST') {
res.writeHead(404);
return res.end();
}
// 1. Get Headers
const signature = req.headers['x-gstable-signature'];
const timestamp = req.headers['x-gstable-timestamp'];
let rawBody = '';
// 2. Receive Raw Body
req.on('data', chunk => (rawBody += chunk));
req.on('end', () => {
try {
// 3. Construct String to Sign: timestamp + ":" + rawBody
const payload = `${timestamp}:${rawBody}`;
// 4. Calculate HMAC-SHA256
// Standard libraries in most languages natively support sha256
const calculatedSig = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
// 5. Secure Comparison
// Recommend using timingSafeEqual to prevent timing attacks
const sourceBuffer = Buffer.from(signature || '');
const targetBuffer = Buffer.from(calculatedSig);
if (sourceBuffer.length !== targetBuffer.length ||
!crypto.timingSafeEqual(sourceBuffer, targetBuffer)) {
console.error('❌ Invalid signature');
res.writeHead(400);
return res.end(JSON.stringify({ error: 'Invalid signature' }));
}
// 6. Signature Verified, Process Business Logic
const event = JSON.parse(rawBody);
console.log('✅ Verified event:', event.eventType);
// TODO: Handle business logic based on event.eventType (e.g., fulfill order)
} catch (err) {
console.error('Processing error:', err);
res.writeHead(400);
return res.end();
}
// 7. Quickly Return 200 OK
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
});
});
server.listen(13000, () => {
console.log('Webhook receiver listening on port 13000');
});
- Prevent Replay Attacks: After signature verification passes, check if
timestampis within an allowable error range of the current time (e.g., 5 minutes). - Idempotency Handling: Use
eventIdto record processed events. If the sameeventIdis received, return 200 OK directly without executing business logic. - Asynchronous Processing: If business logic takes a long time, it is recommended to return 200 OK first, then execute processing asynchronously to avoid Webhook request timeout causing platform retries.