Receive webhooks from Stripe, GitHub, Shopify or any service: each event is checked against its signing secret and stored as a row in your database, optionally handed to an Edge Function.
Inbound webhooks give each service that sends you webhooks its own URL on your project. Every event that arrives is checked against the endpoint's signing secret and stored as a row in inbound.events in your database. From there a trigger, a cron job or your server picks it up, and an endpoint can also hand each event to an Edge Function, retried until it succeeds. You don't run a server to receive webhooks, and you don't write signature checks.
- Verified: Stripe, GitHub, Shopify and Standard Webhooks (Svix, Clerk, Resend and others) signatures are checked out of the box. Any other sender works with an HMAC signature header you describe, or a secret token in the URL.
- Stored once: senders retry, so the same event often arrives twice. An event with an id the endpoint already has is answered with
200and not stored again. - Not lost: the event is stored before the sender gets its
200. If your Edge Function is down, delivery is retried with backoff.
#Set it up
- Open Studio › Integrations › Inbound webhooks and choose Turn on inbound webhooks (project owners and admins). This adds the
inboundschema to your database. - Add endpoint: pick who sends it and a name for the URL, for example
stripe. The URL ishttps://<project-ref>.p.stitchdb.com/inbound/v1/stripe. - Add the URL in the sender's dashboard. For Stripe and Shopify, paste the signing secret they show into Studio. For GitHub, Standard Webhooks, HMAC and token endpoints, Studio generates a secret for you to give the sender, or you paste theirs.
- Use Send a test event to try your trigger or function without waiting for a real event.
#Senders
| Who sends it | What is checked | Event id and type |
|---|---|---|
| Stripe | Stripe-Signature: HMAC-SHA256 of timestamp.body with the endpoint’s signing secret, at most 5 minutes old | id and type from the body |
| GitHub | X-Hub-Signature-256: sha256= HMAC-SHA256 of the body | X-GitHub-Delivery; X-GitHub-Event plus the body’s action, e.g. issues.opened |
| Shopify | X-Shopify-Hmac-Sha256: base64 HMAC-SHA256 of the body | X-Shopify-Webhook-Id; X-Shopify-Topic, e.g. orders/create |
| Standard Webhooks | webhook-id, webhook-timestamp, webhook-signature (or the svix- names) with a whsec_ secret, at most 5 minutes old | webhook-id; type from the body |
| HMAC signature | The header you choose (default X-Signature), SHA-256, SHA-1 or SHA-512, hex or base64, with an optional prefix such as sha256= | id and type (or event) from the body |
| Secret token | A long random token in ?token= or the X-Webhook-Token header, for senders that cannot sign | As for HMAC |
A request that fails the check is refused with 401 and a message saying why, which shows up in the sender's delivery log. JSON bodies are stored in payload; form bodies (application/x-www-form-urlencoded) are turned into JSON; anything else is kept as text in body.
#The events table
| Column | What it holds |
|---|---|
id | A number that grows with each event |
endpoint | The endpoint’s name in the URL, e.g. stripe |
event_type, event_id | The sender’s event type and id (see above) |
payload | The body as JSON (or body as text) |
headers | The sender’s descriptive headers (event, delivery id, user agent); never signatures |
status | received (stored), pending (being handed to the Edge Function), delivered or failed |
attempts, last_error, next_attempt_at, delivered_at | Edge Function delivery |
test | True for test events sent from Studio |
received_at | When it arrived |
The inbound schema is not readable with your project API keys, not even the anon or authenticated roles: it holds whatever senders send. Read it from SQL, triggers, Edge Functions with a database connection, or your server. To react to an event as it arrives, add a trigger:
create or replace function public.on_stripe_event() returns trigger
language plpgsql security definer set search_path = public as $$
begin
if new.event_type = 'checkout.session.completed' then
update orders set paid = true
where checkout_id = new.payload -> 'data' -> 'object' ->> 'id';
end if;
return new;
end $$;
create trigger on_stripe_event after insert on inbound.events
for each row when (new.endpoint = 'stripe')
execute function public.on_stripe_event();#Hand events to an Edge Function
Set an endpoint's Edge Function and every event is also sent to it as POST with { event: { id, endpoint, type, event_id, payload, headers, received_at, attempt } }. The request carries your project's service key and an X-Stitchdb-Inbound header (t=<unix time>,v1=<hex HMAC-SHA256 of "t.body" with the service key>), so the function can check that the call came from your project.
Deno.serve(async (req) => {
const { event } = await req.json()
if (event.type === 'orders/create') await fulfil(event.payload)
return new Response('ok')
})Any answer other than 2xx, or no answer within 100 seconds, is retried after 10 seconds, 40 seconds, 3 minutes, 11 minutes, 43 minutes, 3 hours and 6 hours. After 8 attempts the event is marked failed; Deliver again in Studio starts over. The function can get the same event more than once (after a timeout, for example), so use event.id to skip ones it already handled.
#Keeping events
Events are kept until you delete them. Each endpoint can instead keep them for 1 day to a year; older events are deleted once an hour, except ones still being delivered. You can also delete them in SQL: delete from inbound.events where received_at < now() - interval '30 days'.
#Secrets
Signing secrets are stored encrypted and only owners and admins can see them in Studio. Generate new replaces the secret at once; events signed with the old one are refused from then on, so update the sender at the same time. Pausing an endpoint (Accept events off) answers 404, which makes most senders retry later. Turning inbound webhooks off does the same for every endpoint and keeps endpoints, secrets and events.
#Limits
Bodies can be up to 1 MB. Once the project has received its plan's events for the month, new events are refused with 429 until the next month; senders retry, so nothing is lost if you upgrade.
| Limit | Free | Pro | Scale | Enterprise |
|---|---|---|---|---|
| Inbound webhook endpoints per project | 2 | 20 | 100 | 500 |
| Inbound webhook events per project per month | 10,000 | 1,000,000 | 5,000,000 | 50,000,000 |