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.
Polling
Section titled “Polling”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.
Verify the signature
Section titled “Verify the signature”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));}import hmac, hashlib, time
def verify(raw: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) if abs(time.time() - int(parts["t"])) > tolerance_sec: return False expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"])
