Agent skill

Junction Webhooks Skill

Receive and verify Junction webhooks (health-data API for wearables, lab testing and continuous queries, formerly Vital; delivered via Svix). Use when setting up Junction webhook handlers, debugging svix-signature verification, or handling events like provider.connection.created, daily.data.sleep.created, historical.data.workouts.created, and labtest.order.updated.

Install this skill

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


Junction (junction.com, formerly Vital) delivers webhooks through Svix, which implements Standard Webhooks. Every delivery is an HTTP POST with a JSON body and three signature headers: svix-id, svix-timestamp, and svix-signature. The event name is the top-level event_type field, and the payload is under data, next to team_id, user_id and client_user_id.

When to Use This Skill

  • How do I receive Junction webhooks?
  • How do I verify Junction webhook signatures (svix-signature)?
  • How do I handle provider.connection.created or provider.connection.error events?
  • How do I react to daily.data.sleep.created or historical.data.workouts.created?
  • How do I handle labtest.order.updated or labtest.result.critical?
  • Why is my Junction webhook signature verification failing, or rejecting Hookdeck retries?

Verification (core)

Signed content is {svix-id}.{svix-timestamp}.{raw body}. Svix signs it with HMAC-SHA256 using the base64-decoded secret (the part after whsec_), base64-encodes the result, and sends one or more space-separated v1,<signature> entries. Use the official svix library, which compares in constant time. Read the svix-* headers, and when svix-id is absent fall back to the webhook-* headers (webhook-id, webhook-timestamp, webhook-signature), as Hookdeck does.

import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.JUNCTION_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["JUNCTION_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

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

Find the secret in the Junction Dashboard under Webhooks > the endpoint > Signing Secret (it can also be read and rotated through the team-webhook API). Junction doesn't print a sample, but as a Svix-issued secret it is expected to start with whsec_. Each endpoint has its own. Keep the full whsec_... value in JUNCTION_WEBHOOK_SECRET. Handlers fail closed: with the variable unset they return 500 instead of accepting anything.

Hookdeck retries and replays hit the 5-minute window

The svix and standardwebhooks libraries always enforce a fixed 5-minute timestamp window, and it can't be configured. Hookdeck retries and replays resend the original svix-timestamp, so the library rejects any retry or replay that happens more than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in references/verification.md, where the window can be dropped or widened. Deduplicate on svix-id instead of relying on the timestamp.

Common Event Types

About 185 events are documented. The name is event_type in the JSON body. The examples handle these:

EventTriggered When
provider.connection.createdA user connects a provider through Junction Link
provider.connection.errorA provider connection fails (for example, expired credentials)
provider.device.createdA device is added (also provider.device.updated)
historical.data.{resource}.createdThe historical backfill for a resource has completed (data-less)
daily.data.{resource}.createdNew data for a resource is discovered (also .updated)
labtest.order.createdA lab test order is created (also labtest.order.updated)
labtest.appointment.createdA lab appointment is created (also labtest.appointment.updated)
labtest.result.criticalA lab result has a critical value needing urgent attention
continuous_query.result_table.changedA Sense continuous query's result table changes

{resource} is a name such as activity, sleep, workouts, body, steps or glucose. See references/overview.md for the payload shape and event catalog.

Environment Variables

# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
JUNCTION_WEBHOOK_SECRET=whsec_xxxxx

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 junction --path /webhooks/junction

Register the tunnel URL as an endpoint under Webhooks in the Junction dashboard, then connect a provider to a test user (or send a test event from the portal) to see an event arrive.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →