Agent skill

MeasureOne Webhooks Skill

Receive and authenticate MeasureOne webhooks. Use when setting up a MeasureOne webhook endpoint, adding a static secret header to delivery_details, debugging 401s, or handling events like datarequest.report_complete, datarequest.items_available, session.rejected, item.processed and datarequest.refresh_failed.

Install this skill

npx skills add hookdeck/webhook-skills --skill measureone-webhooks


MeasureOne (consumer-permissioned data: academic, income, insurance) POSTs a flat JSON webhook to the URL you set in delivery_details[] on each Data Request. Webhooks are not signed. You authenticate them with a secret header that you add yourself under delivery_details[].headers and then check on your endpoint.

When to Use This Skill

  • How do I receive MeasureOne webhooks?
  • How do I authenticate MeasureOne webhook requests? (There is no signature: use a static header)
  • How do I handle datarequest.report_complete, datarequest.items_available or item.processed events?
  • Why does my MeasureOne webhook endpoint return 401?
  • Can I verify MeasureOne's JWT or Basic webhook authorization? (See limits below)

Authentication Setup

Add a static header to delivery_details.headers on the Data Request and enter it here. The JWT option fetches a new token for every delivery and cannot be checked.

MeasureOne also offers Basic and JWT authorization for webhooks. Basic arrives as a fixed Authorization value, so it works as the static header: enter Authorization as the header name and the full Basic <base64> value. The JWT option (x_auth_token_credentials) fetches a fresh token from your own endpoint before every delivery, so it can't be checked against a stored value.

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 verifyMeasureOneRequest(headers, secret = process.env.MEASUREONE_WEBHOOK_SECRET,
    headerName = process.env.MEASUREONE_WEBHOOK_HEADER || 'Authorization') {
  if (!secret) return false;                          // never accept when unconfigured
  const received = headers[headerName.toLowerCase()]; // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret); // WHOLE value, no "Bearer " parsing
}
  • The header name is whatever you typed in delivery_details[].headers, so read it from MEASUREONE_WEBHOOK_HEADER (default Authorization) and look it up case-insensitively.
  • MEASUREONE_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed. Compare the whole value; don't strip or parse the prefix.
  • Return 401 for a missing header or a mismatch, 400 for a body that isn't a JSON object, and any 2xx once handled.
  • Read the body raw (express.raw({ type: '*/*' })) so any content type works, and authenticate before parsing it.
  • Python: hmac.compare_digest on bytes (encode both sides; a str with non-ASCII raises).
  • 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.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The body is flat JSON: event, datarequest_id, individual_id, external_id, metadata, plus event-specific fields. Dispatch on event.

EventSent When
datarequest.report_completeThe complete report for a Data Request is available
datarequest.report_update_availableReport data was updated or added (complete report follows)
datarequest.report_errorGenerating the report failed (see reason_codes)
datarequest.items_availableExtraction finished and Items are available
datarequest.items_processedAll Items for the Data Request are processed
datarequest.no_itemsExtraction finished and no Item is available
datarequest.refresh_failedA scheduled refresh failed
datasource.connectedA Datasource connection succeeded
transaction.processedA services API transaction completed
unprocessable.entityA received item can't be processed
session.rejected / session.terminated / session.no_loginA Link session was rejected, terminated or closed without connecting
item.created / item.processed / item.unavailableAn Item was created, processed, or couldn't be extracted
individual.purgedAn Individual was purged

Deprecated but still documented: datarequest.report_available, datarequest.updates_available, datarequest.acquisition_completed, session.completed.

Any 2xx acknowledges a delivery. Failed deliveries are retried up to 10 times with exponential backoff (1, 2, 4 ... 512 minutes, with jitter). Make handlers idempotent.

Environment Variables

MEASUREONE_WEBHOOK_HEADER=Authorization            # the header name you chose (default Authorization)
MEASUREONE_WEBHOOK_SECRET=Bearer your-shared-secret # the exact, whole header value

Local Development

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

npx hookdeck-cli listen 3000 measureone --path /webhooks/measureone

Use the printed URL as the url in delivery_details. In Hookdeck, the source type is MeasureOne (API key verification: API Key Header plus the exact whole API Key).

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →