Skip to content
Als Markdown

Webhooks

Webhooks deliver HTTP POST notifications to your server when events occur in Mottainai. Use them to trigger external systems (n8n, Zapier, custom services) without maintaining a persistent WebSocket connection.

Quick start

  1. Go to Settings > Developer > Webhooks and click Add webhook.
  2. Enter your HTTPS endpoint URL.
  3. Select the events you want to receive (or leave empty for all events).
  4. Copy the signing secret — it is shown only once.
  5. Click Send test event to verify your endpoint.

You can also manage webhooks via the CLI (mo webhooks) or the API.

Event types

EventTrigger
item.createdAn item is added to a container
item.updatedAn item's fields are modified (includes changed_fields)
item.deletedAn item is removed
item.expiredA storage item's expiration date is today
item.expiring_soonA storage item expires within 7 days (also fires every 10 days for overdue items)
container.createdA new container is created
container.updatedA container is renamed or its settings change
container.deletedA container is deleted
member.addedA user is added to a container
member.removedA user is removed from a container
webhook.testSent manually via the test button (not queued)

Subscribe to specific events in the endpoint configuration, or leave the list empty to receive all events.

Payload format

Every webhook delivery is an HTTP POST with Content-Type: application/json. The payload follows a consistent envelope:

json
{
  "schema_version": "1",
  "event": "item.created",
  "event_id": "a1b2c3d4-...",
  "created_at": "2026-03-11T10:30:00Z",
  "actor_id": "user-uuid",
  "container_id": "container-uuid",
  "container_type": "storage",
  "data": { ... },
  "batch_id": null
}

Fields

FieldTypeDescription
schema_versionstringAlways "1"
eventstringEvent type (e.g. item.created)
event_idUUIDUnique ID for this event
created_atISO 8601When the event occurred
actor_idUUIDUser who triggered the action (absent for system events)
container_idUUIDAffected container
container_typestringstorage, board, calendar, etc.
dataobjectEvent-specific payload (see below)
batch_idUUIDPresent when multiple events share a batch operation

Data by event type

Item events (item.created, item.updated, item.deleted):

json
{
  "item_id": "item-uuid",
  "item_type": "storage",
  "title": "Oat Milk",
  "category": "Dairy",
  "changed_fields": ["quantity"],
  "snapshot": { "id": "item-uuid", "title": "Oat Milk", "quantity": 3, "..." : "..." }
}

changed_fields is only present on item.updated. snapshot contains the full item as returned by the API and is included on item.created and item.updated (omitted on item.deleted and positional updates like card moves).

Expiry events (item.expired, item.expiring_soon):

json
{
  "item_id": "item-uuid",
  "title": "Yogurt",
  "expiration_date": "2026-03-11",
  "quantity": 2,
  "category": "Dairy"
}

These are system-generated — actor_id is absent.

Container events (container.created, container.updated, container.deleted):

json
{
  "container_id": "container-uuid",
  "container_type": "storage",
  "name": "Fridge"
}

Member events (member.added, member.removed):

json
{
  "user_id": "user-uuid",
  "email": "user@example.com",
  "name": "Alice",
  "role": "member"
}

Test event (webhook.test):

json
{
  "schema_version": "1",
  "event": "webhook.test",
  "event_id": "...",
  "created_at": "2026-03-11T10:30:00Z",
  "actor_id": "user-uuid",
  "message": "This is a test event from Mottainai."
}

HTTP headers

Every delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
User-AgentMottainai-Webhook/1.0
X-Mottainai-EventEvent type (e.g. item.created)
X-Mottainai-DeliveryUnique delivery ID (UUID)
X-Mottainai-Signature-256HMAC-SHA256 signature (sha256=<hex>)

Signature verification

Every payload is signed with your endpoint's secret using HMAC-SHA256. Verify the signature to confirm the request came from Mottainai and was not tampered with.

How it works

  1. Mottainai computes HMAC-SHA256(secret, raw_request_body).
  2. The result is sent as X-Mottainai-Signature-256: sha256=<hex-digest>.
  3. Your server recomputes the HMAC and compares using constant-time comparison.

Example (Node.js)

javascript
import crypto from 'node:crypto';

function verifySignature(secret, payload, signature) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// In your handler:
const signature = req.headers['x-mottainai-signature-256'];
const body = await getRawBody(req); // raw bytes, not parsed JSON
if (!verifySignature(process.env.WEBHOOK_SECRET, body, signature)) {
  return res.status(401).send('Invalid signature');
}

Example (Python)

python
import hashlib
import hmac

def verify_signature(secret: str, payload: bytes, signature: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Example (Go)

go
func verifySignature(secret string, payload []byte, signature string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Secret rotation

Rotate secrets without downtime:

  1. Call POST /api/webhooks/{id}/rotate-secret (or click Rotate secret in Settings).
  2. A new secret is generated and returned once.
  3. The old secret remains valid for 1 hour (the response includes expires_old_at).
  4. During the overlap window, verify against both the old and new secrets.
  5. After 1 hour, the old secret is no longer valid.

Retry behavior

Failed deliveries are retried with exponential backoff:

AttemptDelay after failure
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 hours
724 hours
After 7Marked as dead

Total retry window: approximately 35 hours.

A delivery is considered successful if your server returns any 2xx status code. Any other status, timeout, or connection error triggers a retry.

Dead deliveries

Dead deliveries can be retried manually from Settings > Developer > Webhooks > Logs, or via POST /api/webhooks/{id}/deliveries/{deliveryId}/retry.

Auto-disable

If an endpoint has only failed or dead deliveries for 7 consecutive days (no successful delivery), the endpoint is automatically disabled. You receive an email notification. Re-enable the endpoint in Settings after fixing the issue. Missed events are not replayed.

SSRF restrictions

To prevent abuse, webhook URLs are validated before every delivery:

  • HTTPS required (http:// is allowed only in development mode)
  • Private IP ranges blocked: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8, 100.64.0.0/10, ::1/128, fc00::/7
  • DNS is re-resolved on every attempt to prevent DNS rebinding attacks

If your URL resolves to a blocked IP, the delivery fails immediately with a clear error message.

CLI

bash
mo webhooks list
mo webhooks create --url "https://example.com/hook" [--events item.created,item.updated] [--containers ID1,ID2] [--description "My hook"]
mo webhooks get WEBHOOK_ID
mo webhooks update WEBHOOK_ID --url "https://example.com/new-hook" [--events item.created]
mo webhooks delete WEBHOOK_ID --yes
mo webhooks test WEBHOOK_ID
mo webhooks rotate-secret WEBHOOK_ID
mo webhooks deliveries WEBHOOK_ID
mo webhooks retry WEBHOOK_ID DELIVERY_ID
mo webhooks enable WEBHOOK_ID

MCP tools

  • list_webhooks — list all webhook endpoints
  • create_webhook — register a new endpoint
  • get_webhook — get endpoint details
  • update_webhook — update URL, events, or status
  • delete_webhook — delete an endpoint
  • test_webhook — send a test event to an endpoint
  • rotate_webhook_secret — rotate the signing secret (1-hour overlap)
  • list_webhook_deliveries — list delivery attempts for an endpoint
  • retry_webhook_delivery — retry a failed or dead delivery

Limits

LimitValue
Max endpoints per user5
Max retry attempts7 (~35 hours)
Delivery timeout10 seconds
Response body captured4096 characters
Auto-disable after7 days of only failures