Skip to navigation

Webhooks

Get a signed message when a quote request or a quote changes, and recover anything you miss from the event feed.
View as Markdown

Quote requests run in the background, so you need a way to hear when one finishes. Instead of polling, you can subscribe a URL to the event types you care about, and Truvo sends a signed POST to it each time one of them happens. Every event is also in the event feed for 30 days, so a missed delivery is never lost.

Subscribe

Create a subscription with the URL that receives events and the exact event types you want. There’s no wildcard, so list each type:

curl https://api.truvo.com/v1/event-subscriptions \
-H "Authorization: Bearer $TRUVO_API_KEY" \
-H "Idempotency-Key: 5f0c7d8e-2b1a-4c3d-9e8f-0a1b2c3d4e5f" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/truvo",
"event_types": ["quote_request.completed", "quote_request.failed"]
}'

The answer is the subscription, with its signing secret:

{
"id": "sub_...",
"environment": "sandbox",
"url": "https://hooks.example.com/truvo",
"status": "active",
"event_types": ["quote_request.completed", "quote_request.failed"],
"signing_secret": "whsec_..."
}

Store the signing_secret right away. It’s shown once: after a create succeeds, a retry with the same request key returns the subscription with signing_secret: null. If you lose the secret, rotate it.

If a create answers 503, send it again with the same request key, and it finishes with the same secret. A create left unfinished for 10 minutes is dropped. A retry of its request key then gets 409 with the retry action use_new_request_key, so send the create again with a new key.

A few rules apply to every subscription:

  • Only a server key can manage subscriptions. A widget key gets 403.
  • The URL must use https and name a public host. A URL with a user name or a password, an IP address, localhost, or a .local or .internal host gets 422. A port is fine.
  • An unknown event type gets 422 at that entry of event_types.
  • Each integration can hold 10 active subscriptions in each environment.
  • A sandbox key sees sandbox events, and a live key sees live events.

To change the URL or the event types, send PATCH /v1/event-subscriptions/{id}. To stop deliveries, send DELETE /v1/event-subscriptions/{id}, which disables the subscription. GET /v1/event-subscriptions lists your subscriptions, 10 to a page by default.

Rotate the signing secret

To rotate (roll) the secret, send POST /v1/event-subscriptions/{id}/rotate with a new request key. The answer carries the new secret once, and previous_secret_expires_at says when the old secret stops working: 24 hours after the rotation. Until then, each delivery carries a signature made with each secret, so your endpoint accepts it with either one. Deploy the new secret before the old one expires. Only one rotation of a subscription can be in progress, so a second one gets 409 until the first finishes. A rotation left unfinished blocks new ones for up to 10 minutes.

Verify each delivery

Anyone can send a request to your URL, so check the signature before you act on a delivery. Truvo sends webhooks through Svix, and each delivery carries Svix’s standard signature headers:

HeaderHolds
svix-idThe ID of the delivered message. It stays the same on every retry of that message.
svix-timestampWhen the message was signed, in Unix seconds.
svix-signatureOne or more signatures, separated by spaces. Accept the message if any one of them matches.

The signature is an HMAC-SHA256 over {svix-id}.{svix-timestamp}.{body}. Its key is the part of your signing secret after whsec_, decoded from base64. Each entry in svix-signature is v1, followed by a signature in base64.

The signature follows the Standard Webhooks scheme. The svix libraries accept these headers directly. Any other Standard Webhooks library needs them renamed to webhook-id, webhook-timestamp, and webhook-signature, with the same values. The svix package on npm is the simplest:

import { Webhook } from "svix";
const webhook = new Webhook(process.env.TRUVO_WEBHOOK_SECRET!);
// rawBody is the request body exactly as it arrived, before any JSON parsing.
export function verifiedEvent(rawBody: string, headers: Record<string, string>) {
return webhook.verify(rawBody, headers) as { id: string; type: string };
}

verify throws when no signature matches, or when the timestamp is more than 5 minutes away from your clock. Answer 400 in that case and do nothing else. Verify against the raw body: a body that you parsed and serialized again won’t match.

Handle retries and duplicates

Answer with a 2xx status within 15 seconds to confirm a delivery. Any other answer, a redirect included, counts as a failure, and the message is sent again. The first delivery goes out right away. After a failure, the message is sent again after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, and 10 hours.

