Verifying and rotating your signing secret

Available in the Enterprise and Pro plans. View plans

What this covers

Verifying that a webhook genuinely came from Tabledoo, and replacing the secret without losing deliveries. Requires the Webhooks feature and the manage-settings permission.

Why verification is not optional

Your webhook URL sits on the public internet. Anybody who learns it can post anything to it.

Without verification, your endpoint will believe whatever it is sent. Somebody could invent bookings in your till, or mark tables served. It does not take a determined attacker — a URL in a chat message, a screenshot, a log file somebody pastes.

An unverified endpoint is an open door. Verification is a handful of lines and it closes it completely.

How it works

Each endpoint has its own signing secret — a long random string known only to Tabledoo and you. Every request carries a header:

X-Tabledoo-Signature-256: sha256=<hex digest>

The digest is an HMAC-SHA256 of the exact raw request body, keyed with your secret. You recompute it and compare.

Verifying, in principle

  1. Read the raw body as it arrived — bytes, not a re-serialised object.
  2. Compute HMAC-SHA256(raw_body, your_secret) as lowercase hex.
  3. Prefix it with sha256=.
  4. Compare with the header using a timing-safe comparison.
  5. If they differ, reject the request and do not process it.

Most languages have this built in — hash_hmac and hash_equals in PHP, crypto.createHmac and timingSafeEqual in Node, hmac and compare_digest in Python.

The mistake almost everybody makes

Sign the raw body, not a re-encoded version of it. If your framework parses the JSON and you re-encode it to verify, the bytes will differ — different key order, different spacing, different escaping — and every signature will fail while your code looks correct.

Most frameworks need an explicit step to keep the raw body for exactly this reason. If your signatures all fail and nothing seems wrong, this is the cause about nine times in ten.

Where to find the secret

It is shown once, when you register the endpoint, and cannot be retrieved afterwards — only replaced.

Store it where your application reads configuration — an environment variable or your secret manager. Not in your code, and not in a repository.

Lost it? Rotate to get a new one.

The secret expires after a year

Read this even if you skip the rest.

Unless you chose keep alive when registering the endpoint, the secret expires one year after it was issued. When it does, deliveries stop.

And they stop silently: no delivery record is created, so the delivery log shows nothing at all rather than a failure. Your integration simply goes quiet, a year after somebody set it up and forgot.

The settings page flags an expired secret, so that is where to look — but nothing comes to find you. Two sensible responses:

  • Choose keep alive for a production integration, and put your own annual reminder to rotate deliberately.
  • Or accept the expiry and record the date somewhere you will actually see it.

Either way, write the date down. This is the single most likely reason a working webhook integration stops working.

Rotating the secret

Rotate when a secret may have been exposed, when a developer or agency stops working with you, when the expiry approaches, or on a schedule you set.

Rotation is immediate, with no overlap

The important mechanical detail: the new secret replaces the old one at once. There is no grace period during which both work. The moment you rotate, deliveries are signed with the new secret only — and an endpoint still checking the old one will reject every request as a forgery.

So roll it in this order

  1. Make your endpoint accept either secret. Verify against both, and accept if either matches. Deploy that first.
  2. Rotate in Tabledoo. Copy the new secret — shown once.
  3. Add the new secret to your configuration alongside the old one.
  4. Confirm real deliveries are arriving and verifying — use the test action and watch the delivery log.
  5. Remove the old secret from your endpoint.

Done this way there is no gap. Done in the obvious order — rotate first, update your server afterwards — every delivery in between is rejected by your own code, and those are retried only four times over about an hour before being abandoned.

If your endpoint cannot easily accept two secrets, the pragmatic alternative is to deactivate the endpoint, rotate, update your server, then reactivate. You lose the events in that window, but you lose them visibly rather than as rejected deliveries.

What to do with a request that fails verification

  • Return a 4xx and do not process it. Never fall back to trusting it.
  • Log it — the delivery id, the time, the source address.
  • Investigate if it persists. Occasional failures around a rotation are expected; a steady stream is either a bug in your verification or somebody probing your endpoint.

Good to know

Each endpoint has its own secret. Rotating one does not affect another, which is another reason to register one endpoint per consuming system.

The test payload is signed identically, so it is a genuine test of your verification (link Webhooks).

Verification is about authenticity, not encryption. HTTPS protects the contents in transit; the signature proves who sent it. You need both, which is why HTTPS is required.

Nobody at Tabledoo can tell you your secret. Only a scrambled form is usable for comparison — rotate instead.

Where to go next