Skip to content

Events and webhooks

Every change on your files is an event. The full list with payloads is also an AsyncAPI spec, for code generators and agents.

Event When
submission.received A file was created for your agency
submission.status_changed A file moved stage
needs.requested Valjean asked you for more information
markets.updated A market answered (anonymous counts, never names)
quote.presented Your broker presented a quote
bind.status_changed A bind request moved forward
policy.bound A policy was bound
policy.status_changed A policy was issued, cancelled, expired or renewed
document.available A binder, policy or endorsement is ready
invoice.sent, invoice.paid, invoice.overdue Invoice changes

Payloads for each are in the API reference, under Webhooks.

GET /events?after=<next_after> returns events oldest first. Keep the next_after from each response and pass it next time. With no after (or since) you get the last 7 days. type filters, comma-separated, with prefixes like bind.*. Events are kept 90 days. Status events (in the feed and in webhooks) include status_text in plain words you can show as is.

Add a webhook in your portal or with POST /webhooks: a public https address, up to 5 per agency, all events or a list. The signing secret is returned once.

We POST each event as JSON, { id, type, created_at, agency, submission_id, data }, with these headers:

Header Value
Valjean-Signature t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> with your signing secret
Valjean-Event-Id The event id (the same cursor as /events)
Valjean-Event-Type The event type

Reply 2xx within 8 seconds. Failures retry after 1, 5, 30, 120, 360, 720 and 1440 minutes (8 attempts, about 45 hours). A webhook that fails 50 times in a row is turned off. POST /webhooks/{id}/test sends a signed webhook.ping straight away.

Check the signature on the raw body before parsing it, and reject old timestamps.

import crypto from "node:crypto";
function verify(raw, header, secret, toleranceSec = 300) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}