Gareth Wilson Gareth Wilson

Guide to OnceHub Webhooks: Features and Best Practices

Published


OnceHub webhooks tell your systems what happens to the meetings people book through OnceHub and the conversations they have with its chatbots: a booking is scheduled, rescheduled, cancelled or completed, a guest does not show up, or a website visitor starts or abandons a conversation. If you schedule meetings with OnceHub, webhooks are how those bookings reach your CRM, billing and follow-up workflows as they happen.

This guide covers how OnceHub webhooks are created and signed, the difference between v1 and v2 webhooks, the 11 events and their payload, and the best practices for running a OnceHub receiver in production.

What are OnceHub webhooks?

A OnceHub webhook subscription is a URL plus a list of booking and conversation events. When one of those events happens, OnceHub sends an HTTPS POST to the URL with a single JSON event in the body. The event's type names what happened, and data holds the booking or conversation.

OnceHub signs webhooks created with v2 of its API. Each delivery carries an Oncehub-Signature header with two parts: t, a Unix timestamp in seconds, and s, the hex HMAC-SHA256 of the string <t>.<raw body>, keyed with the endpoint's secret. OnceHub generates a separate secret for each endpoint.

Webhooks created with v1 of the API are not signed. They have no secret, and their requests carry no Oncehub-Signature header, so there is nothing to verify. OnceHub's docs give a simple test: if the dashboard does not show a View secret option for the webhook, it is a v1 webhook.

OnceHub webhook features

FeatureDetails
ConfigurationAccount Integrations > APIs & Webhooks, or the /webhooks API
SubscriptionA URL and a list of events per webhook. Several webhooks per account
URLMust use HTTPS
Signature headerOncehub-Signature: t=<unix seconds>,s=<hex>
SchemeHMAC-SHA256, hex
Signed content<t>.<raw body>
SecretOne per endpoint, shown on creation and under View secret
Unsigned webhooksv1 webhooks, which have no secret
Replay windowNone specified. OnceHub leaves the tolerance to you
Envelopeid, object, creation_time, type, api_version, data
RetriesNon-2xx responses are retried with exponential backoff for about 3 days
Auto-disableThe webhook is disabled once the retry window ends without a successful delivery
IP allowlistNone published

Common events

EventFires when
booking.scheduledA customer schedules a booking
booking.rescheduledA customer reschedules a booking on the same booking calendar
booking.reassignedA booking is reassigned to a different user
booking.canceled_then_rescheduledA customer cancels a booking and then reschedules on a different booking page
booking.canceled_reschedule_requestedA user cancels and sends the customer a request to reschedule
booking.canceledA user or customer cancels a booking
booking.completedThe booking's end time has passed
booking.no_showA user sets a completed booking to no-show
conversation.startedA website visitor starts interacting with a chatbot
conversation.closedA website visitor reaches the end of the conversation flow
conversation.abandonedA website visitor stops interacting with a bot for more than 10 minutes

Note the spelling: OnceHub uses canceled with one l in every event name.

Here is OnceHub's booking.scheduled example, trimmed:

{
  "id": "EVNT-2AB8RJY3LF",
  "object": "event",
  "creation_time": "2021-01-20T12:10:03.140Z",
  "type": "booking.scheduled",
  "api_version": "v2",
  "data": {
    "object": "booking",
    "id": "BKNG-YNMGHKQ24XV5",
    "subject": "15-minute meeting",
    "status": "scheduled",
    "starting_time": "2021-01-20T16:45:00.000Z",
    "duration_minutes": 15,
    "owner": {
      "id": "USR-YX0J4ANZTV",
      "object": "user",
      "email": "michael@example.com"
    },
    "rescheduled_booking_id": "",
    "cancel_reschedule_information": { "reason": "", "actioned_by": "", "user_id": "" },
    "form_submission": { "name": "Carrie", "email": "carrie@customer.com", "custom_fields": [] },
    "booking_calendar": { "id": "BKC-LDJ878496X", "object": "booking_calendar" }
  }
}

id is unique per event and makes a good idempotency key. object is always event. Booking events carry a booking in data, and conversation events carry a conversation. In OnceHub's examples, empty values are empty strings rather than null, so rescheduled_booking_id: "" means there is no linked booking.

One detail is inconsistent in OnceHub's docs: the signatures page shows data.owner as a user object, while the API reference's generated example shows it as a user ID string. Accept both.

Setting up OnceHub webhooks

