# Guide to Arcadia Webhooks: Features and Best Practices

Arcadia webhooks tell your systems when something changes in the utility data Arcadia collects for you. A credential starts failing to log in, a new bill statement is discovered, interval data arrives for a meter, or a user is asked for a multi-factor authentication code. If you build on Arcadia's Plug API, webhooks replace polling and let you fetch, store or act on new data as soon as Arcadia has it.

This guide covers how Arcadia routes webhooks through business units, the event types and payload shape, how to authenticate requests with a custom header that Arcadia support configures, how retries work, and the practices that keep an Arcadia receiver reliable.

## What are Arcadia webhooks?

Arcadia sends a JSON `POST` to the webhook URLs you configure on a business unit. Each business unit can have a live URL and a sandbox URL. When an entity changes, Arcadia sends the webhook to every business unit related to it and to all of those business units' ancestors, if they have a URL configured. Each URL gets its own webhook with a distinct `id`, and the payloads are otherwise identical.

There is no per-event subscription. Every URL receives every event type, and Arcadia's own advice is to filter on the top-level `resource` and `eventType` fields and acknowledge the rest with a `2xx`.

Arcadia doesn't sign webhooks. The only authentication it documents is a set of custom headers or query parameters sent with every webhook, and you can't set those yourself: Arcadia support configures them for your organisation on request.

## Arcadia webhook features

| Feature | Details |
| --- | --- |
| Configuration | Arcadia Dashboard, **Business units** > a business unit > **Webhooks** panel |
| URLs | A live URL and a sandbox URL per business unit |
| Fan-out | Sent to related business units and their ancestors, one webhook `id` per URL |
| Event selection | None. Every URL receives every event type |
| Method and format | `POST`, JSON |
| Event field | `eventType`, with the entity type in `resource` |
| Authentication | Custom headers or query parameters set by Arcadia support |
| Header limits | Name up to 80 characters, value up to 255. Alphanumerics, `_` and `-`; values also allow `/`, `.`, `\` and `,`. No spaces |
| Signature and timestamp | None |
| Response window | A `2xx` within 10 seconds after the connection is made |
| Retries | Three times every six hours for a day, twelve retries at most, with an identical body |
| RTCV retries | At most three |
| Resending | API endpoints list and resend stored webhooks |
| Source IPs | Not published |

## Event types

Arcadia's event types span several resources, so a router needs both `eventType` and `resource` to know what happened.

| `eventType` | `resource` values | Sent when |
| --- | --- | --- |
| `STATUS_CHANGE` | `CREDENTIAL`, `ACCOUNT`, `FILE`, `DELETION`, `DOWNLOAD`, `METER` | A status or status detail changes, including account and meter voiding |
| `NEW_DATA_AVAILABLE` | `STATEMENT`, `DISCOVERED_STATEMENT`, `METER` | A statement is discovered for the first time, or a new meter is discovered |
| `EXISTING_DATA_UPDATED` | `STATEMENT`, `DISCOVERED_STATEMENT`, `METER` | A stored statement changes, or new interval data is available for a meter |
| `REAL_TIME_CREDENTIAL_VALIDATION_SUCCESS` | `CREDENTIAL` | Arcadia logged in to the utility account, for credentials created with `isInteractive: true` |
| `REAL_TIME_CREDENTIAL_VALIDATION_FAILURE` | `CREDENTIAL` | Arcadia failed to log in, for credentials created with `isInteractive: true` |
| `MFA_CHALLENGE_METHOD` | `MFA_METHOD_PROMPT` | The user has to choose an MFA method |
| `MFA_CHALLENGE_CODE` | `MFA_CODE_PROMPT` | The user has to enter an MFA code |
| `BUSINESS_UNIT_ASSIGNMENT` | `ROOT_BUSINESS_UNIT_ACCOUNT` | An account is assigned to the root business unit. Sent only to the root business unit's URL |
| `DATA_DELETED` | `BUSINESS_UNIT_DELETION` | A user deletes a business unit |

The two MFA types appear in Arcadia's examples and scenarios rather than in its event tables. They are part of the interactive "Connect" flow, together with the real-time credential validation (RTCV) events.

Arcadia treats an adjustment statement as a new statement with its own ID, so it arrives as `NEW_DATA_AVAILABLE`. When Arcadia improves its extraction logic, a statement can be re-sent as `EXISTING_DATA_UPDATED` with the same statement ID and slightly different data.

## Payload structure

Every webhook has the same envelope. Here is a trimmed `STATUS_CHANGE` for a credential, from Arcadia's examples:

```json
{
  "businessUnitIds": ["bus_b2d4e1d1-3028-becd-1821-ff80aa8107f5"],
  "createdAt": "2024-02-21T00:00:00.000+00:00",
  "data": {
    "id": "crd_0c5d396f-f11c-c76b-9be8-89343463af21",
    "connectionHealth": "CONNECTION_SUCCESS",
    "status": "CONNECTION_SUCCESS",
    "statusDetail": "LOGIN_AND_DATA_DISCOVERY_SUCCESS",
    "totalAccounts": 1
  },
  "errors": [],
  "eventType": "STATUS_CHANGE",
  "id": "whk_9ec2fbdc-33ff-13b2-9b97-47d9ee7dcc73",
  "resource": "CREDENTIAL"
}

