Skip to the main content
Whenabouts Whenabouts
CreatePricingHelpSign in

Help

Webhooks

On a paid plan, we can post to an address of yours whenever something happens, so your code doesn't have to keep asking the API. Each message is signed, so you can check it came from us.

What's on this page

  • Adding an address
  • What we tell you about
  • What a message looks like
  • Answering, retries and switching off
  • Checking it came from us
  • On the free plan, and when a plan ends

Adding an address

Sign in, open "Keys and the API" from your pages, and go to "Being told when something happens". Type an address of yours under "Where should we post", pick what to be told about, and press "Add an address".

  • The address has to be https, and can't be a private or local one. We don't try it when you add it, so check it's reachable from the internet yourself.
  • Each time an address is added, we email your account's address the domain it's on, never the rest of it or the secret, so you'd know if somebody else added it.
  • You're shown a signing secret once. Copy it then: we can't show it again. There's no way to change the secret or what an address is told about; add a new address and delete the old one.
  • Up to 5 addresses on a paid plan. One that's been switched off still counts.
  • Each address hears about every page on your account. There's no way to pick particular pages.

What we tell you about

What we post about, and when
NameOn the formSent when
response.createdSomebody respondedSomebody answers on the page.
response.updatedSomebody changed their responseSomebody changes their answer, on the page or from their own link.
event.closedA page was closedSomebody presses "Stop taking responses" or locks in a time, or a page is closed through the API. Only once for each page: locking in a time on a page already closed one of those ways sends nothing more.
series.driftedA regular time stopped working as wellThe nightly check finds a repeating thing's time suits fewer people than it did, at the same moment we'd email you about it.

Some things send nothing: an answer being taken back or removed, a page closing because its date passed, opening it again, and anything on a shift schedule.

What a message looks like

A POST with a JSON body: id, event, createdAt and data. The id is the same as the Whenabouts-Delivery header, and stays the same if we try again.

POST https://your-server.example/whenabouts
Content-Type: application/json; charset=utf-8
User-Agent: Whenabouts-Webhooks/1
Whenabouts-Event: response.created
Whenabouts-Delivery: 01K6J0M2V8Q4R7T1W5X9Z3B6CD
Whenabouts-Signature: t=1790925865,v1=2da2030c4554...

{"id":"01K6J0M2V8Q4R7T1W5X9Z3B6CD","event":"response.created",
 "createdAt":"2026-10-02T18:22:05Z",
 "data":{"event":{"id":"01K6G7Z3QH8V4N2R5T9W0XJ6MB","kind":"rsvp",...},
   "response":{"id":"01K6H2C8W4M7P9R3T5V1X0Z2QA","name":"Sam","note":null,
     "payload":{"v":1,"answer":"Yes","guests":1},
     "createdAt":"2026-10-02T18:22:04Z","updatedAt":"2026-10-02T18:22:04Z"}}}
  • response.created and response.updated: data has the page and the answer, in the same shapes the API returns. The page is as it was just before the answer was saved, so its responseCount may not include it.
  • event.closed: data has the page.
  • series.drifted: the repeating thing's id, title, timezone, status and a summary sentence. There's no page in it.

No message ever contains an email address, on any plan.

Answering, retries and switching off

Answer with any 2xx within ten seconds and that message is done. We don't follow redirects.

  • No answer, a timeout, or a 408, 429 or 5xx, and we try again after 1, 2, 4 and 8 minutes: five tries in all, over about 15 minutes. These don't count against the address on their own.
  • Any other answer, a redirect or any other 4xx, is a refusal. Trying again would get the same answer, so we don't.
  • We switch an address off in two cases: one message has failed all five tries with nothing getting through to that address in the meantime, or five messages in a row were refused. Any message getting through clears both. We email you why, such as "It responded 404, so nothing is listening on that path".
  • A message that still hasn't got through after five tries is dropped.

A switched-off address says so on the page, with "Switch it back on". That starts the count again. Anything that happened while it was off isn't sent.

Messages can arrive out of order, and occasionally more than once. Use the Whenabouts-Delivery header to spot one you've already handled, and answer quickly, doing any slow work afterwards.

Checking it came from us

Every message carries a Whenabouts-Signature header like t=1790925865,v1=2da2…. To check it:

  1. Read the raw body before parsing it as JSON. Re-encoding it changes the bytes.
  2. Take t and v1 from the header.
  3. Refuse it if t, in seconds, is more than five minutes away from now, either way.
  4. Work out HMAC-SHA256 of t, a period, and the raw body, using your signing secret as the key, written as lowercase hex.
  5. Compare that with v1 using your language's constant-time comparison, not ==.

Each try is signed again, so a retry has a new t and v1 but the same body. In Node.js:

import crypto from "node:crypto";

// rawBody: the request body exactly as it arrived, before any JSON parsing.
// header: the Whenabouts-Signature header. secret: your signing secret.
export function isFromWhenabouts(rawBody, header, secret) {
  const parts = {};
  for (const part of (header ?? "").split(",")) {
    const [name, value] = part.split("=");
    if (name && value) parts[name.trim()] = value.trim();
  }
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;
  if (Math.abs(Date.now() - t * 1000) > 5 * 60 * 1000) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

In Python:

import hashlib
import hmac
import time


def is_from_whenabouts(raw_body: bytes, header: str, secret: str) -> bool:
    """raw_body is the request body exactly as it arrived, as bytes."""
    parts = dict(
        part.strip().split("=", 1) for part in (header or "").split(",") if "=" in part
    )
    try:
        t = int(parts["t"])
        given = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > 5 * 60:
        return False
    expected = hmac.new(
        secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected.encode(), given.encode())

On the free plan, and when a plan ends

Webhooks need a paid plan. On Free you can't add an address. If a paid plan ends, sending stops right away, every address says "Paused on the free plan", and we email you once. The addresses and secrets are kept, and sending starts again with the next thing that happens after you subscribe. Nothing that happened in between is sent.

Didn't find what you were after? The rest of the help pages might have it, or write to us and somebody will answer.

© WhenaboutsFind a timeSign-up sheetsRSVPTemplatesHelpPrivacyTermsSecurityAccessibilityContact