> ## Documentation Index
> Fetch the complete documentation index at: https://docs.incard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive signed Webhook notifications

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.

| Action             | What to know                                                                            |
| ------------------ | --------------------------------------------------------------------------------------- |
| **Add endpoint**   | Public **HTTPS** URL, optional label, and which **event types** to receive (see below). |
| **Signing secret** | Keep it safe. It will be used to verify the signature of the webhook notification.      |
| **Edit**           | Update URL, label, or subscribed events.                                                |
| **Disable**        | Stops 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:

| Event                | When it fires                                                    |
| -------------------- | ---------------------------------------------------------------- |
| `transaction.create` | A new transaction is recorded for your company.                  |
| `transaction.update` | An 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**

| Header                | Purpose                                                      |
| --------------------- | ------------------------------------------------------------ |
| `X-Incard-Event-Id`   | Unique id for this notification — use for idempotency.       |
| `X-Incard-Event-Type` | `transaction.create` or `transaction.update` for this guide. |
| `X-Incard-Timestamp`  | Unix time (seconds) when the request was signed.             |
| `X-Incard-Signature`  | `v1=` plus hex HMAC-SHA256 digest.                           |

**Body** — versioned envelope:

```json theme={null}
{
    "id": "c93a7a3a-918d-4f62-ac79-c4ae64a4b8bc",
    "data": {
        "id": "342ea759-8870-418a-aa4c-ebc3a93ff70d",
        "name": "ACME Corp",
        "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",
        "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
}
```

**Envelope fields**

| Field            | Notes                                                                                                  |
| ---------------- | ------------------------------------------------------------------------------------------------------ |
| `id`             | Unique event id (UUID). Deduplicate on this value — safe replays should not double-apply side effects. |
| `type`           | Event type: `transaction.create` or `transaction.update`.                                              |
| `source`         | Always `transactions` for these events.                                                                |
| `occurred_at`    | RFC 3339 timestamp of when the event was produced.                                                     |
| `schema_version` | Envelope version. Currently `1`.                                                                       |
| `data`           | Transaction snapshot — see the fields below.                                                           |

**`data` fields**

The transaction snapshot is produced by the `transactions` service. Fields marked **Always** are present on every delivery; fields marked **When set** are omitted when they have no value, so treat them as optional. New fields may be added over time — ignore any you don't recognise.

| Field                  | Type              | Presence | Description                                                                                                                                                                                      |
| ---------------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                   | string (UUID)     | Always   | Transaction id. Stable across `transaction.create` and later `transaction.update` events.                                                                                                        |
| `name`                 | string            | Always   | Human-readable label (for example payee or merchant name).                                                                                                                                       |
| `type`                 | enum              | Always   | Transaction type, e.g. `card`, `atm`, `out`, `in`, `fee`, `exchange_out`, `exchange_in`, `transfer_out`, `transfer_in`, `chargeback`, `reversal`, `adjustment`, `direct_debit`, `direct_credit`. |
| `points`               | integer           | When set | Loyalty points associated with the transaction.                                                                                                                                                  |
| `status`               | enum              | Always   | Lifecycle status: `pending`, `completed`, `declined`, or `reversed`.                                                                                                                             |
| `user_id`              | string (UUID)     | When set | User who initiated the transaction.                                                                                                                                                              |
| `payee_id`             | string            | When set | Identifier of the payee.                                                                                                                                                                         |
| `direction`            | enum              | Always   | `debit` (money out) or `credit` (money in).                                                                                                                                                      |
| `reference`            | string            | When set | Free-text payment reference.                                                                                                                                                                     |
| `account_id`           | string (UUID)     | Always   | Account the transaction belongs to.                                                                                                                                                              |
| `company_id`           | string (UUID)     | Always   | Company that owns the transaction.                                                                                                                                                               |
| `created_at`           | string            | Always   | When the transaction record was created.                                                                                                                                                         |
| `fee_amount`           | string (decimal)  | When set | Fee charged for the transaction.                                                                                                                                                                 |
| `updated_at`           | string            | Always   | When the transaction record was last updated.                                                                                                                                                    |
| `base_amount`          | string (decimal)  | When set | Amount in the base currency.                                                                                                                                                                     |
| `payee_route`          | string            | When set | Payment route (for example `local`).                                                                                                                                                             |
| `authorized_at`        | string            | When set | When the transaction was authorised.                                                                                                                                                             |
| `base_currency`        | string (ISO 4217) | When set | Reporting/base currency.                                                                                                                                                                         |
| `account_amount`       | string (decimal)  | When set | Amount in the account currency (e.g. `"300.00"`).                                                                                                                                                |
| `transaction_at`       | string            | Always   | When the transaction occurred.                                                                                                                                                                   |
| `account_currency`     | string (ISO 4217) | Always   | Currency of the account.                                                                                                                                                                         |
| `source_payment_id`    | string            | Always   | Identifier of the payment in the originating source system.                                                                                                                                      |
| `transaction_amount`   | string (decimal)  | When set | Amount in the transaction currency.                                                                                                                                                              |
| `transaction_currency` | string (ISO 4217) | When set | Currency the transaction was made in.                                                                                                                                                            |

## 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:

```text theme={null}
{X-Incard-Timestamp}.{raw_body}
```

Expected header value:

```text theme={null}
v1=HMAC_SHA256(signing_secret, signed_string)   // lowercase hex
```

Compare to `X-Incard-Signature` with a constant-time function.

<Tip>
  Reject requests whose timestamp is far from your server clock (for example older than five minutes).
</Tip>

```javascript theme={null}
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 response                      | Typical result                                                           |
| ---------------------------------- | ------------------------------------------------------------------------ |
| `2xx`                              | Success — no further retries for that attempt.                           |
| `4xx`                              | Treated as failure — generally **not** retried.                          |
| `5xx` / timeout / unreachable host | Retried 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.
