Agent skill

OnceHub Webhooks Skill

Receive and verify OnceHub webhooks. Use when setting up OnceHub webhook endpoints, debugging Oncehub-Signature verification, or handling booking events like booking.scheduled, booking.rescheduled, booking.canceled, booking.completed, and booking.no_show.

Install this skill

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


When to Use This Skill

  • How do I receive OnceHub webhooks?
  • How do I verify the Oncehub-Signature header?
  • How do I handle booking.scheduled, booking.rescheduled, or booking.canceled events?
  • How do I react to booking.completed or booking.no_show events?
  • Why does my OnceHub webhook have no signing secret (v1 vs v2)?
  • Why is my OnceHub webhook signature verification failing?

Verification (core)

OnceHub signs v2 webhooks with an Oncehub-Signature header of the form t=<unix seconds>,s=<hex>. s is the hex HMAC-SHA256 of "<t>.<raw body>", keyed with the endpoint secret as a UTF-8 string (Webhook & API integration > View secret). Always sign over the raw body.

Only v2 webhooks are signed. If the dashboard doesn't show View secret for the webhook, it is a v1 webhook: it sends no signature and can't be verified. Create a new webhook (new ones are v2).

Timestamp window is opt-in. Hookdeck retries and replays resend the original timestamp, so a 5-minute window rejects late retries. It is controlled by ONCEHUB_TIMESTAMP_TOLERANCE_SECONDS (default 0 = off); see references/verification.md for where the window can be dropped or widened.

const crypto = require('crypto');

// toleranceSeconds = 0 (default) skips the timestamp window; a positive value opts in
function verifyOncehubWebhook(rawBody, header, secret, toleranceSeconds = 0) {
  if (!header) return false;
  const parts = header.split(',').map((p) => p.split('='));
  const t = parts.find(([k]) => k === 't')?.[1];
  const sigs = parts.filter(([k]) => k === 's').map(([, v]) => v);
  if (!t || !/^\d+$/.test(t) || sigs.length === 0) return false;
  if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
  return sigs.some((sig) => {
    try { return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); }
    catch { return false; } // length mismatch
  });
}

OnceHub has no verification SDK, so verification is manual in every framework. In Express, read the body with express.raw({ type: '*/*' }) so any content type verifies.

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

Common Event Types

The body is a single JSON event (id, object, creation_time, type, api_version, data). Route on type:

EventTriggered WhenCommon Use Cases
booking.scheduledA booking is madeCRM record, confirmation flow
booking.rescheduledA booking is rescheduledUpdate calendar/CRM time
booking.reassignedA booking is reassignedUpdate owner
booking.canceled_then_rescheduledCanceled, then rescheduledLink old and new booking
booking.canceled_reschedule_requestedCanceled with a reschedule requestedFollow-up outreach
booking.canceledA booking is canceledRelease resources
booking.completedA booking is completedFollow-up, billing
booking.no_showA booking is marked no-showNo-show workflow
conversation.started / conversation.closed / conversation.abandonedConversation lifecycleLead routing

Payload details are in references/overview.md.

Environment Variables

ONCEHUB_WEBHOOK_SECRET=your_endpoint_secret   # Webhook & API integration > View secret (v2 webhooks only)
ONCEHUB_TIMESTAMP_TOLERANCE_SECONDS=0       # Optional replay window; a positive value opts in

Local Development

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →