Falaah Falaah AI

Webhooks

Receive Muin events on your own server: create an outgoing webhook, the events that deliver today, the payload and headers, signature checks and retries.

An outgoing webhook makes Muin send an HTTP POST to your server each time a chosen event happens, so your system does not have to poll.

When you’d use this

  • Update your website or CRM when an invoice is paid or an event registration comes in.
  • Start your own processing when a document finishes extraction.
  • Keep an external attendance list in step with check-ins.

How it works

  1. You create a webhook with your URL and the events you want.
  2. When one of those events happens, Muin sends a signed POST to your URL.
  3. Your server checks the signature, answers with a 2xx status, and does its work.
  4. If your server fails, Muin retries, then pauses the webhook if failures continue.

Create a webhook

In the app

  1. Open Settings → Integrations → Webhooks, select outgoing, and click Create Webhook.
  2. Enter a Name and your URL, then tick the events you want.
  3. Copy the Webhook Secret. It is shown once.

Field-by-field steps, including incoming webhooks: Setting Up Webhooks.

With the API

Use an API key that has the Write scope (Authentication).

curl -X POST https://muin-api.falaah.ai/api/v1/webhooks/outgoing \
  -H "Authorization: Bearer muin_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Website sync",
    "url": "https://your-server.example/webhooks/muin",
    "subscribed_events": ["invoice.paid", "registration.created"],
    "description": "Production webhook"
  }'

subscribed_events must list at least one event. Optional fields: custom_headers (extra headers sent with every delivery) and max_retries (0 to 10, default 5).

The response carries the webhook and its signing secret, shown once:

{
  "webhook": {
    "id": "6f1c9a52-3d0e-4b7a-9a43-2f0d8e1b7c11",
    "name": "Website sync",
    "url": "https://your-server.example/webhooks/muin",
    "subscribed_events": ["invoice.paid", "registration.created"],
    "max_retries": 5,
    "is_active": true,
    "health_status": "healthy"
  },
  "secret": "whsec_..."
}

Events that deliver today

Only these events reach a webhook. The event picker in the app offers exactly this list.

Event Fires when
document.processed A document finishes processing
invoice.created An invoice is created
invoice.approved An invoice is approved
invoice.paid An invoice is paid
calendar.event.published An event is published to your public calendar
calendar.event.unpublished A published event is taken down
calendar.event.cancelled A published event is cancelled
registration.created Someone registers for an event
registration.cancelled A registration is cancelled
registration.checked_in A registrant is checked in
registration.no_show A registrant is marked as a no-show
event.capacity_full An event reaches capacity

The machine-readable list, with a deliverable flag on every event, is at GET /api/v1/integrations/events/catalog. Events marked deliverable: false are declared but nothing sends them to webhooks yet, and a test send for one is refused.


Payload

Every delivery has the same envelope:

{
  "id": "evt_8sQ1o2k3HhYz0m4pX7rT1w",
  "type": "invoice.paid",
  "created": "2026-10-05T17:30:28.000000+00:00",
  "data": {
    "...": "event-specific fields"
  }
}

Headers

Header Description
Content-Type application/json
X-Muin-Event Event type, for example invoice.paid
X-Muin-Delivery-Id Unique delivery ID. Use it to ignore duplicates
X-Muin-Signature HMAC-SHA256 signature, t=<unix>,v1=<hex> (see below)
X-Muin-Timestamp Unix time the delivery was signed

Any custom_headers you set on the webhook are added to these.


Verifying Signatures

Every webhook Muin sends includes an X-Muin-Signature header. That covers both senders: event-subscription webhooks (the ones you create above) and workflow webhook actions, which post to a URL on your organization’s allowed list. Both use the scheme below and the same verification code, so you write the check once.

The one exception: a workflow allowed-URL entry registered before per-destination signing existed has no secret yet, and its deliveries carry no signature. Rotate its secret once (POST /api/v1/workflows/allowed-urls/{id}/rotate-secret) and every later delivery to that destination is signed.

The header