Start your receiver first, then create the webhook:

  1. Click the gear icon and select Account Integrations.
  2. Select the APIs & Webhooks tile.
  3. In the Webhooks section, click Create Webhook.
  4. Enter a Webhook Name and the Webhook URL (it must start with https://), then click Next.
  5. Choose the events, then click Create.
  6. OnceHub displays the Webhook Secret. Click Copy & close and store it server-side, for example as ONCEHUB_WEBHOOK_SECRET. You can see it again later with View secret.

New webhooks are created as v2, so they are signed. If you have an older webhook with no View secret option, create a replacement and delete the old one.

To create webhooks from code, send POST /webhooks with a name that is unique in your account, the URL and the event list. The same endpoint lists webhooks with GET and removes them with DELETE.

If you receive OnceHub webhooks through Hookdeck, paste the endpoint secret into the Hookdeck source. Only v2 webhooks can be verified: a v1 webhook has no secret, so leave verification off for it or replace it with a v2 webhook.

Securing OnceHub webhooks

Split Oncehub-Signature on commas, then each part on =, to get t and s. Compute the hex HMAC-SHA256 of t, a dot and the raw request body, with the endpoint secret as the key, and compare it with s in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyOncehubSignature(rawBody, header, secret) {
  if (!header || !secret) return false;
  const parts = header.split(',').map((part) => part.trim().split('='));
  const timestamp = parts.find(([key]) => key === 't')?.[1];
  const signatures = parts.filter(([key]) => key === 's').map(([, value]) => value);
  if (!timestamp || signatures.length === 0) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest('hex');
  const computed = Buffer.from(expected, 'utf8');

  return signatures.some((signature) => {
    const received = Buffer.from(signature, 'utf8');
    return received.length === computed.length && crypto.timingSafeEqual(received, computed);
  });
}

const app = express();

app.post('/webhooks/oncehub', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyOncehubSignature(req.body, req.get('oncehub-signature'), process.env.ONCEHUB_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }

  // Optional: compare t with the current time. Leave this off behind Hookdeck,
  // because retries and replays resend the original header.

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.type, deduplicate on event.id, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac


def verify_oncehub_signature(raw_body: bytes, header, secret: str) -> bool:
    if not header or not secret:
        return False
    pairs = [part.strip().split("=", 1) for part in header.split(",") if "=" in part]
    timestamp = next((value for key, value in pairs if key == "t"), None)
    signatures = [value for key, value in pairs if key == "s"]
    if not timestamp or not signatures:
        return False
    expected = hmac.new(secret.encode("utf-8"), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s.encode("utf-8"), expected.encode("utf-8")) for s in signatures)

The signed string must use the exact bytes OnceHub sent. OnceHub signs the raw single-line JSON, so formatting or re-serialising the body adds whitespace and changes the signature.

OnceHub's docs suggest comparing t with the current time and deciding whether the difference is within your tolerance, without naming a number. That check is yours to make, and it is optional. OnceHub's own retries resend the original t for up to about 3 days, and so do Hookdeck's retries and replays, so a short window rejects legitimate late deliveries. Deduplicating on the event id covers replays without that risk.

Make OnceHub webhooks production-ready. Hookdeck Event Gateway verifies the Oncehub-Signature header on every v2 webhook, stores each booking and conversation event, and retries delivery on your schedule, so a slow week on your side does not get the webhook disabled.

OnceHub webhook limitations and pain points

v1 webhooks cannot be verified

The Problem: Requests arrive with no Oncehub-Signature header, and there is no secret anywhere in the dashboard.

Why It Happens: Only webhooks created with v2 of the API are signed. Older v1 webhooks have no secret.

Workarounds:

  • Look for View secret on each webhook. If it is missing, create a new webhook, which will be v2, and delete the old one.
  • Until you migrate, treat a missing header as a v1 webhook rather than an attack, and do not act on unverified bookings without checking them through the API.

How Hookdeck Can Help: A Hookdeck OnceHub source verifies v2 signatures for you. A request without a valid signature is rejected with a VERIFICATION_FAILED cause and shows up in the request log, which makes leftover v1 webhooks easy to spot.

Failing for three days disables the webhook

The Problem: After a long outage, bookings stop arriving entirely.

Why It Happens: OnceHub retries a failed delivery with exponential backoff for about 3 days. If delivery still has not succeeded, OnceHub disables the webhook automatically.

Workarounds:

  • Return a 2xx as soon as the request is verified and stored.
  • Check your webhooks in the dashboard or through the API after any extended outage, and re-enable or recreate disabled ones.
  • Backfill missed bookings from the bookings API.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives, so OnceHub sees success even while your service is down. Hookdeck then delivers with retries you configure and lets you replay any event once you recover.

Re-serialised bodies break the signature

The Problem: Signatures fail only in production, or only behind a particular proxy.

Why It Happens: OnceHub signs the raw single-line JSON. A body parser, logging middleware or proxy that pretty-prints or re-encodes the JSON changes the bytes.

Workarounds:

  • Verify before any middleware parses the body, using express.raw(), request.text() or request.body().
  • Keep the timestamp exactly as it appears in the header.

How Hookdeck Can Help: Hookdeck verifies the signature on the bytes it receives and forwards the original body and headers unchanged.

Several events for one booking change

The Problem: A cancellation followed by a reschedule produces events your handler treats as unrelated.

Why It Happens: OnceHub has separate events for cancel-and-reschedule cases, and a rescheduled booking points back to the old one through rescheduled_booking_id.

Workarounds:

  • Follow rescheduled_booking_id to link the new booking to the old one.
  • Read cancel_reschedule_information for the reason and who acted.

How Hookdeck Can Help: Hookdeck filters can send booking events and conversation events to different destinations, so each handler only sees the lifecycle it manages.

Best practices

Verify against the raw body

Use express.raw({ type: '*/*' }) in Express, await request.text() in Next.js, and await request.body() in FastAPI. Parse only after the signature matches.

Use the secret as a plain string

The endpoint secret is the HMAC key as UTF-8 text. Do not base64-decode it, and watch for trailing whitespace when you copy it. Each webhook has its own secret.

Deduplicate on the event id

OnceHub retries for days, so the same event can arrive more than once. Store the EVNT- ID before processing. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Verify, store, return 2xx, and update your CRM or send follow-ups afterwards. See why to process webhooks asynchronously.

Treat empty strings as missing

OnceHub's examples use "" for empty fields. Check for empty strings as well as null before you use IDs like rescheduled_booking_id.

Conclusion

OnceHub signs v2 webhooks with Oncehub-Signature: t=...,s=..., where s is the hex HMAC-SHA256 of <t>.<raw body> keyed with the endpoint secret. Make sure your webhooks are v2, verify against the raw bytes, keep any timestamp check optional and wide, deduplicate on the event id, and acknowledge quickly so three days of failures never disable a webhook.

Hookdeck Event Gateway verifies every OnceHub signature, keeps each booking event until your service has processed it, and lets you replay events after an outage.

Get started with Hookdeck for free and handle OnceHub webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.