```

`data` holds the same representation the API returns for that resource, so you often don't need a follow-up call. For statement events, it matches the summarised statement endpoint, not the detailed one. `businessUnitIds` lists only the directly related business units, not the ancestors that also received the webhook.

> **Inspect Arcadia webhooks as they arrive.** Set a [Hookdeck Inspect](https://inspect.hookdeck.com) URL as a sandbox webhook URL on a business unit to see the exact headers and body Arcadia sends, with no account or setup required.

## Setting up Arcadia webhooks

Pick a header name and a long random value first. Because spaces aren't allowed, the value can't be `Bearer <token>`, so use a custom header such as `X-Arcadia-Token` with a bare token, for example from `openssl rand -hex 32`. That keeps the value within Arcadia's character set.

1. Open the Arcadia Dashboard, go to **Business units** and open the business unit's details.
2. Edit the **Webhooks** panel and enter your HTTPS endpoint as the live or sandbox webhook URL.
3. Open a support ticket with Arcadia through Zendesk and ask them to add your header name and value to your webhooks. Arcadia sets these values internally; they can't be set through the API or the Dashboard.
4. Wait for support to confirm, then trigger an event, for example by creating a test credential in sandbox, and check that the header arrives.

The header applies to every business unit in your organisation, so one value covers all your URLs. Arcadia can also add query parameters, but a header is the better choice: query strings end up in access logs.

**With Hookdeck.** Arcadia is a supported source in Hookdeck. Create a source with the **Arcadia** type and use its URL as the webhook URL. Enter the header name you asked for in **API Key Header** and its exact value in **API Key**. Header names match case-insensitively; the value must match exactly. The source accepts `POST` only, which is what Arcadia sends. It checks the header, not a query parameter, so ask support for a header. A request with a missing or wrong value still gets a `200` from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

For local development, `hookdeck listen 3000 arcadia --path /webhooks/arcadia` gives you a public URL that forwards to your machine.

## Securing Arcadia webhooks

With no signature, verification means reading the header Arcadia support configured and comparing its whole value with the one you stored, in constant time. Reject the request if the header is missing or the value isn't configured.

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

const HEADER_NAME = (process.env.ARCADIA_WEBHOOK_HEADER || 'x-arcadia-token').toLowerCase();
const SECRET = process.env.ARCADIA_WEBHOOK_SECRET;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/arcadia', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get(HEADER_NAME); // case-insensitive lookup
  if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Route on the pair; acknowledge anything you don't handle.
  const key = `${event.eventType}:${event.resource}`;
  if (key === 'NEW_DATA_AVAILABLE:STATEMENT') {
    // Queue a job to store the new statement in event.data.
  }

  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

HEADER_NAME = os.environ.get("ARCADIA_WEBHOOK_HEADER", "X-Arcadia-Token")
SECRET = os.environ.get("ARCADIA_WEBHOOK_SECRET", "")

def verify_arcadia_request(headers) -> bool:
    if not SECRET:
        return False  # never accept when unconfigured
    received = headers.get(HEADER_NAME)  # Starlette and Flask headers are case-insensitive
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))

```