X-Muin-Signature: t=1742049028,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-Muin-Timestamp: 1742049028

t is the unix-second timestamp and v1 is the hex HMAC. The timestamp is part of the signed material, so a captured delivery cannot be replayed with a newer date.

How to Verify

  1. Split the header on , and read the t and v1 values
  2. Concatenate the timestamp and the RAW request body: {t}.{body}
  3. Compute HMAC-SHA256 over that string using your webhook secret
  4. Compare the hex digest with v1, using a constant-time comparison
  5. Reject a delivery whose t is more than five minutes away from your own clock

Verify against the raw body bytes exactly as received. Parsing the JSON and re-serializing it changes the characters and the signature will not match.

Example (Node.js)

const crypto = require('crypto');

function verifyWebhook(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=', 2))
  );
  if (!parts.t || !parts.v1) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(parts.v1);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Example (Python)

import hashlib
import hmac


def verify_webhook(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    timestamp, provided = parts.get("t"), parts.get("v1")
    if not timestamp or not provided:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, provided)

Where each secret comes from

Sender Secret
Event-subscription webhooks Shown once when you create the webhook; regenerate it from Settings → Integrations → Webhooks (Regenerate Secret)
Workflow webhook actions Shown once when you add the allowed URL; rotate it with POST /api/v1/workflows/allowed-urls/{id}/rotate-secret

Workflow secrets are held per destination, not per organization, so one partner’s secret never verifies another partner’s deliveries.


Retries and pauses

Muin waits up to 30 seconds for your server. Anything other than a 2xx answer counts as a failure.

Attempt When
1 Immediately
2 1 minute after attempt 1
3 2 minutes after attempt 2
4 4 minutes after attempt 3
5 8 minutes after attempt 4

That is the default of 5 attempts; max_retries changes it. After the last attempt fails, the delivery is marked failed.

If 5 deliveries in a row fail, the webhook is paused for 30 minutes (the app shows Circuit Open) and then resumes on its own.

Failed deliveries

Open Settings → Integrations → Webhooks, open the webhook, and look at Recent Deliveries. Each row shows the event, status, attempts and your server’s HTTP status code. The API’s delivery list also returns the error message. Retry a failed delivery from there, or with the API below.


Managing webhooks with the API

All paths are on https://muin-api.falaah.ai.

Action Request
List webhooks GET /api/v1/webhooks/outgoing
Get one GET /api/v1/webhooks/outgoing/{webhook_id}
Change name, URL, events, headers, or turn on/off PATCH /api/v1/webhooks/outgoing/{webhook_id} with any of name, description, url, subscribed_events, custom_headers, is_active
Send a test delivery POST /api/v1/webhooks/outgoing/{webhook_id}/test
List deliveries GET /api/v1/webhooks/outgoing/{webhook_id}/deliveries
Retry a failed delivery POST /api/v1/webhooks/outgoing/{webhook_id}/deliveries/{delivery_id}/retry
Replace the signing secret POST /api/v1/webhooks/outgoing/{webhook_id}/regenerate-secret
Delete DELETE /api/v1/webhooks/outgoing/{webhook_id}

Regenerating the secret takes effect immediately. There is no overlap period, so update your server’s verification at the same moment.


When it goes wrong

What you see Why What to do
Signatures never match Verifying a re-serialized body, or using an old secret. Hash the raw body bytes with the current secret.
An event never arrives It is not in the list of events that deliver today. Subscribe to a deliverable event.
Deliveries stopped for a while, then resumed The webhook paused for 30 minutes after 5 failed deliveries in a row. Fix the endpoint, then retry the failed deliveries.
403 from the API Your API key lacks the Write scope, or the person who created it cannot manage webhooks. Use a key with Write, created by an owner or admin.

Best practices

  • Answer fast. Return 2xx right away and do slow work in the background.
  • Expect duplicates. Use X-Muin-Delivery-Id to process each delivery once.
  • Always verify the signature before acting on a delivery.
  • Use HTTPS for your endpoint.

Next step