Skip to main content

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

FieldTypeDescription
eventIdStringGlobal unique identifier for the event (e.g., evt_i4NWz...). Recommended for idempotency handling (deduplication).
eventTypeStringSpecific event type (e.g., session.created, session.paid).
businessTypeStringBusiness category type. Currently includes: session, payment_link, product, account.
occurrenceStringBusiness time when the event occurred (Format: YYYY-MM-DD HH:mm:ss).
isSubscribableBooleanWhether this event supports subscription.
payloadObjectBusiness 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
Note Separator

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');
});
Best Practices
  1. Prevent Replay Attacks: After signature verification passes, check if timestamp is within an allowable error range of the current time (e.g., 5 minutes).
  2. Idempotency Handling: Use eventId to record processed events. If the same eventId is received, return 200 OK directly without executing business logic.
  3. 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.