A static header proves only that the sender knows the value. It doesn't cover the body, so a modified payload still passes, and there is no timestamp, so a captured request can be replayed. Use HTTPS only, keep the value out of logs, and ask Arcadia support to change it if it might have leaked.

> **Make Arcadia webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the header Arcadia sends, answers within Arcadia's 10-second window, and queues every webhook so you can retry and replay deliveries on your own schedule.

## Arcadia webhook limitations and pain points

### You can't set or rotate the secret yourself

**The Problem:** Adding authentication, or changing a leaked value, takes a support ticket and a wait.

**Why It Happens:** Arcadia sets custom headers and query parameters internally. They can't be changed through the API or the Dashboard.

**Workarounds:**

* Ask for the header when you first set up webhooks, not later.
* To rotate, accept both the old and the new value in your receiver until Arcadia confirms the change, then drop the old one.

**How Hookdeck Can Help:** Hookdeck's Arcadia source checks the header for you and keeps every request, including the ones that fail verification, with full headers, so you can see which value Arcadia is sending during a rotation.

### Every URL gets every event

**The Problem:** A service that only cares about new statements still receives credential status changes, MFA prompts and meter updates.

**Why It Happens:** Arcadia doesn't support opting in or out of event types, and the fan-out to ancestor business units means a parent URL also sees its children's events.

**Workarounds:**

* Route on `eventType` and `resource` and return a `2xx` for anything you don't handle, as Arcadia recommends.
* Use `businessUnitIds` to tell which business unit an event belongs to.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) can route on `eventType` and `resource` in the body, so each of your services only receives the events it handles.

### Ten seconds, then hours of waiting

**The Problem:** A slow handler or a short outage turns into a delay of six hours or more.

**Why It Happens:** Arcadia waits 10 seconds for a `2xx` after connecting. A failed delivery is retried three times every six hours for a day, and RTCV webhooks only three times in total.

**Workarounds:**

* Acknowledge first and process asynchronously, as Arcadia recommends for anything longer than 10 seconds.
* Use Arcadia's endpoints to list and resend stored webhooks after an outage.

**How Hookdeck Can Help:** Hookdeck answers Arcadia as soon as it has stored the request, then delivers to your service with [retries](/docs/retries) you configure. You can replay any event after a fix without waiting for Arcadia's next attempt.

### Duplicates across business units

**The Problem:** The same change can reach your system several times.

**Why It Happens:** One webhook is created per URL, so a child and a parent business unit pointing at the same service each get a copy with a different `id`. Retries resend the same body, with the same `id`.

**Workarounds:**

* Deduplicate retries on `id`.
* Deduplicate fan-out copies on the resource: `resource`, `data.id` and `eventType`, plus a field that changes between real updates, such as the status.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeats based on fields you choose, before they reach your service.

## Best practices

### Ask for a header, not a query parameter

Arcadia can add either, but URLs are logged in many more places than headers. A custom header with a bare token, such as `X-Arcadia-Token`, fits Arcadia's character rules and is what Hookdeck's source checks.

### Treat `data` as the API representation

`data` matches what the API returns for the resource, so you can usually store it without another call. Remember that statement webhooks carry the summarised statement; fetch the detailed statement if you need line items.

### Make handlers idempotent

Store `id` before acting and skip repeats. For statements, key on the statement ID, since `EXISTING_DATA_UPDATED` re-sends a known statement with revised values. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Verify the header, store the body, return `200`, and do the work in a background job. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Handle the interactive flow promptly

If you build your own Connect experience, the MFA and RTCV events drive what the user sees next. Push them to the user's session quickly, and don't rely on retries: RTCV webhooks are retried at most three times.

## Conclusion

Arcadia sends every event type to every webhook URL on a business unit and its ancestors, as JSON with `eventType`, `resource` and the API representation of the resource in `data`. There is no signature, so ask Arcadia support for a custom header with a long random value, compare it in full and in constant time, route on `eventType` and `resource`, and deduplicate on `id` and the resource.

[Hookdeck Event Gateway](https://hookdeck.com) checks that header, answers within Arcadia's 10-second window, and lets you filter, retry and replay events, so an outage on your side doesn't leave you waiting six hours for Arcadia's next attempt.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Arcadia webhooks reliably in minutes.