Capvant

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 plus update {status, reason, funded_amount}.
  • message.created: the full deal plus message {id, direction, from_email, subject, body, created_at}.
  • request.fulfilled: the full deal plus request {id, item_type, label, file_count, uploaded_at, files[]} where every file carries a signed download URL, the same ones GET /deals/{ref}/requests/{id}/files returns.
  • offer.accepted, deal.account_submitted, deal.agreement_signed: the full deal plus offer or completion.

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

Shell
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.

JSON
{
  "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:

Shell
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

The retry scheduleSix attempts across a little over a day, on a log axis so the first minute and the last day both fit.
  1. immediately
    First delivery
  2. +1m
    Retry 1
  3. +6m
    Retry 2
  4. +36m
    Retry 3
  5. +3h
    Retry 4
  6. +15h
    Retry 5
  7. +1d
    ExhaustedSix 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_at is updated and its failure_count resets to 0.
  • A non-2xx response, a timeout, or a network error marks it a failure: last_failure_at is set and failure_count increments. 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 200 immediately. Do the real work asynchronously so you comfortably clear the 5-second window.
  • Make handling idempotent: use the event's data reference (deal ref or referral id) plus event as your dedupe key, since a retried delivery means the same event can arrive more than once.

On this page