Skip to content
Back to Steward

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.

Terminal window
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"
}
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.

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

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.

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 })

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.

Update or delete webhooks via the API:

Terminal window
# Update events
curl -X PATCH https://api.getsteward.ai/api/v1/webhooks/{id} \
-H "Authorization: Bearer stw_live_..." \
-d '{"events": ["application.submitted"]}'
# Pause
curl -X PATCH https://api.getsteward.ai/api/v1/webhooks/{id} \
-H "Authorization: Bearer stw_live_..." \
-d '{"status": "PAUSED"}'
# Delete
curl -X DELETE https://api.getsteward.ai/api/v1/webhooks/{id} \
-H "Authorization: Bearer stw_live_..."