Skip to content
Finovel
API documentation

Webhooks

Finovel tells your server when something happens, so you do not have to keep asking.

Setting up an endpoint

Add an HTTPS address in the console, on the Webhooks page. You can add up to 10 for each environment. Sandbox events go only to sandbox endpoints, and production events only to production endpoints.

Finovel shows the signing secret once, so store it with your server's other secrets. Private and internal addresses are refused.

Events

Choose the events an endpoint receives, or leave them all unticked to receive every event, including ones added later.

EventWhen
job.completedAn async job finished: succeeded, failed or error. Fetch the result from GET /v1/jobs/{id}.
esign.envelope.completedEveryone has signed an envelope, and the signed PDF is ready.
esign.envelope.declinedA signer declined to sign an envelope.
esign.envelope.expiredAn envelope expired before everyone signed.
webhook.testA test event you sent from the console.

What you receive

A POST with a JSON body.

{
  "id": "evt_8mQ2xV...",
  "type": "job.completed",
  "created_at": "2026-11-01T09:30:00.000Z",
  "environment": "production",
  "data": {
    "job_id": "job_...",
    "request_id": "req_...",
    "api": "bank-statement-analysis",
    "status": "succeeded",
    "finished_at": "2026-11-01T09:29:58.000Z"
  }
}

An event says what happened, and does not carry the result. Fetch that with your API key, so results only ever reach someone who holds one.

Checking a delivery

Every delivery is signed the Standard Webhooks way. Check the signature before you trust a delivery.

HeaderMeaning
webhook-idThe event id. It stays the same when a delivery is retried or resent.
webhook-timestampWhen this attempt was sent, in seconds since 1970.
webhook-signaturev1, then a comma, then the base64 HMAC-SHA256 signature.
// Node.js: check a delivery before trusting it. Use the raw body, before any JSON parsing.
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, headers, rawBody) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // older than 5 minutes
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = 'v1,' + createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64');
  return headers['webhook-signature'].split(' ').some((sig) =>
    sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}

Answering and retries

Answer with any 2xx status within 10 seconds. Do slow work after you answer. Redirects are not followed.

Anything else is tried again, up to 8 attempts over about 42 hours: after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 24 hours.

An endpoint is switched off after 20 deliveries in a row fail for good. Switch it back on from the console, where you can also see each delivery and send it again.

Handling repeats

The same event can arrive more than once, and events can arrive out of order. Keep the webhook-id of events you have handled and ignore repeats.

Replacing a secret

Replace a secret from the console. The old one stops working at once, so update your server straight away.