Agent skill
Arcadia Webhooks Skill
Receive and authenticate Arcadia webhooks (utility data platform, Plug and Spark APIs). Use when setting up an Arcadia webhook endpoint, checking the custom authentication header Arcadia support configures, debugging 401s, or handling STATUS_CHANGE, NEW_DATA_AVAILABLE, EXISTING_DATA_UPDATED, DATA_DELETED and credential validation events.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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_AVAILABLEorDATA_DELETEDevents? - Why does my Arcadia webhook endpoint return 401?
- How do I handle
MFA_CHALLENGE_CODEand credential validation events?
Verification (core)
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(defaultX-Arcadia-Token) and look it up case-insensitively. - Compare the whole value with
ARCADIA_WEBHOOK_SECRET. Do not strip or parse aBearerprefix; if you asked for one, it is part of the expected value. - Python:
hmac.compare_digeston bytes (.encode()both sides;strraises on non-ASCII). - Return
401for a missing or wrong header,400for a body that isn't a JSON object,200once 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/, examples/nextjs/, 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_ | An account is assigned to a business unit |
REAL_ | CREDENTIAL | A real-time credential validation succeeds |
REAL_ | 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 for the full list.
Environment Variables
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):
npx hookdeck-cli listen 3000 arcadia --path /webhooks/arcadia
Give the printed URL to Arcadia as the webhook URL.
Resources
- references/overview.md - What Arcadia webhooks are, events and payload shape
- references/setup.md - Request the header from Arcadia support and register the endpoint
- references/verification.md - Header check details, limits, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests