Webhooks
Before posts a signed JSON event to your endpoint when a sale
you opened reaches a milestone. Webhooks fire only for sales opened through
POST /v1/partner/sessions.
Add an endpoint
Add endpoints in the Before Partner Portal: an HTTPS URL on the public internet, the events to send, and the funeral home it serves. Like keys, endpoints belong to one funeral home. Before refuses a URL that is plain HTTP or resolves to a private, loopback or link-local address, and does not follow redirects.
The endpoint's signing secret, whsec_…, is shown once, when the endpoint is
created. Rotating it in the portal shows a new secret and the old one stops
signing at once.
An endpoint receives events from live and test sales alike; data.mode tells
them apart.
Events
| Type | When |
|---|---|
sale.signed | Every signer has signed. |
sale.submitted | Before sent the application to the carrier. |
policy.issued | The carrier issued the policy. data.policy is set. |
sale.declined | The carrier declined the application. |
A sale raises them in this order, ending in either policy.issued or
sale.declined.
{
"id": "0d3e5c3a-8f5e-4c43-9f0e-7d1b8f9a6b21",
"type": "policy.issued",
"created_at": "2026-11-02T15:31:07.000Z",
"data": {
"sale_id": "6f1c1f0e-2b7a-4d39-9d0f-2a4f0b1d7c55",
"partner_reference": "case-1042",
"status": "issued",
"mode": "live",
"policy": { "number": "FPA-0012345", "carrier": "Forethought Life", "premium": "84.12" }
}
}
| Field | Meaning |
|---|---|
id | Unique per event and endpoint. Deduplicate on it. |
type | One of the types above. |
created_at | When Before recorded the milestone. |
data.status | The sale's status when the event was sent, which can be later than the milestone. |
data.policy | null until the policy is issued. premium is an exact decimal string; never parse it as a float. |
data is built when Before sends the event, not when the milestone happened.
A retried sale.signed can arrive with status: "submitted". Act on type,
and read GET /v1/partner/sales/:id when you need the current state.
Each request also carries these headers:
| Header | Value |
|---|---|
Before-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Before-Event-Id | The event's id. |
Before-Event-Type | The event's type. |
User-Agent | Before-Webhooks/1 |
Verify the signature
Compute an HMAC-SHA256 over <t>.<raw body>, keyed with the whole
whsec_… secret, and compare it in constant time with v1. Use the raw
request body exactly as received: parsing and re-serialising the JSON changes
the bytes. Refuse a timestamp more than five minutes from now, so a captured
request cannot be replayed.
- Node
- Python
const crypto = require('node:crypto');
function verifyBeforeSignature(secret, header, rawBody, toleranceSeconds = 300) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
if (!match) return false;
const timestamp = Number(match[1]);
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
return crypto.timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
}
With Express, read the body raw on this route only:
app.post('/webhooks/before', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifyBeforeSignature(process.env.BEFORE_WEBHOOK_SECRET, req.get('Before-Signature'), rawBody)) {
return res.sendStatus(400);
}
const event = JSON.parse(rawBody);
res.sendStatus(200);
queueForProcessing(event);
});
import hashlib
import hmac
import re
import time
SIGNATURE = re.compile(r"^t=(\d+),v1=([0-9a-f]{64})$")
def verify_before_signature(secret: str, header: str, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
match = SIGNATURE.match(header or "")
if not match:
return False
timestamp = int(match.group(1))
if abs(time.time() - timestamp) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, match.group(2))
With Flask, pass request.get_data() as raw_body.
Respond quickly
Answer with any 2xx status within 10 seconds, then do the work. Before
treats every other answer as a failure: 3xx redirects are not followed, and
4xx and 5xx statuses, timeouts and refused connections are all retried.
Retries
Each event is one delivery per endpoint, and every delivery ends delivered or failed.
Before sends each event within about a minute of the milestone. After a
failed attempt it waits min(2^attempts, 60) minutes before the next: 2, 4, 8,
16, 32, 60 and 60 minutes. After 8 failed attempts, about three hours, the
event is marked failed.
- Delivery is at least once: the same
idcan arrive more than once. - Order is not guaranteed:
sale.submittedcan arrive beforesale.signed.
The Partner Portal lists every delivery with its last status code and error,
and can send any delivered or failed event again. A resent event keeps its
id.