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_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)

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/, 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.

eventTyperesource (example)Sent When
STATUS_CHANGECREDENTIALA credential's status changes
NEW_DATA_AVAILABLEMETERNew data is available for a resource
EXISTING_DATA_UPDATEDMETERExisting data is updated
DATA_DELETEDBUSINESS_UNIT_DELETIONA user deletes a business unit
BUSINESS_UNIT_ASSIGNMENTROOT_BUSINESS_UNIT_ACCOUNTAn account is assigned to a business unit
REAL_TIME_CREDENTIAL_VALIDATION_SUCCESSCREDENTIALA real-time credential validation succeeds
REAL_TIME_CREDENTIAL_VALIDATION_FAILURECREDENTIALA real-time credential validation fails
MFA_CHALLENGE_METHODMFA_METHOD_PROMPTThe user must pick an MFA verification method
MFA_CHALLENGE_CODEMFA_CODE_PROMPTThe 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →