Skip to content

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

bash
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}
HeaderRequiredPurpose
Content-Type: application/jsonyesThe body is the event payload
X-Webhook-Signature: sha256=<hex>yesHMAC-SHA256 of the raw body keyed on the webhook secret
X-Webhook-Timestamp: <unix-seconds>yesReplay-protection timestamp; Martha rejects requests >5 min skewed

The signed payload is {timestamp}.{raw_body}. Pseudocode:

python
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:

yaml
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

json
{
  "event_name": "approval_received",
  "data": {
    "approver": "[email protected]",
    "decision": "approved",
    "comment": "Looks good"
  }
}

Headers

HeaderRequiredPurpose
Content-Type: application/jsonyes
X-Workflow-Signature: sha256=<hex>yesHMAC-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 OK if the workflow was waiting and resumed successfully.
  • Returns 404 if the workflow isn't paused (already resumed, completed, or cancelled).
  • Returns 401 on signature mismatch.
  • Returns 409 if the event_name doesn'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.

Martha is built by aiaiai-pt.