tabsettleDEVELOPERS
Webhooks

Webhooks.

Signed, retried notifications when checks and payments change.

Event types.

check.created check.updated check.closed check.fully_paid payment.succeeded payment.refunded claim.created feedback.received

Every delivery is a POST with this envelope — data is the resource’s full current state, identical to what the matching GET returns:

JSON
{
  "id": "…",
  "type": "check.fully_paid",
  "created_at": "2026-08-24T19:04:11Z",
  "data": { }
}

Registering an endpoint.

Requires write scope. URLs must be HTTPS.

curl -X POST https://api.tabsettle.com/v1/webhook-endpoints \
  -H "Authorization: Bearer ts_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-server.example.com/tabsettle-webhook",
        "event_types": ["check.fully_paid", "payment.succeeded"] }'
await fetch('https://api.tabsettle.com/v1/webhook-endpoints', {
  method: 'POST',
  headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://your-server.example.com/tabsettle-webhook',
    event_types: ['check.fully_paid', 'payment.succeeded'],
  }),
});
The response is the only place your signing secret (whsec_…) is ever returned — store it immediately.

Use POST /v1/webhook-endpoints/{id}/test to send yourself a signed ping and verify your receiver end-to-end before going live.

Verifying signatures.

Every delivery carries:

HTTP HEADER
X-TabSettle-Signature: t=<unix_seconds>,v1=<hex hmac-sha256(secret, "<t>.<rawBody>")>

If you’ve verified a Stripe webhook, this is the same scheme:

NODE
const crypto = require('node:crypto');

function verifyTabSettleWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((kv) => kv.split('='))
  );
  const t = Number(parts.t);
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  if (expected !== parts.v1) throw new Error('signature mismatch');
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) throw new Error('timestamp outside tolerance — possible replay');
  return JSON.parse(rawBody);
}
Verify against the raw request body — re-serializing JSON can change byte order and break the signature.

Delivery semantics.

  • At-least-once: dedupe on the envelope id.
  • Order is not guaranteed — but every payload is full state, so processing duplicates or out-of-order deliveries is harmless.
  • Respond 2xx within 10 seconds. Anything else is retried on this backoff: 1m, 5m, 30m, 2h, 8h, 24h (≈35 hours total) before the delivery is marked dead.
  • An endpoint that fails many deliveries in a row is automatically disabled to protect both sides — fix your receiver and re-register.