Receive webhooks
Register an endpoint and receive signed events the moment something changes.
Push beats poll: register an endpoint once and Capvant sends you a signed JSON POST the moment a deal, referral, or message changes. This guide covers registration, verification, and (honestly) exactly how delivery failure is handled today.
Push delivery
An endpoint registered today receives the complete deal object in every delivery: data is exactly what GET /deals/{ref} returns (business, contact, request, the market object with the register facts, statements, your offers, documents), plus the event's own detail keyed next to it. A push-only integration therefore never has to call GET.
deal.created: the full deal, the moment it is routed to you (test deals fire it too).deal.updated: the full deal plusupdate{status, reason, funded_amount}.message.created: the full deal plusmessage{id, direction, from_email, subject, body, created_at}.request.fulfilled: the full deal plusrequest{id, item_type, label, file_count, uploaded_at, files[]} where every file carries a signed download URL, the same onesGET /deals/{ref}/requests/{id}/filesreturns.offer.accepted,deal.account_submitted,deal.agreement_signed: the full deal plusofferorcompletion.
Every delivery states its mode in data.payload_mode and the X-Capvant-Payload header. If you prefer the small payload (the deal ref and a few fields, then GET for the rest), register with "payload": "reference". Endpoints registered before push delivery existed stay on reference mode until you re-register them.
The host you register through decides the endpoint's environment: an endpoint registered on sandbox.capvant.com only ever receives test deals; one registered on api.capvant.com receives every deal.
1. Register an endpoint
curl --request POST \
--url https://api.capvant.com/v1/webhooks \
--header "X-API-Key: $CAPVANT_API_KEY" \
--header "content-type: application/json" \
--data '{
"url": "https://intake.yourbank.co.uk/capvant",
"events": ["deal.created", "deal.updated", "message.created"],
"payload": "full"
}'The response includes secret exactly once. Store it next to the endpoint URL; you'll need it to verify every delivery. The URL must be https on a public host: private, loopback and link-local addresses are refused at registration, and the name is resolved again before every send.
{
"id": "wh_91d",
"url": "https://intake.yourbank.co.uk/capvant",
"events": ["deal.created", "deal.updated", "message.created"],
"payload": "full",
"environment": "live",
"secret": "whsec_1f2a...9c",
"active": true
}2. Verify the signature
Every delivery carries X-Capvant-Signature: sha256=, an HMAC-SHA256 of the raw request body keyed with your endpoint's secret, plus X-Capvant-Timestamp and X-Capvant-Signature-256 (t=,v1=.">) for replay protection. Compute over the raw bytes, before any JSON parsing:
import { createHmac, timingSafeEqual } from 'crypto';
function verify(rawBody, header, secret) {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Full Node and Python examples are on Signature verification.
3. Retry policy: what actually happens
- immediately First delivery
- +1m Retry 1
- +6m Retry 2
- +36m Retry 3
- +3h Retry 4
- +15h Retry 5
- +1d Exhausted
- immediatelyFirst delivery
- +1mRetry 1
- +6mRetry 2
- +36mRetry 3
- +3hRetry 4
- +15hRetry 5
- +1dExhaustedSix attempts in all. Twenty consecutive exhausted deliveries deactivate the endpoint.
Read this section carefully.
- Each event is POSTed to every active, subscribed endpoint with a 5-second timeout.
- A 2xx response marks that delivery a success: the endpoint's
last_success_atis updated and itsfailure_countresets to 0. - A non-2xx response, a timeout, or a network error marks it a failure:
last_failure_atis set andfailure_countincrements. The attempt (event, payload, status, and that it was tried) is durably recorded either way. - Deliveries are retried on a backoff schedule (1m/5m/30m/2h/12h, up to 6 attempts), then marked exhausted if none succeed. An endpoint that racks up 20 consecutive exhausted deliveries is automatically deactivated.
Even with retries, treat webhooks as a low-latency notification, not your only source of truth. Reconcile periodically with GET /deals or GET /referrals so a dropped delivery (endpoint down for the full backoff window, or deactivated after repeated failures) never silently loses an update. If you know you missed one, ask your partnership contact for a manual replay of the affected event.
4. Build a fast, boring endpoint
- Verify the signature, enqueue the event, return
200immediately. Do the real work asynchronously so you comfortably clear the 5-second window. - Make handling idempotent: use the event's
datareference (dealrefor referralid) pluseventas your dedupe key, since a retried delivery means the same event can arrive more than once.
