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_ 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_oravailable item.processedevents? - 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_, so read it fromdetails[].headers MEASUREONE_(defaultWEBHOOK_ HEADER Authorization) and look it up case-insensitively. MEASUREONE_is the exact, whole header value, including anyWEBHOOK_ SECRET Bearerprefix you typed. Compare the whole value; don't strip or parse the prefix.- Return
401for a missing header or a mismatch,400for a body that isn't a JSON object, and any2xxonce handled. - Read the body raw (
express.raw({ type: '*/*' })) so any content type works, and authenticate before parsing it. - Python:
hmac.compare_digeston bytes (encode both sides; astrwith 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.
| Event | Sent When |
|---|---|
datarequest.report_ | The complete report for a Data Request is available |
datarequest.report_ | Report data was updated or added (complete report follows) |
datarequest.report_error | Generating the report failed (see reason_codes) |
datarequest.items_ | Extraction finished and Items are available |
datarequest.items_ | All Items for the Data Request are processed |
datarequest.no_items | Extraction finished and no Item is available |
datarequest.refresh_ | A scheduled refresh failed |
datasource.connected | A Datasource connection succeeded |
transaction.processed | A services API transaction completed |
unprocessable.entity | A received item can't be processed |
session.rejected / session.terminated / session.no_login | A Link session was rejected, terminated or closed without connecting |
item.created / item.processed / item.unavailable | An Item was created, processed, or couldn't be extracted |
individual.purged | An Individual was purged |
Deprecated but still documented: datarequest.report_, datarequest.updates_, datarequest.acquisition_, 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
- references/overview.md - What MeasureOne webhooks are, events and payload shape
- references/setup.md - Add the static header to a Data Request
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests