Introduction
Webhooks allows the client to set a callback URL on which the client will receive a POST request containing event data relating to a specific system event or API request.
An event may be raised as the outcome of a specific operation initiated by a client via Order Management API, and Checkout API.
When a new payment method is added Update Subscription needs to be called , to get the events for the new payment method.
URL requirements
- Webhook URLs must accept HTTPS connections using TLS v1.2 or higher.
- Webhook URLs must accept POST requests.
- Webhook URLs must accept media type “application/json”.
- Webhook URLs must indicate successful reception of event by returning 2xx response.
Request Body
The body of a webhook request contains event data with mandatory fields as well as event specific fields based on the type of operation that caused the event to be raised.
Mandatory fields
| Name | Description | Type |
|---|---|---|
| EventName | The event type, this is used to determine the type of event received. | String |
| CorrelationId | The correlation id provided in the origin request if the event is raised based on the outcome of a client-initiated operation. | Guid |
| TimestampUtc | A timestamp representing the moment in time when the event was raised. | Timestamp |
Delivery Guarantees
- Events might be delivered more than once, so duplicates are possible.
- Events are not guaranteed to be delivered in chronological order.
- Events are not guaranteed to be delivered in scenarios where the receiving webhook endpoint is unavailable.
- If a request does not receive a 2xx response, the system will retry with exponential backoff over a set period until a maximum number of attempts is reached or a successful response is received.
To avoid some failures and to minimize retries Svea recommends a few practices when integrating support for webhook events.
Recommendations
- Since the webhook URL can be invoked more than once for the same event, we recommend that clients make their event processing idempotent. This is to avoid duplicates on the client side.
- We recommend that the client adds the incoming events to a queue for future processing so that a response can be returned to our API as quickly as possible.
- Since event delivery is not guaranteed if the client endpoint is unavailable for the full retry attempt duration, we recommend that data is also synched through GET Order
- A good practice is also to implement support for full order sync when receiving the generic order updated event for an order.
HMAC Signature Verification
When creating or updating a subscription, you can provide an optional Secret field. If a secret is set, every webhook
request sent to your callback URL will include HMAC signature headers that you can use to verify the authenticity of
the request. The secret used for verification must be the same value you provided in the Secret field when you created or last updated the subscription.
Headers
When a secret is configured, the following headers are included in each webhook request:
| Header | Description |
|---|---|
X-Signature-512 | HMAC-SHA512 signature generated from the timestamp and request payload. |
X-Timestamp | Unix timestamp (seconds since epoch) when the request was created. |
Signature Generation
The signature is computed using HMAC-SHA512 with your secret as the key:
- Construct the signed payload:
{timestamp}.{request_body}timestampis the value from theX-Timestampheaderrequest_bodyis the raw JSON body of the request (empty string if no body)
- Compute HMAC-SHA512 using the secret you provided when creating (or updating) the subscription (UTF-8 encoded) as the key and the payload (UTF-8 encoded) as the message.
- The signature is the Base64-encoded representation of the resulting hash bytes.
Signed Payload Format
{timestamp}.{request_body}
Example
Given a timestamp of 1713001200, body {"orderId":123,"status":"confirmed"}, and secret your-secret-key:
The signed payload:
1713001200.{"orderId":123,"status":"confirmed"}
The signature is Base64(HMAC-SHA512(key="your-secret-key", message="1713001200.{\"orderId\":123,\"status\":\"confirmed\"}")).
Verification Examples
- C#
- Node.js
- Python
- PHP
using System.Security.Cryptography;
using System.Text;
public static class WebhookSignatureVerifier
{
public static bool VerifySignature(string requestBody, string secret, string receivedSignature, string timestampHeader)
{
var payload = $"{timestampHeader}.{requestBody}";
var secretBytes = Encoding.UTF8.GetBytes(secret);
var messageBytes = Encoding.UTF8.GetBytes(payload);
using var hmac = new HMACSHA512(secretBytes);
var hashBytes = hmac.ComputeHash(messageBytes);
var computedSignature = Convert.ToBase64String(hashBytes);
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(computedSignature),
Encoding.UTF8.GetBytes(receivedSignature));
}
}
const crypto = require('crypto');
function verifySignature(requestBody, secret, receivedSignature, timestampHeader, maxAgeSeconds = 300) {
// Reject old timestamps to guard against replay attacks
const timestamp = parseInt(timestampHeader, 10);
if (Math.abs(Date.now() / 1000 - timestamp) > maxAgeSeconds) {
return false;
}
const payload = `${timestampHeader}.${requestBody}`;
const computedSignature = crypto
.createHmac('sha512', secret)
.update(payload, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(computedSignature, 'utf8'),
Buffer.from(receivedSignature, 'utf8')
);
}
import hmac
import hashlib
import base64
import time
def verify_signature(request_body: str, secret: str, received_signature: str, timestamp_header: str, max_age_seconds: int = 300) -> bool:
# Reject old timestamps to guard against replay attacks
timestamp = int(timestamp_header)
if abs(time.time() - timestamp) > max_age_seconds:
return False
payload = f"{timestamp_header}.{request_body}"
hash_bytes = hmac.new(
secret.encode("utf-8"),
payload.encode("utf-8"),
hashlib.sha512
).digest()
computed_signature = base64.b64encode(hash_bytes).decode("utf-8")
return hmac.compare_digest(computed_signature, received_signature)
function verifySignature(string $requestBody, string $secret, string $receivedSignature, string $timestampHeader, int $maxAgeSeconds = 300): bool
{
// Reject old timestamps to guard against replay attacks
$timestamp = intval($timestampHeader);
if (abs(time() - $timestamp) > $maxAgeSeconds) {
return false;
}
$payload = "{$timestampHeader}.{$requestBody}";
$computedSignature = base64_encode(hash_hmac('sha512', $payload, $secret, true));
return hash_equals($computedSignature, $receivedSignature);
}
Recommendations
- Always verify the
X-Signature-512header before processing the webhook payload. - Use the
X-Timestampheader to reject requests that are too old (e.g., older than 5 minutes) to guard against replay attacks. - Use a constant-time comparison function (as shown in the examples) to prevent timing attacks.
- Store your secret securely and never expose it in client-side code. This is the same
Secretvalue you provided when creating or updating the subscription.