# Incode Webhooks

Incode sends a JSON `POST` when something happens in an identity verification session: a
session starts, succeeds, fails or needs review, an identity is enrolled, or the onboarding
status changes. Webhooks are **not signed**. You authenticate them with a **custom header** that
you add in Dashboard > Configuration > Webhooks; Incode sends it on every webhook, and your
endpoint compares it with the value you stored.

> Add a custom header in Dashboard > Configuration > Webhooks and enter it here. Incode retries unless it gets a 200 with a JSON content type, or a 204, so keep any custom source response JSON.

(That is the setup note Hookdeck shows for its Incode source, type `INCODE`.)

## When to Use This Skill

* How do I receive Incode webhooks?
* How do I authenticate Incode webhook requests? Does Incode sign them? (No: use a custom header)
* What response does Incode need to stop retrying?
* How do I handle `SESSION_SUCCEEDED`, `SESSION_FAILED` or `IDENTITY_ENROLLED` events?
* Why does my Incode webhook endpoint return 401, or why does Incode keep retrying?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyIncodeRequest(headers, secret = process.env.INCODE_WEBHOOK_SECRET,
                             name = process.env.INCODE_WEBHOOK_HEADER || 'Authorization') {
  if (!secret) return false;                      // never accept when unconfigured
  const received = headers[name.toLowerCase()];   // Node lowercases header names
  return typeof received === 'string' && received !== '' && safeEqual(received, secret); // WHOLE value, no prefix parsing
}

```

* The header **name** is whatever you typed as *Custom Header Key*, so read it from
  `INCODE_WEBHOOK_HEADER` (default `Authorization`) and look it up case-insensitively.
* The expected **value** is the exact, whole *Custom Header Value*, including any `Bearer `
  prefix you typed. Do not strip or parse a prefix.
* Missing header or mismatch: `401`. Python: `hmac.compare_digest` on `.encode()`d bytes, never
  on `str` (it raises on non-ASCII).
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **Security limits:** a static shared credential authenticates the sender, not the body, and
  gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential
  if it leaks.
* **OIDC / OAuth is not covered.** Incode can instead authorize webhooks with an OAuth 2.0 bearer
  token (`Authorization: Bearer <ACCESS_TOKEN>`, issued by your authorization server). That
  token changes, so it can't be checked against a fixed value, and Hookdeck's Incode source does
  not support it. Use a custom header.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/incode-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/incode-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/incode-webhooks/examples/fastapi/).

## Respond Correctly

Incode retries (5 attempts, exponential from 30s, about 32 minutes in total) on a timeout or any
status other than **`200` with an `application/json` content type, or `204`**. The examples answer
`200 {"success": true}` via `res.json` / `NextResponse.json` / `JSONResponse`. If you configure
a custom response in Hookdeck for this source, keep it JSON.

## Event Types

Each webhook kind carries its name in a different field. Read `event_type`, then `eventType`,
then `onboardingStatus`.

| Field | Event | Sent When |
| --- | --- | --- |
| `event_type` | `SESSION_STARTED` | A Flow or Workflow session starts |
| `event_type` | `SESSION_PROGRESS` | A session is resumed (a `/start` call after creation) |
| `event_type` | `SESSION_SUCCEEDED` | A session succeeds |
| `event_type` | `SESSION_FAILED` | A session fails (see `failureReasons`) |
| `event_type` | `SESSION_PENDING_REVIEW` | A session needs manual review |
| `eventType` | `IDENTITY_ENROLLED` | An Identity is created for the first time |
| `onboardingStatus` | `ID_VALIDATION_FINISHED`, `ID_VALIDATION_FINISHED_SECOND_ID`, `GOVERNMENT_VALIDATION_FINISHED`, `FACE_VALIDATION_FINISHED`, `POST_PROCESSING_FINISHED`, `POST_PROCESSING_FINISHED_SECOND_ID` | A module of an onboarding session completes |
| `onboardingStatus` | `ONBOARDING_FINISHED` | All data is collected and the final status is set |
| `onboardingStatus` | `MANUAL_REVIEW_APPROVED`, `MANUAL_REVIEW_REJECTED` | A reviewer decides a manual-review session |
| `onboardingStatus` | `EXPIRED` | A session passes its expiry time |
| `onboardingStatus` | `DELETED` | Session data is deleted (carries `interviewIds`, an array) |

Other webhooks (video selfie, watchlist, face authentication, KYB results and the
Mexico-only ones) have their own payloads; see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/incode-webhooks/references/overview.md).

## Environment Variables

```bash
INCODE_WEBHOOK_HEADER=Authorization        # Custom Header Key you added in the dashboard
INCODE_WEBHOOK_SECRET=your-header-value    # Custom Header Value, the exact whole value

```

## Local Development

Incode can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 incode --path /webhooks/incode

```

Use the printed URL as the webhook URL in Incode. Add the custom header in Incode and in the
Hookdeck source (header name and exact value) so both ends agree.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/incode-webhooks/references/overview.md) - Webhook kinds, events and payload shapes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/incode-webhooks/references/setup.md) - Add the custom header and webhook URL in the dashboard
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/incode-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/incode-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests