Inbound webhooks are how third-party services push events into Marfa. When an Integration connector declaresDocumentation Index
Fetch the complete documentation index at: https://docs.myme.so/llms.txt
Use this file to discover all available pages before exploring further.
triggers: webhook in its manifest, the platform exposes a public receipt endpoint per Connection. Senders POST events; the platform verifies, deduplicates, and dispatches to the per-Integration Worker.
This is the inverse of outbound webhooks: outbound delivers Marfa item events to URLs the tenant registers; inbound accepts events from external services into Marfa.
Receipt endpoint
runtime-control). The connection_id is the system.connection id created when the user installed the Integration. The path is published once per Connection and the sender configures it on their side; rotating it requires re-installing the Connection.
The receipt accepts whatever body shape the sender ships — JSON, form-encoded, raw bytes — and reads it as ArrayBuffer for signature verification. The body is preserved byte-for-byte through to the per-Integration Worker.
Signature verification
Signatures are checked against the adapter named inwebhook_verification.method on the Connection’s manifest. Six adapters ship with the platform:
| Method | What it verifies |
|---|---|
hmac-sha256 | Generic HMAC-SHA256 over the raw body, with timestamp anti-replay (5-minute default window). The default for connectors that don’t have a sender-specific quirk to honour. |
slack | Slack’s v0 signing scheme — concat timestamp + body, HMAC-SHA256 with the signing secret, prefix v0=, compare in constant time. |
stripe | Stripe’s t=...,v1=... format — extract timestamp + signatures, HMAC-SHA256 over t.body, accept any matching v1 signature. |
github | GitHub’s sha256=... format — strip the prefix, HMAC-SHA256 the raw body, constant-time compare. |
google-channel | Google’s channels.watch push notifications — verifies the X-Goog-Channel-Token header against the per-channel secret persisted on the Connection when the watch was created. Used by connectors subscribing to Google APIs that support channels.watch. |
cloudflare-email | Cloudflare Email Worker → server bridge for inbound-email integrations — verifies an HMAC-SHA256 signature stamped on the bridge payload by the Email Worker. Used by connectors that capture email via Cloudflare Email Routing. |
@withmarfa/webhooks — Web Crypto only, consumed by both the Cloudflare control plane and the server so signature behavior cannot drift between runtimes. Failed verification returns 401 invalid_signature; the body is not forwarded.
The verification method is declared in the Integration manifest, not on a per-receipt basis. Senders see exactly the verification path the connector author intended.
Deduplication
Each verified receipt carries a sender-supplied delivery id (extracted from the relevant header per adapter — Stripe’sStripe-Webhook-Id, GitHub’s X-GitHub-Delivery, etc.). The platform caches the id in KV for 1 hour; replays of the same (connection_id, delivery_id) return 200 ok without re-dispatching. Callers can safely retry on transient failures.
Adapters that don’t expose a canonical delivery-id header fall back to a request-time identifier — replay protection still works, but the window is per-attempt rather than per-event.
Dispatch
After verification and dedup, the platform enqueues aWebhookHandlerInput envelope on the per-Integration queue:
connection_id— the Connection that owns the subscription.external_delivery_id— the sender’s delivery id (or the request-time fallback).headers— the full request header set, surfaced to the handler.body— the raw bytes, decoded frombody_base64at the SDK seam into anArrayBuffer.
webhook trigger gets its own per-Integration webhook-receipt queue: Cloudflare Queues permit a single consumer per queue, so the control plane routes envelopes by integration_name to the matching producer binding. A shared receipt queue remains as a fallback for the one legacy in-tree connector that pre-dates the routing change. The per-Integration Worker reads its dedicated queue, dispatches to the connector’s handleWebhook function, and acks the message on success. Failed dispatches retry per-message with exponential backoff up to the queue’s max-attempts ceiling, then route to the per-Connection DLQ.
Failures and the DLQ
Each Connection has its own dead-letter queue. A receipt that exhausts its retry budget — handler keeps returningretry: true, or throws on every retried attempt — lands in the DLQ rather than disappearing.
Operators inspect failed deliveries through the Connection’s audit row and the system.activity rows the handler emits at severity: error. Each DLQ entry preserves the verified envelope plus the failure reason.
Manual replay
Operators can re-dispatch a failed delivery once the underlying problem is fixed:Subscription management
Subscriptions are part of the Connection’s lifecycle, not a separate resource. They’re created at install time when the manifest declarestriggers: webhook, and they’re revoked when the Connection is uninstalled. The CRUD surface — list subscriptions, inspect a delivery, replay — sits under /connections/{id}/inbound-webhooks/.... See the Connections API reference for the full set.