If every delivery to your URL fails for 5 days, delivery to that subscription turns off. The subscription still reads active, and its health reads disabled. To turn delivery back on, send PATCH /v1/event-subscriptions/{id} with your URL, then recover the missed events from the feed.

Because of retries, the same event can arrive more than once, and events can arrive out of order. Handle both:

  • Store the id of each event you processed, and skip an id you’ve already seen.
  • Answer 2xx first, and do the work afterward. A slow handler turns into retries.
  • Compare resource.version with the version you hold. Read the resource only when the event’s version is higher, and never apply an older version over a newer one.

An event never repeats an insurance action. Reading a resource because of an event is always safe.

Read the resource

Events are thin on purpose. An event says which resource changed and which version it reached, and it carries no applicant data:

{
"id": "evt_...",
"type": "quote_request.completed",
"schema_version": "2026-09-26",
"occurred_at": "2026-09-28T17:04:11.482Z",
"environment": "sandbox",
"line": "personal_auto",
"resource": {
"type": "quote_request",
"id": "qreq_...",
"version": 4
}
}

To see what changed, read the resource. A quote_request event points to GET /v1/quote-requests/{id}, and a quote event points to GET /v1/quotes/{id}. The read gives you the current state, which may already be newer than the event.

Truvo can add event types and fields without notice. Ignore a type or a field that you don’t know, and still answer 2xx.

Event types

TypeSent when
quote_request.queuedTruvo accepts a new quote request.
quote_request.updatedA quote request starts, gets its first quotes, or changes in another way before it reaches a final status.
quote_request.completedCollection ends with results. A request with zero quotes can still complete.
quote_request.failedThe request can’t start, or no carrier answers by the deadline.
quote_request.cancelledYou cancel a request while it’s still queued.
quote.createdTruvo saves a new quote.
quote.updatedA quote changes in a way other than expiring.
quote.expiredA quote reaches its expiry time. The expiry changes the quote.
purchase.updatedA purchase is created, or changes without a more specific event.
purchase.requires_actionA purchase needs an action from the customer, or that action changes.
purchase.completedA purchase is complete, with a verified policy.
purchase.needs_reviewA person at Truvo needs to review a purchase.
purchase.failedA purchase fails with no payment or bind left unresolved.

Purchases aren’t in the API yet, so the five purchase types don’t fire yet. A pet quote request sends only quote_request events.

Recover missed events

The event feed holds every event of the last 30 days, of every type, whether or not you subscribed to it. Each event has the same id in the feed as in a webhook, so you can use one handler for both. Read the feed after an outage, after you turn a subscription back on, or on a schedule as a safety net:

curl "https://api.truvo.com/v1/events?cursor=$TRUVO_EVENT_CURSOR&limit=100" \
-H "Authorization: Bearer $TRUVO_API_KEY"

The answer has events and a next_cursor. Every page has a next_cursor, an empty one included. Process the events, store the next_cursor only after that work is saved, and send it on the next read. When a page comes back empty, you’ve caught up.

  • Leave out cursor to start at the oldest event the feed still holds.
  • limit is 10 by default and 100 at most.
  • A cursor that the feed didn’t give you gets 400 with the code invalid_cursor.
  • A cursor gets 410 with the code event_cursor_expired when events after it are gone: the feed deleted them after 30 days, or a sandbox reset removed them. Read each resource that you track, then read the feed again without a cursor.

Test your endpoint

Send a test event to check that your endpoint gets deliveries and verifies their signatures:

curl -X POST https://api.truvo.com/v1/event-subscriptions/sub_.../test \
-H "Authorization: Bearer $TRUVO_API_KEY" \
-H "Idempotency-Key: 9b2e4f60-1c7d-4a8e-b3f5-6d0e2a9c8b71"

The answer is 202, with the event that Truvo queued:

{
"subscription_id": "sub_...",
"event": {
"id": "evt_test...",
"type": "webhook.test",
"schema_version": "2026-09-26",
"occurred_at": "2026-09-28T17:04:11.482Z",
"environment": "sandbox",
"subscription_id": "sub_..."
}
}

A test event goes only to that subscription, whatever event types it lists. It names no resource and carries no applicant data, and it isn’t in the event feed. Answer it with 2xx, as you answer any event. To retry after an error, send the same request key: it queues the same event, with the same id.

A test can take up to 35 seconds to answer, so set your client’s timeout to 40 seconds or more. If it answers 503, wait for the Retry-After seconds and send it again with the same request key.

