# Arcadia Webhooks

Arcadia sends a JSON `POST` when utility data changes: a credential's status changes, new data
becomes available, existing data is updated or deleted, or a user must answer an MFA challenge.
Webhooks are **not signed**. The only authentication Arcadia offers is a custom header (name and
value) that Arcadia support adds to every webhook for your organization, and your endpoint checks
that header against the value you stored.

Arcadia support sets the header name and value: open a support ticket to request one. Values cannot contain spaces.

Because spaces are not allowed, a value such as `Bearer abc` is impossible; use a custom header
such as `X-Arcadia-Token`. The setting applies to all webhooks in the organization.

## When to Use This Skill

* How do I receive Arcadia webhooks?
* How do I authenticate Arcadia webhooks if they aren't signed?
* How do I handle `STATUS_CHANGE`, `NEW_DATA_AVAILABLE` or `DATA_DELETED` events?
* Why does my Arcadia webhook endpoint return 401?
* How do I handle `MFA_CHALLENGE_CODE` and credential validation events?

## 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 verifyArcadiaRequest(headers, env = process.env) {
  const secret = env.ARCADIA_WEBHOOK_SECRET;                      // exact, whole header value
  if (!secret) return false;                                      // never accept when unconfigured
  const name = (env.ARCADIA_WEBHOOK_HEADER || 'X-Arcadia-Token').toLowerCase();
  const received = headers[name];                                 // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret);       // compare the WHOLE value
}

```

* The header name is whatever you asked Arcadia support for. Read it from `ARCADIA_WEBHOOK_HEADER`
  (default `X-Arcadia-Token`) and look it up case-insensitively.
* Compare the **whole** value with `ARCADIA_WEBHOOK_SECRET`. Do not strip or parse a `Bearer `
  prefix; if you asked for one, it is part of the expected value.
* Python: `hmac.compare_digest` on bytes (`.encode()` both sides; `str` raises on non-ASCII).
* Return `401` for a missing or wrong header, `400` for a body that isn't a JSON object, `200`
  once handled. Arcadia expects a 2xx within 10 seconds.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and authenticate
  before parsing it.

**What this does and doesn't give you:** a static shared credential authenticates the sender, not
the body. There is no signature and no timestamp, so there is no integrity check and no replay
protection. Use HTTPS only, treat the value like a password, and ask Arcadia support to rotate it
if it leaks.

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

## Event Types

Every payload has `id` (`whk_...`), `eventType`, `resource`, `createdAt`, `businessUnitIds`,
`errors` and `data`. Dispatch on `eventType` (and `resource`) and de-duplicate on `id`.

| eventType | resource (example) | Sent When |
| --- | --- | --- |
| `STATUS_CHANGE` | `CREDENTIAL` | A credential's status changes |
| `NEW_DATA_AVAILABLE` | `METER` | New data is available for a resource |
| `EXISTING_DATA_UPDATED` | `METER` | Existing data is updated |
| `DATA_DELETED` | `BUSINESS_UNIT_DELETION` | A user deletes a business unit |
| `BUSINESS_UNIT_ASSIGNMENT` | `ROOT_BUSINESS_UNIT_ACCOUNT` | An account is assigned to a business unit |
| `REAL_TIME_CREDENTIAL_VALIDATION_SUCCESS` | `CREDENTIAL` | A real-time credential validation succeeds |
| `REAL_TIME_CREDENTIAL_VALIDATION_FAILURE` | `CREDENTIAL` | A real-time credential validation fails |
| `MFA_CHALLENGE_METHOD` | `MFA_METHOD_PROMPT` | The user must pick an MFA verification method |
| `MFA_CHALLENGE_CODE` | `MFA_CODE_PROMPT` | The user must supply an MFA code |

`resource` names the entity (`CREDENTIAL`, `ACCOUNT`, `STATEMENT`, `METER`, ...). See the
[webhooks reference](https://docs.arcadia.com/reference/webhooks-overview) for the full list.

## Environment Variables

```bash
ARCADIA_WEBHOOK_HEADER=X-Arcadia-Token      # header name Arcadia support configured
ARCADIA_WEBHOOK_SECRET=your-header-value    # exact, whole header value (no spaces)

```

## Local Development

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

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

```

Give the printed URL to Arcadia as the webhook URL.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/arcadia-webhooks/references/overview.md) - What Arcadia webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/arcadia-webhooks/references/setup.md) - Request the header from Arcadia support and register the endpoint
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/arcadia-webhooks/references/verification.md) - Header check details, limits, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/arcadia-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests