Skip to main content
Incard can notify your systems over HTTPS when activity occurs on the platform. You register endpoints in the Incard dashboard. This guide covers how to receive and verify transaction webhook deliveries.

How it works

Setup
Someone with permission to manage webhooks adds an HTTPS URL in the Incard dashboard, chooses which events to receive, and saves the signing secret when it is shown. That secret is what your server uses to confirm deliveries really came from Incard.
When a webhook event happens
If the event matches what that endpoint subscribed to, Incard sends a signed POST to your URL with a JSON body and X-Incard-* headers. Your server should verify the signature on the raw body, process the event once (using the event id for deduplication), and return 2xx if you accepted it. If verification fails, respond with 4xx. If your server errors or times out, Incard may retry the delivery a limited number of times with incrementally increasing delays.

Configure in the dashboard

Outbound webhook can be managed in the Incard web app — under Settings → Webhooks. Each endpoint has its own URL, signing secret, and list of subscribed event types.
ActionWhat to know
Add endpointPublic HTTPS URL, optional label, and which event types to receive (see below).
Signing secretKeep it safe. It will be used to verify the signature of the webhook notification.
EditUpdate URL, label, or subscribed events.
DisableStops new deliveries; useful for maintenance or incidents.

Endpoint URL rules

  • Must be HTTPS with a normal public hostname.
  • Must be reachable from the public internet (no localhost, private IPs, or internal-only hostnames).
  • Maximum length 2048 characters.
  • Incard does not follow redirects.

Transaction event types

For transaction webhooks, subscribe per endpoint to one or both of:
EventWhen it fires
transaction.createA new transaction is recorded for your company.
transaction.updateAn existing transaction changes (for example status or amounts).
You only receive types you subscribed to. Each POST body includes the event type in JSON and in the X-Incard-Event-Type header.

Transaction webhook request format

Each transaction delivery is a POST with Content-Type: application/json. Headers
HeaderPurpose
X-Incard-Event-IdUnique id for this notification — use for idempotency.
X-Incard-Event-Typetransaction.create or transaction.update for this guide.
X-Incard-TimestampUnix time (seconds) when the request was signed.
X-Incard-Signaturev1= plus hex HMAC-SHA256 digest.
Body — versioned envelope:
{
    "id": "c93a7a3a-918d-4f62-ac79-c4ae64a4b8bc",
    "data": {
        "id": "342ea759-8870-418a-aa4c-ebc3a93ff70d",
        "name": "Working Payee",
        "type": "out",
        "points": 0,
        "status": "pending",
        "user_id": "29b297ca-c617-437e-9c1d-32f494fbf44d",
        "payee_id": "6992e50e-dfd7-4d9c-891c-362ccfc01b58",
        "direction": "debit",
        "reference": "Sent from Incard",
        "account_id": "0d6c0a2a-2b08-49bc-89a9-eb936c9ff28c",
        "company_id": "bebb185d-8210-4e66-ac63-397b49a1e09f",
        "created_at": "2026-07-02T16:07:39.970773Z",
        "fee_amount": "0.00",
        "updated_at": "2026-07-02T16:07:39.970773Z",
        "base_amount": "300.00",
        "payee_route": "local",
        "authorized_at": "2026-07-02T16:07:39.942Z",
        "base_currency": "GBP",
        "source_system": "payment-service",
        "account_amount": "300.00",
        "transaction_at": "2026-07-02T16:07:39.917Z",
        "account_currency": "GBP",
        "source_payment_id": "ab8861be-020f-6449-655e-c7b00ff71b74",
        "transaction_amount": "300.00",
        "transaction_currency": "GBP"
    },
    "type": "transaction.create",
    "source": "transactions",
    "occurred_at": "2026-07-02T16:07:39.970773Z",
    "schema_version": 1
}
FieldNotes
idDeduplicate on this value — safe replays should not double-apply side effects.
schema_versionCurrently 1.
dataTransaction snapshot. Always includes id and company_id. Other fields vary and may grow over time — ignore unknown fields.

Verify the signature

Transaction webhooks use the same signing scheme described here. Always verify before trusting the body. Use the raw request body exactly as received (before parsing JSON). Signed string:
{X-Incard-Timestamp}.{raw_body}
Expected header value:
v1=HMAC_SHA256(signing_secret, signed_string)   // lowercase hex
Compare to X-Incard-Signature with a constant-time function.
Reject requests whose timestamp is far from your server clock (for example older than five minutes).
const crypto = require("crypto");

function verifyIncardWebhook({ secret, rawBody, headers }) {
  const timestamp = headers["x-incard-timestamp"];
  const signature = headers["x-incard-signature"];
  if (!timestamp || !signature?.startsWith("v1=")) return false;

  const expected =
    "v1=" +
    crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

Handle transaction webhook deliveries

Recommended handler flow:
  1. Read raw body → verify signature → parse JSON.
  2. Skip or no-op if id was already processed.
  3. Queue or persist work, then return 2xx promptly (within a few seconds).
Your responseTypical result
2xxSuccess — no further retries for that attempt.
4xxTreated as failure — generally not retried.
5xx / timeout / unreachable hostRetried with increasing delays (up to a small fixed number of attempts).
Do heavy processing asynchronously after responding 2xx.

Go live checklist

  1. Dashboard: create an endpoint with production URL and the transaction event types you need.
  2. Store signing secret securely (environment variable or secrets manager).
  3. Deploy verifier + idempotent handler.
  4. Return 2xx only after the event is safely recorded on your side.
  5. Monitor failures and disable the endpoint from the dashboard if your service is down.