AtTable posts a signed JSON payload to your endpoint when an event happens on your organisation (KYB approved/rejected, etc.). You verify the signature with the secret you saved when registering the endpoint, then act on the event.
Respond 2xx within 5 seconds to acknowledge. Anything else is treated as a failure and retried with exponential backoff.
After 20 consecutive failures the endpoint is auto-disabled. You can re-enable it from the dashboard.
Every delivery is identified by AtTable-Event-Id — use it as your idempotency key.
{
"id": "evt_01HXYZ…", // delivery id (idempotency key)
"type": "org.verification_approved", // event type
"organisationId": "9f7…", // your org id
"createdAt": "2026-05-14T09:12:33Z", // ISO 8601 UTC
"data": { … } // event-specific body
}
More event types are coming (member changes, plan changes, payout events). The envelope is stable — you can subscribe today and your handler keeps working as new fields land in data.
HMAC-SHA256 verification in Node, Python, and Ruby.
Every request includes three headers:
Header
Value
AtTable-Event
The event type (e.g. org.verification_approved)
AtTable-Event-Id
The delivery id — use as your idempotency key
AtTable-Signature
t=<unix-timestamp>,v1=<hex hmac>
The signature is HMAC_SHA256(secret, "<timestamp>.<raw-body>").
Sign the raw body
Sign the raw request body bytes, not a re-serialised JSON. If your framework parses JSON before you can see the raw body, re-serialisation will reorder keys and the HMAC will not match.
Common integration patterns: billing, CRM, fan-out.
Billing — provision when KYB is approved
When an org's KYB passes, flip them from “pending” to “active” in your downstream billing/ledger system, generate a customer record, and unlock production usage.
Subscribe to:org.verification_approved
typescript
async function handleApproved(event: VerificationApprovedEvent) {
const { organisationId, data } = event;
// 1. Find or create the customer in your billing tool.
const customer = await ourBilling.customers.upsert({
externalId: organisationId,
name: data.legalName,
country: data.countryCode,
status: 'active',
});
// 2. Provision the entitlements your product gates on.
await entitlements.grant(customer.id, ['production-api', 'high-volume-tier']);
// 3. Notify the AM in Slack.
await slack.send(
'#sales',
`:white_check_mark: ${data.legalName} approved — provisioned in billing.`
);
}
Idempotency
Index your attable_events table on event.id so a retried delivery doesn't double-provision.
If you're currently running a cron that polls GET /api/orgs/:id/... every N minutes to detect state changes, replace it with a webhook. You get a signed HTTPS POST the moment the event happens — sub-second latency, no quota churn, no missed-window bugs.
Data warehouse: push every event into BigQuery / Snowflake via Fivetran's HTTP source.
Status pages: flip your internal “AtTable up?” indicator when webhook.test arrives successfully.
Notification fan-out: receive once, fan out to Slack + email + PagerDuty.
Skeleton handler
typescript
const handlers: Record<string, (e: AtTableEvent) => Promise<void>> = {
'org.verification_approved': handleApproved,
'org.verification_rejected': handleRejected,
'webhook.test': async () => { /* no-op, just 200 */ },
};
app.post('/hooks/attable', verifySignature, async (req, res) => {
const event = JSON.parse(req.body.toString('utf8')) as AtTableEvent;
// De-dupe — same delivery id may arrive twice if our ack was lost.
if (await seenEvents.has(event.id)) return res.status(200).send('dup');
await seenEvents.add(event.id, { ttlSeconds: 24 * 60 * 60 });
const handler = handlers[event.type];
if (handler) await handler(event);
return res.status(200).send('ok');
});
You have 5 seconds to return a 2xx. If your handler does heavy work, push the event onto a queue inside the HTTP handler and return 200 immediately, then process async.
Retry & failure policy
Behaviour
Value
Connect timeout
5s
Response body cap
2 KB (we won't read more)
Successful status
2xx
Retry on
Network error, non-2xx, timeout
Backoff
Exponential (1m, 5m, 30m, 2h, 6h, …)
Auto-disable after
20 consecutive failures
Replay protection
Reject events whose timestamp (t from the signature header) is more than 5 minutes away from your server clock. Make sure your servers run NTP.
Idempotency
AtTable-Event-Id (also available as event.id) is the canonical idempotency key. Store it for at least 24 hours.
Don't expose internal services
We resolve your URL's hostname on every delivery and refuse to connect to RFC1918 / loopback / link-local IPs (127.0.0.0/8, 10/8, 192.168/16, etc.) even if your DNS resolves to them. This is an anti-SSRF measure and is not configurable.
Rotate secrets
If a secret is leaked, click Reveal new secret in the dashboard. The old key is invalidated immediately — deploy the new one before rotating in production.
Why isn't my endpoint receiving?
Disabled? Red badge in the dashboard means we auto-disabled after 20 consecutive failures.
Wrong event types? The subscription is a positive allow-list.
Signature mismatch? Use Send test — it shows the exact body and headers so you can repro the HMAC locally.
Hostname resolving to a private IP? The Delivery log says private host.
Slow handler? Anything over 5s counts as a timeout.