Webhooks
How Vantage API delivers order and result updates to your endpoint: event families and payloads, HMAC signature verification, and retry semantics.
Overview
Webhooks are how Vantage API keeps you connected to the kit throughout the fulfilment and results process. Every state change is delivered as a signed HTTP POST to the HTTPS endpoint you registered (Account setup).
There are two event families, distinguished by the event_type field:
event_type
Scope
What it carries
order.status_changed
Order
Fulfilment progress: a status in the same order.* vocabulary REST uses, plus tracking_number once available
order_item.results_status_change
Order item
Results progress: a results_status in the results.* vocabulary, plus the test_taker
Every payload has the same envelope:
{
"event_type": "order.status_changed",
"event_id": "249956485092777984",
"timestamp": 1763661470,
"data": { }
}event_id- unique per event, serialized as a JSON string (like all Vantage IDs). Delivery is at-least-once: deduplicate onevent_id. It equals theX-Terra-Trace-Idheader.timestamp- Unix seconds when the event was sent.
Headers
X-Terra-Signature
HMAC signature with timestamp
t=1763661470,v1=a1b2c3...
X-Terra-Trace-Id
Unique ID for debugging (matches the event_id)
249956485092777984
Content-Type
Always application/json
application/json
Signature verification
Webhooks are signed with HMAC-SHA256 using your Terra signing secret (the same signing secret shown in your Terra dashboard). Verify every delivery before trusting it.
The X-Terra-Signature header has the format:
t- Unix timestamp in seconds when the webhook was sentv1- hex-encodedHMAC-SHA256(signing_secret, "<t>.<raw_body>")
Verification steps:
Parse
tandv1from the header.Reject if
tis outside your tolerance window of now (5 minutes is a sensible default).Concatenate
<t>+.+ the raw, unaltered request body.Compute HMAC-SHA256 over that string with your signing secret.
Compare to
v1with a constant-time comparison.
Compute the HMAC over the raw request body bytes, before any JSON parsing or re-serialization - re-encoding the JSON can reorder keys and change whitespace, which breaks the signature.
Delivery and retries
Respond with any 2xx status quickly (within 10 seconds); do heavy processing asynchronously.
On failure (network error, timeout,
408,429, or any5xx), the first delivery makes up to 5 HTTP attempts with exponential backoff and jitter (roughly 1s, 2s, 4s, 8s). If they all fail, the event is re-queued and redelivered as single attempts with growing delays (starting at 5s, doubling up to a 10-minute cap), up to 10 deliveries in total - at most ~14 calls spread over ~30 minutes - before the event is parked. Make your handler idempotent either way. Other4xxresponses are treated as a rejection and are not retried.Parked (dead-lettered) events can be replayed by Terra - they are not silently lost.
Delivery is at-least-once and ordering is not guaranteed: deduplicate on
event_id, and treat each event's status as the authoritative current state rather than assuming you saw every intermediate step.
You can inspect delivery outcomes (delivered, rejected, dead-lettered, attempt counts, final status codes) via GET /api/v1/webhook-deliveries - see Monitoring and debugging.
Event reference
order.status_changed
data fields: order_id (string), status, plus tracking_number and (sandbox only) supplier_item_id once available.
status
Meaning
order.payment_processing
Payment being processed (initial state of payment-gated orders; appears in the order response rather than as a webhook)
order.payment_complete
Payment confirmed
order.payment_failed
Payment failed
order.processing
Order accepted and being prepared by the supplier
order.delayed
Fulfilment delayed
order.delivery_fulfilled
Delivery details available - carries tracking_number
order.completed
Order complete
order.cancelled
Order cancelled
Example:
order_item.results_status_change
data fields: order_id, order_item_id, variant_id (all strings), results_status, and a test_taker object (test_taker_id string, first_name, last_name, email, phone_number E.164 string). Depending on the status, also: failure_cause, escalation_level, acknowledgment_due_by.
results_status
Meaning
Extra fields
results.kit_activated
End user activated their kit (suppliers with an activation step only)
-
results.sample_processing_in_lab
Lab confirmed receipt of the sample
-
results.partial_results_ready
A subset of the panel has resulted
-
results.results_ready
Results available - fetch and acknowledge
-
results.sample_rejected
Lab rejected the sample
failure_cause (e.g. "blood contaminated")
results.lab_processing_error
Lab could not process the sample
-
results.escalation_raised
Clinical escalation on the result set
escalation_level, acknowledgment_due_by
(results.awaiting_sample is the initial per-item state set at order creation; it appears in the order response rather than as a webhook.)
Example - results ready:
Example - escalation raised:
escalation_level is one of not_escalated, very_low, low, medium, high, very_high. Escalations require prompt acknowledgment - see Acknowledging Results.
Last updated
Was this helpful?