Webhooks
Webhooks are the inbound direction: something outside your app calls in. A form tool posting a new response, a payment provider posting a receipt, a CRM posting a changed contact. API connections are the outbound direction — your app calling out. They are different doors and they are configured separately.
You add a webhook in the Webhooks tab: a name, and the collection deliveries should land in. The platform mints one long unguessable URL for it. You paste that URL into the outside service, and from then on every call it makes is written as a record.
The wire
Section titled “The wire”This is the one route Webhooks mount, and it is what the outside service calls. It isn’t a route your app code ever posts to — the token in it is the credential, and it’s yours to paste, not to publish.
| Method & path | Body | Returns |
|---|---|---|
POST /api/_hook/:name/:token |
whatever the outside service sends |
200 {"received":true} — written to that hook's collection |
There is nothing new to import
Section titled “There is nothing new to import”That’s the point. Webhooks add no module to import and no route for your app to call. A delivery is a record in a Data collection, so you read it with the import you already use:
import { collection } from "data";
const { items } = await collection("deliveries").list({ sort: "-createdAt", limit: 20 });Filtering, sorting, paging, aggregating and a Realtime socket all work on deliveries exactly as they do on anything else. A socket on the collection turns an arriving delivery into an on-screen update with no polling — which is what makes an inbox or an activity feed live.
Each record carries what the platform writes:
{ "id": "dlv_...", "body": { "…": "whatever they sent" }, "source": "typeform", "createdAt": 1753900000000}body is the payload, whole, as a json field. Outside services send shapes you don’t
control and change without warning, so it’s stored as it arrived rather than flattened into
columns. Reach into it with the dotted paths the data import already takes:
const { items } = await collection("deliveries").list({ "body.type": "invoice.paid" });and index what you filter by — "indexed": ["body.type"]. You can declare the same collection
in data.json to add your own fields alongside — a handled flag, an assignee for a
queue — and write them like any other record. The platform owns body and source; yours sit
next to them. A hook collection is scope shared.
The URL is the credential
Section titled “The URL is the credential”There is no signature to configure and no per-vendor setup, because every service signs differently and supporting each one would mean maintaining an integration per vendor. Instead the URL itself is unguessable — 32 random bytes — and that is what stands between your collection and the internet.
So treat it like a password:
- Never print it in the app or hand it to a visitor. Anyone holding it can write to that collection.
- Paste it only into the service that needs it. If it leaks, delete the hook and add it again — the new one gets a fresh URL, and the old one stops working immediately.
- The bot check never stands in front of a hook. A machine can’t solve a challenge, so the URL being unguessable is the whole of its protection.
The URL only exists once the app has been deployed, since it points at the app’s own address. Add the hook, ship, then copy it.
Deliveries repeat
Section titled “Deliveries repeat”Most services retry on any non-2xx response, and some retry anyway. The same event can land twice.
So treat a delivery as a fact that happened, not a command that runs once. If your app acts on one — sending a confirmation, granting access — make that action safe to repeat, and key it on something in the payload (an invoice id, an event id) rather than on the record’s own id. Deliveries also arrive whether or not anyone has the app open, which is the whole reason they’re written down rather than handled live.
Bodies are capped at 1 MB and a hook takes 120 deliveries a minute. Past either, the platform answers the sender and nothing is written.
Don’t write your own receiving route
Section titled “Don’t write your own receiving route”You can add any route you like to the app’s server file, and a route that receives a POST looks like it would do the job. It won’t: it has no way to tell a real delivery from a stranger who guessed the path, so it’s a public endpoint that writes to your database. That’s the hole this capability exists to close. The agent is told the same thing, and is given the list of hooks you’ve configured so it builds against the collection instead.