Advanced Webhooks#
Advanced webhook configuration: retries, signatures, event filtering and delivery history.
All endpoints below are admin API routes: authenticate with Authorization: Bearer <JWT> (or an API key in x-api-key). A merchant-scoped caller can only see and manage its own merchant's endpoints.
Webhook Retry Configuration#
Retries are configured per endpoint with two fields, set on create or update:
| Field | Type | Default | Description |
|---|---|---|---|
maxRetries |
integer | 3 |
Total delivery attempts, including the first one |
retryDelaySeconds |
integer | 60 |
Base delay; doubles after each failed attempt |
Endpoint#
curl -X PATCH https://uat.auxcore.net/api/v1/webhooks/endpoints/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"maxRetries": 5,
"retryDelaySeconds": 120
}'Other updatable fields: url, description, events, isActive.
How retries work#
- A delivery succeeds only on an HTTP
2xxresponse within 30 seconds. Anything else (non-2xx, timeout, connection error) is a failure. - After failed attempt n, if n <
maxRetries, the delivery is set toretryingwith the next attempt atretryDelaySeconds × 2^(n-1)(defaults: 60 s, then 120 s). - A background sweep picks up due retries about once a minute, up to 100 per sweep, so actual retry times can lag by up to a minute.
- When attempts are exhausted the delivery stays
failed. A delivery for an endpoint that has since been deactivated or deleted is markedfailedwithout being sent.
Webhook Signatures#
Every delivery is signed with the endpoint's secret (HMAC-SHA256).
Headers sent#
| Header | Value |
|---|---|
X-Webhook-Signature |
t=<unix seconds>,v1=<hex HMAC-SHA256> |
X-Webhook-Event-Type |
Event name, e.g. transaction.approved |
X-Webhook-Event-ID |
Event id (same for every retry of the event) |
X-Webhook-Delivery-ID |
Delivery id |
User-Agent |
AuxVault-Webhooks/1.0 |
Content-Type |
application/json |
Verifying#
The signed string is <t>.<raw request body>. Compute HMAC-SHA256 over it with your secret and compare (constant-time) to v1. Reject timestamps more than 5 minutes from your clock to prevent replays. Always verify against the raw body bytes, not a re-serialised parsed object.
const crypto = require('crypto');
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`, 'utf8').digest('hex');
return crypto.timingSafeEqual(Buffer.from(parts.v1, 'hex'), Buffer.from(expected, 'hex'));
}Payload envelope#
{
"id": "0b7f5c2e-3d1a-4f6b-9e8c-1a2b3c4d5e6f",
"type": "transaction.approved",
"created": "2026-01-28T15:00:00.000Z",
"data": { "...": "event-specific fields" },
"metadata": {
"tenantId": "your-tenant-id",
"merchantId": "3f2a9c1e-7b4d-4e8a-9f10-2c5d6e7f8a90"
}
}Secret handling#
The signing secret is returned only in the create response and by regenerate-secret. List/get/update responses mask it (hasSecret, secretHint with the last 4 characters).
Response: { "success": true, "data": { "secret": "…" } } — the old secret stops working immediately.
Webhook Filtering#
Subscribe to specific events by listing them in events. Names must match exactly — wildcards (transaction.*) and payload filters (e.g. amount thresholds) are not supported.
Endpoint#
curl -X POST https://uat.auxcore.net/api/v1/webhooks/endpoints \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhook",
"description": "Order system",
"events": ["transaction.approved", "transaction.declined", "transaction.refunded"],
"maxRetries": 3,
"retryDelaySeconds": 60
}'| Field | Required | Description |
|---|---|---|
url |
Yes | Must be a valid URL |
events |
Yes | Array of event names (below) |
description |
No | Free text |
merchantId |
No | Only for tenant-level callers; a merchant-scoped caller always gets its own merchant |
maxRetries |
No | Default 3 |
retryDelaySeconds |
No | Default 60 |
Response (201): the endpoint plus secret, and "message": "Store the signing secret now — it cannot be retrieved again."
Event types#
| Event | When |
|---|---|
transaction.approved |
Payment approved |
transaction.pending |
Payment accepted but not yet settled (e.g. ACH) |
transaction.declined |
Payment declined |
transaction.voided |
Transaction voided |
transaction.refunded |
Refund processed |
transaction.refund_failed |
Refund failed |
transaction.returned |
ACH debit returned by the bank (data.returnCode carries the R-code) |
session.cancelled |
Hosted checkout session cancelled |
session.expired |
Hosted checkout session expired |
batch.resubmit is a platform event delivered only to system endpoints and cannot be subscribed to from a merchant endpoint. Unknown names are rejected with 400 UNKNOWN_EVENT_TYPE (the message lists every invalid name and the valid set). See Webhook Events for payloads.
Errors#
| Status | Code | When |
|---|---|---|
400 |
VALIDATION_ERROR |
url missing, or events missing or not an array |
400 |
UNKNOWN_EVENT_TYPE |
An event name is not in the list above |
400 |
INVALID_URL |
url is not a valid URL |
404 |
ENDPOINT_NOT_FOUND |
Endpoint not found (or not yours) |
Testing an Endpoint#
Sends a signed webhook.test event to the endpoint and returns the delivery result:
{
"success": true,
"data": {
"success": true,
"deliveryId": "5f0c1d2e-8a9b-4c3d-9e1f-2a3b4c5d6e7f",
"statusCode": 200,
"responseTime": 184
}
}A failed delivery still returns 200 with data.success: false and an error message.
Webhook Logs#
View delivery history for an endpoint, newest first.
| Query | Default | Description |
|---|---|---|
limit |
50 |
Page size |
offset |
0 |
Rows to skip |
curl "https://uat.auxcore.net/api/v1/webhooks/endpoints/7c9e6679-7425-40de-944b-e07fc1f90ae7/deliveries?limit=20" \
-H "Authorization: Bearer YOUR_TOKEN"Response:
{
"success": true,
"data": {
"deliveries": [
{
"id": "5f0c1d2e-8a9b-4c3d-9e1f-2a3b4c5d6e7f",
"eventType": "transaction.approved",
"eventId": "0b7f5c2e-3d1a-4f6b-9e8c-1a2b3c4d5e6f",
"attemptNumber": 2,
"status": "retrying",
"responseStatusCode": 503,
"errorMessage": "HTTP 503: Service Unavailable",
"responseTimeMs": 412,
"nextRetryAt": "2026-01-28T15:03:00.000Z",
"createdAt": "2026-01-28T15:00:00.000Z"
}
],
"pagination": { "total": 1, "limit": 20, "offset": 0, "hasMore": false }
}
}Delivery status is one of pending, success, failed, retrying.
Retry a delivery manually#
Re-sends the delivery immediately (incrementing attemptNumber) and returns the same result shape as the test endpoint. The retry does not check whether the endpoint is still active, and it is counted as an attempt (if it fails and the attempts are already used up, no further automatic retry is scheduled). An unknown delivery, or one belonging to another tenant or merchant, returns 404 NOT_FOUND.
Need help? Contact support@auxcore.net