Docs

Receive webhooks

Register an inbox with Stripe, GitHub or Slack, wait for the event, and verify its signature.

Webhook providers need a public HTTPS URL. An inbox is one, and it records every delivery so your agent can wait for the event and read it in full.

1. Create an inbox#

create_inbox
{ "ttl_minutes": 120 }
result (abridged)
{
  "inbox_key": "porch_sk_…",
  "url": "https://porch.okibi.ai/i/Xk3vQ9…",
  "expires_at": "2026-10-18T14:00:00.000Z"
}

Keep the inbox_key: every other call needs it.

2. Register a path with the provider#

Use a path per provider, so you can filter and so each one is easy to spot in the inbox:

Provider Where to put the URL Example
Stripe Dashboard → Developers → Webhooks → Add endpoint, or stripe listen --forward-to <url>/stripe
GitHub Repository → Settings → Webhooks → Add webhook (content type application/json) <url>/github
Slack App settings → Event Subscriptions → Request URL <url>/slack
Twilio Phone number → Messaging → "A message comes in" <url>/twilio

Agents can often do this step themselves with the provider's CLI or API.

3. Wait for the event#

wait_for_request
{ "inbox_key": "porch_sk_…", "path_prefix": "/stripe", "timeout_seconds": 120 }
result (abridged)
{
  "timed_out": false,
  "cursor": "req_1",
  "request": {
    "id": "req_1",
    "method": "POST",
    "path": "/stripe",
    "headers": { "stripe-signature": "t=1729…,v1=5257a8…", "content-type": "application/json; charset=utf-8" },
    "json": { "type": "checkout.session.completed", "data": { "object": { "id": "cs_test_…" } } }
  }
}

If it times out, call it again. Requests that arrive between calls are never skipped.

4. Answer verification handshakes#

Some providers check the URL before sending events. Porch answers 200 by default, which satisfies most of them. For the ones that expect a specific body, set a response first:

set_response: Slack's url_verification expects the challenge back
{ "inbox_key": "porch_sk_…", "path_prefix": "/slack", "status": 200, "body": "<challenge value>" }

When the challenge changes per request (as Slack's does), read it from the captured request and answer from your own server instead, or replay the request locally to your app.

5. Verify the signature#

Anyone with the URL can post to it, so check signatures when the provider signs requests. Signatures are computed over the raw body: verify against the exact bytes, not re-serialized JSON. Fetch them with get_request (decode body if body_encoding is base64), or use the byte-exact replay.body_base64 it returns with replay_to.

GitHub signs with HMAC-SHA256 in x-hub-signature-256:

js
import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyGitHub(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex')
  return signatureHeader.length === expected.length &&
    timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
}

Stripe signs <timestamp>.<body> in stripe-signature; the simplest check is Stripe's own library:

js
import Stripe from 'stripe'

const event = Stripe.webhooks.constructEvent(rawBody, request.headers['stripe-signature'], endpointSecret)

Tips#