A test to a disabled subscription gets 409. So does a test while delivery to the subscription is off: check its health.

See each delivery

GET /v1/event-subscriptions/{id}/deliveries lists the events sent to a subscription in the last 30 days, newest first. The log shows every delivery sent to this subscription, test events included:

{
"deliveries": [
{
"event_id": "evt_...",
"event_type": "quote_request.completed",
"status": "retrying",
"created_at": "2026-09-28T17:04:11.482Z",
"next_attempt_at": "2026-09-28T17:09:12.000Z"
}
],
"next_cursor": null
}
statusMeans
pendingThe first attempt hasn’t run yet.
retryingAn attempt failed. The next one runs at next_attempt_at.
succeededYour endpoint answered 2xx.
failedEvery retry failed.
cancelledRetries stopped because delivery to the subscription turned off.

created_at is when Truvo sent the event to delivery. limit is 10 by default and 100 at most. Send the next_cursor of a page as cursor to read the next page. A page can come back empty with a next_cursor, for example after a sandbox reset deletes events, so keep reading until next_cursor is null. A cursor that this subscription’s log didn’t give you gets 400 with the code invalid_cursor.

To see the attempts of one delivery, read GET /v1/event-subscriptions/{id}/deliveries/{event_id}. The answer is the same delivery, with up to 20 attempts, newest first:

{
"event_id": "evt_...",
"event_type": "quote_request.completed",
"status": "retrying",
"created_at": "2026-09-28T17:04:11.482Z",
"next_attempt_at": "2026-09-28T17:09:12.000Z",
"attempts": [
{
"attempted_at": "2026-09-28T17:04:12.103Z",
"status": "failed",
"response_status_code": 500,
"response_duration_ms": 212,
"trigger": "scheduled"
}
]
}

response_status_code is null when your endpoint gave no HTTP answer, as after a timeout. trigger is manual for a test or a replay, and scheduled for the first attempt and each retry.

Replay a delivery

After you fix your endpoint, send an event from the delivery log again:

curl -X POST https://api.truvo.com/v1/event-subscriptions/sub_.../replay \
-H "Authorization: Bearer $TRUVO_API_KEY" \
-H "Idempotency-Key: 4c1d8e2a-7b3f-4e9a-a6c5-0f2b9d1e3a57" \
-H "Content-Type: application/json" \
-d '{ "event_id": "evt_..." }'

The answer is 202. The event arrives again with the same id and the same body, so your duplicate check still applies. You can replay only an event in the subscription’s delivery log. Any other event ID gets 404. Use a new request key for each replay, and the same key to retry after an error. To catch up on many events, read the event feed.

Check delivery health

GET /v1/event-subscriptions/{id}/health says whether deliveries get through:

{
"subscription_id": "sub_...",
"state": "failing",
"last_attempt": {
"attempted_at": "2026-09-28T17:04:12.103Z",
"status": "failed",
"response_status_code": 500
}
}
stateMeansWhat to do
healthyThe newest attempt succeeded, or nothing was sent yet.Nothing.
failingThe newest attempt failed. Retries continue.Fix your endpoint, then read the delivery log.
disabledYou disabled the subscription, or delivery turned off after 5 days of failures.If the subscription reads active, send PATCH /v1/event-subscriptions/{id} to turn delivery back on. If you disabled it, create a new subscription.
no_endpointTruvo has nowhere to send this subscription’s events.Create a new subscription.

Limits for the webhook tools

  • A subscription takes 10 test events a minute, and each event can be replayed 5 times a day. Only a test or a replay that Truvo hands to delivery counts.
  • An integration can send 60 tests and replays a minute in all.
  • Tests and replays don’t use your quote request allowance, so they never slow down your quotes.
  • The delivery log, delivery, and health reads use the quote read allowance: one unit for a page of the delivery log, and up to two for a health read or a read of one delivery. The checks before a test use one unit, and the checks before a replay use two.
  • A 429 carries Retry-After. So does a 503: send the same request again after that many seconds.

An agent can use the same tools over MCP: send_test_event, list_event_deliveries, get_event_delivery, replay_event_delivery, and get_event_subscription_health.

Try it in the sandbox

Send a test event first. Then make a real change with your sandbox key: start a quote request, or run the force_expiry control to expire your sandbox quotes. Each change sends its events to your subscriptions and adds them to the feed.