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.