Webhooks
Delivery mechanism, request headers, event structure, and signature verification.
Stabo pushes event notifications to your server through Webhooks (for example, when a payment is received). You learn about account changes in real time without polling.
Every Webhook request includes an HMAC-SHA256 signature. You must verify the signature to confirm that the request comes from Stabo and to prevent forgery. See Signature verification below.
Delivery mechanism
- Protocol: HTTPS (TLS 1.2+)
- Method: HTTP POST
- URL: The Webhook URL that you configure in the Dashboard under Integrations → Webhooks (must use HTTPS)
- Content type: application/json
- Signature algorithm: HMAC-SHA256
- Retry policy: If the response status code is not 2xx, Stabo retries with exponential backoff, up to 5 times
- Retry intervals: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours
- Timeout: 30 seconds (respond within 30 seconds)
Best practice: When you receive a Webhook notification, return a 2xx response first and process your business logic asynchronously. This prevents processing timeouts from causing duplicate deliveries.
Request headers
Every Webhook request that Stabo sends includes the following HTTP headers:
| Header | Description | Example |
|---|---|---|
Content-Type | Request content type | application/json |
Webhook-Event-Id | Unique event ID | evt_1234567890 |
Webhook-Event-Type | Event type | checkout.payment_intent.succeeded |
Webhook-Signature | Signature (used to verify that the request is authentic) | t=1678888888,v1=a3f7c8b9... |
Event object
{
"id": "evt_1Mxxx",
"object": "event",
"type": "checkout.payment_intent.succeeded",
"created": 1678888888,
"data": {
"object": {}
}
}Fields:
id: Unique event identifierobject: Fixed value"event"type: Event type (for example,checkout.payment_intent.succeeded)created: Event creation timestamp (seconds)data.object: Event payload (varies by event type)
For the event types and payloads of each product, see the product guide, for example Checkout Webhook events.
Signature verification
To make sure that a Webhook request comes from Stabo and is not forged by a third party, you must verify the signature of every request.
The format of Webhook-Signature is t=timestamp,v1=signature. To verify it:
- Parse the header to get
t(timestamp, in seconds) andv1(signature). - Check the timestamp and reject expired requests. You choose the tolerance. The examples below use 300 seconds.
- Build the signed payload:
eventId + "." + timestamp + "." + rawBody.eventIdcomes fromWebhook-Event-Id.rawBodyis the raw, unparsed request body. - Compute HMAC-SHA256 of the signed payload with your Webhook secret, and encode the result as a lowercase hexadecimal string.
- Compare the expected signature with
v1in constant time.
You must compute the signature from the raw request body. Do not parse the JSON and serialize it again. Any change in field order or whitespace makes the signature mismatch. Use the secret as the UTF-8 bytes of the string. Do not Base64-decode it.
Verification code examples
Compute the expected signature manually with openssl. This is useful for troubleshooting signature mismatches during integration testing.
# Variables from the request: EVENT_ID=Webhook-Event-Id, TIMESTAMP=t in Webhook-Signature
# RAW_BODY is the raw request body, WEBHOOK_SECRET is the Webhook secret
printf '%s' "${EVENT_ID}.${TIMESTAMP}.${RAW_BODY}" \
| openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" \
| sed 's/^.* //'The output must match v1 in Webhook-Signature.
Test with a fixed sample
Use the sample below for self-testing. The secret is an example value. Pass the sample to any of the implementations above. The result must be true. When you verify, set the current time to within 300 seconds of t.
| Item | Value |
|---|---|
| Secret | c2VjcmV0LWZvci1kb2NzLXRlc3Qtb25seS0wMDAwMDAwMDAwMA== |
Webhook-Event-Id | evt_1234567890 |
Webhook-Signature | t=1778480710,v1=86d39077e56f6d8b745f2aaff8f52af99e4219b978cdb4269884082745d7bc63 |
Raw request body (single line, no trailing newline):
{"id":"evt_1234567890","object":"event","type":"checkout.payment_intent.succeeded","created":1778480710,"data":{"object":{"paymentIntentId":"pi_MNOL0POUV8IDPXCD","status":"COMPLETED","amount":"20.00","currency":"USD","note":"测试 UTF-8"}}}If you change any character in the request body, or change v1 to uppercase, verification must return false.