Webhooks
Delivery contract, signature verification, retries, and testing for Steve webhooks.
Steve uses webhooks for asynchronous session and submission updates.
There are two related pieces:
POST /api/v1/webhooks/testfor reachability checks- production deliveries for sessions created with a
webhookUrl
New event types and payload fields can appear at any time. Ignore what you do not recognize and acknowledge it with a 2xx response; see Compatibility.
Connectivity test
Use the test endpoint before turning on production deliveries:
Steve performs a direct POST with this payload:
Test-delivery behavior:
- timeout: 5 seconds
- content type:
application/json - user agent:
steve-webhook/1.0 - signature header: not present on test requests
Production delivery headers
Production deliveries include:
| Header | Meaning |
|---|---|
Content-Type: application/json | JSON payload |
X-Webhook-Signature | HMAC-SHA256 signature |
X-Webhook-Event | Event type string |
X-Webhook-Id | Stable delivery id across retries |
X-Webhook-Timestamp | Unix seconds when the attempt was sent |
User-Agent: steve-webhook/1.0 | Sender identifier |
Production payload shape
Steve sends one generic envelope for both session-completion and later submission-action events:
Batch completion uses a smaller aggregate payload:
It is sent to the webhookUrl supplied on the first session in the batch and
uses the same signing headers and retry schedule as session webhooks.
How to read the key fields:
eventIdis the stable event identifier that also appears inX-Webhook-Id- initial processing uses
evt_<sessionId>_<status>. An explicit REST process-anyway rerun adds_g<generation>(for example,evt_<sessionId>_pending_review_g1orevt_<sessionId>_failed_g2). Each run has its own event ID, including repeated failures; delivery retries keep that run's ID. Deduplicate on the complete ID. Admin process-anyway does not reopen a failed API job or send a new session webhook. eventTypeis the primary routing fieldoccurredAtis the business timestamp for the event itselfstateis the session lifecycle statesubmissionStatusis the public business outcome when Steve can determine itresultis the workflow-specific processing payload:extractedData,confidence,fraudStatusandmatchedItemsresult.matchedItemslists the receipt lines Steve matched to your product catalog, with the catalog SKU Open Loyalty also receives. It is[]when the workflow does not match products, and when matching was skipped. Asubmission.approvedevent can list fewer lines than thesession.review_requiredevent before it when the product catalog was switched off in between. See Product matching
Steve builds the payload once, when it creates the event. Every retry of one eventId sends the same bytes, so a reviewer edit made between two attempts does not change what a retry delivers. A later change reaches you as a new event with its own eventId.
clientSubmissionIdandmetadataare echoed from session creation
Event types
Steve currently emits these event types:
| Event type | Fired when |
|---|---|
session.completed | Processing finished and no review is required |
session.failed | Processing failed |
session.review_required | Processing finished and a reviewer must decide the outcome |
submission.approved | A submission was approved |
submission.rejected | A submission was rejected |
submission.cancelled | A submission was cancelled |
submission.fraud_match_resolved | A fraud match was resolved |
batch.completed | Every known session in a batch is processed and the newest session is at least five minutes old |
Two important nuances:
- Session lifecycle routing belongs to
eventType, not to a separate internal status field. - Use
submissionStatusto distinguishreviewfromapproved,synced,rejected,cancelled, orfailed. - submission action events reuse the same envelope and stay intentionally small. If you need the latest reasons, fraud state, or file links, refetch
GET /api/v1/submissions/{submissionId}. submission.approvedandsubmission.rejectedalso fire when a reviewer approves or rejects an API submission in the Steve admin, not only through the API.batch.completeduses the aggregate payload shown above. A pause longer than five minutes during upload can complete the batch before later sessions arrive; later sessions update counts but do not emit another completion event.
Signature verification
Production deliveries include:
Steve signs the raw request body using HMAC-SHA256 with this secret:
That means your receiver must first hash the API key to a hex string and then use that hex string as the HMAC key. The private TypeScript SDK exports deriveWebhookSecret, signWebhookPayload, and verifyWebhookSignature. When the SDK is available, pass deriveWebhookSecret(apiKey)'s result as the secret argument to verifyWebhookSignature.
Deduplication and replay protection
Use the headers together:
- store
X-Webhook-Idto deduplicate retries - reject stale deliveries using
X-Webhook-Timestamp
Typical replay window:
Retry behavior
Steve gives each delivery attempt 10 seconds to complete. If the target does not return 2xx in time:
- the initial attempt fails
- retry 1 is scheduled 30 seconds later
- retry 2 is scheduled 5 minutes later
- retry 3 is scheduled 30 minutes later
- after the third retry fails, delivery status becomes
failed
Retries are counted per event. A receiver that fails one event does not use up the retries of the next one. webhookDeliveryStatus on GET /api/v1/jobs/{sessionId} reports the session's most recent event.
Make your handler idempotent and return quickly.
Webhook URL validation
Steve applies syntactic validation to require HTTPS webhook targets and reject known local destinations. It rejects URLs that:
- are not valid absolute URLs
- do not use
https:// - target
localhostor127.0.0.1 - target RFC1918 private ranges
- end in
.internalor.local
Steve does not resolve DNS during validation. A public-looking hostname can therefore pass this check even if it later resolves to a private address.
Recommended receiver pattern
- verify
X-Webhook-Signature - validate
X-Webhook-Timestamp - deduplicate on
X-Webhook-Id - route on
eventType - use
stateandsubmissionStatusfor lightweight routing, then refetch the submission or job if you need the latest full state - return
200as soon as the event is durably recorded
For multi-page PDFs, result.pageEvidence may contain { complete, pageCount, blockers, fields }. Fields are keyed by schema name with page (or null), optional per-item pages, flagged, and an optional reason. Raw independent-read values are not included. The same shape is cached in the job result; retries use the event's original immutable body. Treat a flagged field or any document blocker as requiring review.