On this page

Inbound webhooks

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 200 and 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

  1. Open Studio › Integrations › Inbound webhooks and choose Turn on inbound webhooks (project owners and admins). This adds the inbound schema to your database.
  2. Add endpoint: pick who sends it and a name for the URL, for example stripe. The URL is https://<project-ref>.p.stitchdb.com/inbound/v1/stripe.
  3. 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.
  4. Use Send a test event to try your trigger or function without waiting for a real event.

#Senders

Who sends itWhat is checkedEvent id and type
StripeStripe-Signature: HMAC-SHA256 of timestamp.body with the endpoint’s signing secret, at most 5 minutes oldid and type from the body
GitHubX-Hub-Signature-256: sha256= HMAC-SHA256 of the bodyX-GitHub-Delivery; X-GitHub-Event plus the body’s action, e.g. issues.opened
ShopifyX-Shopify-Hmac-Sha256: base64 HMAC-SHA256 of the bodyX-Shopify-Webhook-Id; X-Shopify-Topic, e.g. orders/create
Standard Webhookswebhook-id, webhook-timestamp, webhook-signature (or the svix- names) with a whsec_ secret, at most 5 minutes oldwebhook-id; type from the body
HMAC signatureThe 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 tokenA long random token in ?token= or the X-Webhook-Token header, for senders that cannot signAs 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

ColumnWhat it holds
idA number that grows with each event
endpointThe endpoint’s name in the URL, e.g. stripe
event_type, event_idThe sender’s event type and id (see above)
payloadThe body as JSON (or body as text)
headersThe sender’s descriptive headers (event, delivery id, user agent); never signatures
statusreceived (stored), pending (being handed to the Edge Function), delivered or failed
attempts, last_error, next_attempt_at, delivered_atEdge Function delivery
testTrue for test events sent from Studio
received_atWhen 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:

SQL
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.

TypeScript
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.

LimitFreeProScaleEnterprise
Inbound webhook endpoints per project220100500
Inbound webhook events per project per month10,0001,000,0005,000,00050,000,000