Webhooks Guide
Webhooks let you receive real-time notifications when events occur in your Steward account. Instead of polling the API, Steward sends HTTP POST requests to your configured URL.
Creating a Webhook
Section titled “Creating a Webhook”curl -X POST https://api.getsteward.ai/api/v1/webhooks \ -H "Authorization: Bearer stw_live_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/steward", "events": ["application.submitted", "application.approved"] }'The response includes a signing secret - store it securely. It is only returned once.
{ "id": "507f1f77bcf86cd799439011", "url": "https://example.com/webhooks/steward", "events": ["application.submitted", "application.approved"], "secret": "whsec_abc123...", "status": "ACTIVE"}Available Events
Section titled “Available Events”| Event | Description |
|---|---|
application.created |
A new application was created |
application.onboarding_started |
Applicant began the onboarding flow |
application.submitted |
Application was submitted for review |
application.updated |
An application’s details were updated |
application.approved |
Application was approved |
application.rejected |
Application was rejected |
screening.hits_created |
Screening produced one or more new hits |
screening.enrichment_completed |
Enrichment of a screening hit finished |
screening.hit_updated |
A screening hit status was changed |
document.uploaded |
A document was uploaded |
risk_score.created |
Risk score was calculated |
risk_score.updated |
Risk score was recalculated |
reviewer.assigned |
A reviewer was assigned to an application |
reviewer.unassigned |
A reviewer was removed from an application |
note.created |
A note was added |
note.updated |
A note was modified |
note.deleted |
A note was deleted |
export.completed |
A bulk export artifact is ready |
While an application is with Steward (withSteward on the application), customer deliveries for that application are held. On release they are sent in the order they happened, each describing the application as released. A subscription created after an event is not given that delivery. Events that only record Steward’s own changes are not sent. There is no separate internal-review webhook event.
This table is the complete set. Subscribing to any other value (including one you may have seen in an older version of these docs) is rejected with a 400.
Payload Format
Section titled “Payload Format”Each webhook delivery includes:
{ "event": "application.submitted", "timestamp": "2026-03-30T15:00:00.000Z", "data": { "entityType": "APPLICATION", "entityId": "507f1f77bcf86cd799439011", "accountId": "507f1f77bcf86cd799439012", "applicationId": "507f1f77bcf86cd799439011" }}| Field | Description |
|---|---|
event |
The event type, one of the values above. |
timestamp |
When the delivery was created. |
data.entityType |
The kind of entity the event is about: an application, screening hit, document, or note. |
data.entityId |
The id of that entity. For an application-level event it is the application’s own id. |
data.accountId |
Your account id. |
data.applicationId |
The application the event belongs to. Present whenever the event resolves to one. Use it to route deliveries. |
data.metadata |
Present only on events that carry extra context. Treat it as an open object. |
data.applicationId and data.metadata are omitted rather than sent as null when they do not apply, so read them defensively.
For export.completed, data.entityType is EXPORT and data.entityId is the export job id. data.metadata contains jobId, format, status, totalCount, completedCount, failedApplicationIds, statusPath, and downloadPath. The paths require the same authenticated API-key user that created the job; call downloadPath to mint a fresh 7-day signed URL. A replay of the same completed job keeps the same delivery identity, but webhook transport is not an exactly-once guarantee.
Headers:
| Header | Description |
|---|---|
X-Webhook-Signature |
HMAC-SHA256 signature of the request body |
X-Webhook-Event |
Event type (e.g., application.submitted) |
X-Webhook-Delivery-ID |
Unique delivery ID for idempotency (stable on retries) |
Content-Type |
application/json |
Verifying Signatures
Section titled “Verifying Signatures”Verify the X-Webhook-Signature header to ensure the request came from Steward.
The signature is a hex-encoded HMAC-SHA256 of the raw request body. Compute it over the bytes you received, before any JSON parsing and re-serialisation, because re-encoding the body can change it and will break the comparison.
import { createHmac, timingSafeEqual } from 'crypto'
function verifyWebhookSignature(rawBody, signature, secret) { const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
// Compare in constant time so a mismatch cannot be found byte by byte. timingSafeEqual // throws on a length mismatch, so check that first. if (signature.length !== expected.length) return false
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))}Warning: Always verify webhook signatures before processing payloads. Without verification, an attacker could send fake events to your endpoint.
Idempotency
Section titled “Idempotency”Each delivery includes an X-Webhook-Delivery-ID header that is unique per delivery and remains the same across retries. Use this ID to deduplicate events and ensure your handler processes each event only once.
const deliveryId = request.headers['x-webhook-delivery-id']const alreadyProcessed = await cache.get(`webhook:${deliveryId}`)
if (alreadyProcessed) { return res.status(200).send('OK')}
// Process the event...
await cache.set(`webhook:${deliveryId}`, true, { ttl: 300 })Retry Policy
Section titled “Retry Policy”Failed deliveries are retried up to 5 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | 30 seconds |
| 2 | 2 minutes |
| 3 | 10 minutes |
| 4 | 1 hour |
| 5 | 4 hours |
After all retries are exhausted, the delivery is marked as failed. Use the deliveries endpoint to check delivery status.
Managing Webhooks
Section titled “Managing Webhooks”Update or delete webhooks via the API:
# Update eventscurl -X PATCH https://api.getsteward.ai/api/v1/webhooks/{id} \ -H "Authorization: Bearer stw_live_..." \ -d '{"events": ["application.submitted"]}'
# Pausecurl -X PATCH https://api.getsteward.ai/api/v1/webhooks/{id} \ -H "Authorization: Bearer stw_live_..." \ -d '{"status": "PAUSED"}'
# Deletecurl -X DELETE https://api.getsteward.ai/api/v1/webhooks/{id} \ -H "Authorization: Bearer stw_live_..."