Verifying webhook signatures
You should use the following headers to verify authenticity of each webhook request.
| Key | Type | Description |
|---|
| X-Subtotal-Signature | string | HMAC of payload using shared secret |
| X-Subtotal-Timestamp | string | The timestamp of the webhook request |
Step 1: Prepare the signed_payload string
Concatenate the following to create the signed_payload string:
- The timestamp (as a string)
- A
. character
- The request body (as a string)
Step 2: Compute the expected_signature
Compute the expected_signature using the HMAC-SHA256 hash function.
Use the webhook destination’s signing_secret as the key, and use the signed_payload string as the message.
The signing_secret is obtained when a new webhook destination is created in the Subtotal Dashboard.
Step 3: Compare the signatures
Compare the expected_signature to the X-Subtotal-Signature.
To prevent replay attacks, compare the received timestamp to the current time and reject requests outside your tolerance window.
To protect against timing attacks, use a constant-time string comparison.
Properties included with every webhook request
These properties are included with each webhook request:
| Key | Type | Description |
|---|
| type | string | The webhook event type |
| id | string | Identifier for the webhook |
| payload | string | The payload of the webhook |
Event types
Below are all supported event types and their cooresponding payload structures.
connection.activated
Connections are activated when a customer successfully links their account.
| Key | Type | Description |
|---|
| payload.connection.connection_id | string | Identifier for the connection |
| payload.connection.customer_id | string | Identifier for the associated customer |
| payload.connection.email | string | Email address of the associated customer |
| payload.connection.mobile | string | Mobile phone number of the associated customer |
| payload.connection.status | string | Current status of the connection |
| payload.connection.retailer_id | string | Identifier for the retailer in the connection |
connection.unauthenticated
Connections move from activated to unauthenticated when a customer needs to re-authenticate their account. For example, when a customer changes their password with the retailer.
| Key | Type | Description |
|---|
| payload.connection.connection_id | string | Identifier for the connection |
| payload.connection.customer_id | string | Identifier for the associated customer |
| payload.connection.email | string | Email address of the associated customer |
| payload.connection.mobile | string | Mobile phone number of the associated customer |
| payload.connection.status | string | Current status of the connection |
| payload.connection.retailer_id | string | Identifier for the retailer in the connection |
purchase.created
The purchase.created event is sent when a new purchase is received from a customer’s connected retailer account.
We recommend using this event when you need to process customer purchases for your use case.
| Key | Type | Description |
|---|
| payload.connection.connection_id | string | Identifier for the connection |
| payload.connection.customer_id | string | Identifier for the associated customer |
| payload.connection.email | string | Email address of the associated customer |
| payload.connection.mobile | string | Mobile phone number of the associated customer |
| payload.connection.status | string | Current status of the connection |
| payload.connection.retailer_id | string | Identifier for retailer in the connection |
| payload.purchase.purchase_id | string | Identifier for the purchase |
| payload.purchase.date | string | Date of the purchase (ISO 8601) |
| payload.purchase.total | number | Total amount of the purchase |
| payload.purchase.subtotal | number | Subtotal before tax |
| payload.purchase.tax | number | Tax amount |
| payload.purchase.items[].name | string | Name of the item |
| payload.purchase.items[].description | string | Description of the item |
| payload.purchase.items[].quantity | number | Quantity |
| payload.purchase.items[].price | number | Price of the item |
| payload.purchase.items[].product_id | string | Identifier for the product |
| payload.purchase.items[].upc | string | Universal identifier for the product purchased (UPC) |
{
"id": "01J51S0JYV6N7K1030CV1ZBDOW",
"type": "purchase.created",
"payload": {
"connection": {
"connection_id": "01J51S0JYV6N7K1030CV1ZKSCA",
"customer_id": "01K8HDK6Y9FXM7ES4NJSHZDEKF",
"retailer_id": "walmart",
"email": "[email protected]",
"mobile": "+123456789",
"status": "active"
},
"purchase": {
"purchase_id": "01J51S0JYV6N7K1030CV1ZKSJH",
"date": "2025-10-08T14:23:00Z",
"total": 47.85,
"subtotal": 43.50,
"tax": 4.35,
"items": [
{
"item_id": "01JV7ZDWC9GN1VPR41K3BY08X9",
"price": 5.99,
"quantity": 2,
"product": {
"product_id": "01J51S0JYV6N7K1030CV1ZKSCA",
"name": "Sparkling Water 12pk",
"description": "Sparkling Water 12-Pack - 12 fl oz bottles",
"upc": "001234567890",
"brand": "la-croix"
}
},
{
"item_id": "01K3KS9Q4522E3HG444E4XH1R0",
"price": 12.99,
"quantity": 1,
"product": {
"product_id": "01J51S0JYV6N7K1030CV1ZKSCA",
"name": "Trail Mix",
"description": "Trail Mix Family Size",
"upc": "009876543210",
"brand": "harvest-one"
}
},
{
"item_id": "01JV7ZDWC9GN1VPR41K3BY08X9"
"price": 18.53,
"quantity": 1,
"product": {
"product_id": "01J51S0JYV6N7K1030CV1ZKSCA",
"name": "Face Cream",
"description": "Moisturizing Face Cream",
"upc": "011122233344",
"brand": "glowww"
}
}
]
}
}
}