Webhook API
Two webhook surfaces: inbound (external systems push events into Martha that trigger workflows) and resume (external systems signal a paused workflow waiting on a human or callback).
Both are HTTPS-only and HMAC-signed.
Inbound webhooks (event triggers)
External services push events into Martha. Triggers consume them and start workflows.
Register
martha integrations webhooks create \
--name stripe-checkout \
--secret "$WEBHOOK_SECRET" \
--description "Stripe checkout.session.completed"This returns the receive URL — copy it into the source system's webhook config (e.g., Stripe dashboard).
Receive endpoint
POST https://<your-martha-instance>/api/webhooks/{webhook_id}| Header | Required | Purpose |
|---|---|---|
Content-Type: application/json | yes | The body is the event payload |
X-Webhook-Signature: sha256=<hex> | yes | HMAC-SHA256 of the raw body keyed on the webhook secret |
X-Webhook-Timestamp: <unix-seconds> | yes | Replay-protection timestamp; Martha rejects requests >5 min skewed |
The signed payload is {timestamp}.{raw_body}. Pseudocode:
sig = "sha256=" + hmac.new(
secret.encode(),
f"{timestamp}.{raw_body}".encode(),
"sha256",
).hexdigest()Receive returns 200 OK on success. Anything else means the event was rejected — check X-Martha-Error for the reason (invalid_signature, webhook_disabled, quota_exceeded).
Trigger consumption
Once received, the event becomes webhook.received on the platform event bus. Configure a trigger to start a workflow on that event type:
kind: Trigger
name: on-stripe-checkout
event_type: webhook.received
event_filter:
webhook_id: stripe-checkout
target_type: workflow
target_name: process-payment
input_mapping:
customer_id: "{{ event.data.customer }}"
amount: "{{ event.data.amount_total }}"See Event-driven triggers for the full filter and input-mapping syntax.
Workflow resume webhooks
A workflow node of type WAIT or APPROVAL_GATE pauses execution until an external signal arrives. The signal comes via a different endpoint, with a different security model — the signing key is per-workflow-execution, not a long-lived secret.
Endpoint
POST https://<your-martha-instance>/api/webhooks/resume/{workflow_id}Body
{
"event_name": "approval_received",
"data": {
"approver": "[email protected]",
"decision": "approved",
"comment": "Looks good"
}
}Headers
| Header | Required | Purpose |
|---|---|---|
Content-Type: application/json | yes | |
X-Workflow-Signature: sha256=<hex> | yes | HMAC-SHA256 of the raw body keyed on the workflow's signing secret |
The signing secret is returned in the workflow execution payload when it pauses. Capture it from pause.signing_secret — it's surfaced once and never logged.
Behavior
- Returns
200 OKif the workflow was waiting and resumed successfully. - Returns
404if the workflow isn't paused (already resumed, completed, or cancelled). - Returns
401on signature mismatch. - Returns
409if theevent_namedoesn't match what the WAIT node was expecting.
The workflow continues from the WAIT node with the posted body available as nodes.<node_id>.output.
Common patterns
Replay protection
Always check the X-Webhook-Timestamp header on inbound webhooks. Martha already rejects skewed requests, but if you persist webhook events for analytics, include the timestamp in your dedupe key.
Idempotency
Inbound webhooks may be retried by the source system. Consumers should be idempotent — Martha doesn't deduplicate at the receive layer. The platform-event bus does deliver each received webhook exactly once to triggers.
Tenant scoping
Every webhook URL is tenant-scoped via the webhook_id (inbound) or workflow_id (resume). A webhook secret leaked from tenant A cannot be used to deliver to tenant B even if the URLs look similar — the secret-to-tenant mapping is server-side and unforgeable.
Local development
Tunnel via cloudflared, ngrok, or tailscale-funnel to receive real webhooks against a local Martha. Trigger filters and signature checks are identical in dev and prod.
Related
- Event-driven triggers — wiring received events to workflows
- Human-in-the-loop — workflows that pause for external signals
- Notification channels — outbound webhooks (the inverse direction)