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
- Go to Settings > Developer > Webhooks and click Add webhook.
- Enter your HTTPS endpoint URL.
- Select the events you want to receive (or leave empty for all events).
- Copy the signing secret — it is shown only once.
- Click Send test event to verify your endpoint.
You can also manage webhooks via the CLI (mo webhooks) or the API.
Event types
| Event | Trigger |
|---|---|
item.created | An item is added to a container |
item.updated | An item's fields are modified (includes changed_fields) |
item.deleted | An item is removed |
item.expired | A storage item's expiration date is today |
item.expiring_soon | A storage item expires within 7 days (also fires every 10 days for overdue items) |
container.created | A new container is created |
container.updated | A container is renamed or its settings change |
container.deleted | A container is deleted |
member.added | A user is added to a container |
member.removed | A user is removed from a container |
webhook.test | Sent 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:
{
"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
| Field | Type | Description |
|---|---|---|
schema_version | string | Always "1" |
event | string | Event type (e.g. item.created) |
event_id | UUID | Unique ID for this event |
created_at | ISO 8601 | When the event occurred |
actor_id | UUID | User who triggered the action (absent for system events) |
container_id | UUID | Affected container |
container_type | string | storage, board, calendar, etc. |
data | object | Event-specific payload (see below) |
batch_id | UUID | Present when multiple events share a batch operation |
Data by event type
Item events (item.created, item.updated, item.deleted):
{
"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):
{
"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):
{
"container_id": "container-uuid",
"container_type": "storage",
"name": "Fridge"
}Member events (member.added, member.removed):
{
"user_id": "user-uuid",
"email": "user@example.com",
"name": "Alice",
"role": "member"
}Test event (webhook.test):
{
"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:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | Mottainai-Webhook/1.0 |
X-Mottainai-Event | Event type (e.g. item.created) |
X-Mottainai-Delivery | Unique delivery ID (UUID) |
X-Mottainai-Signature-256 | HMAC-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
- Mottainai computes
HMAC-SHA256(secret, raw_request_body). - The result is sent as
X-Mottainai-Signature-256: sha256=<hex-digest>. - Your server recomputes the HMAC and compares using constant-time comparison.
Example (Node.js)
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)
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)
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:
- Call
POST /api/webhooks/{id}/rotate-secret(or click Rotate secret in Settings). - A new secret is generated and returned once.
- The old secret remains valid for 1 hour (the response includes
expires_old_at). - During the overlap window, verify against both the old and new secrets.
- After 1 hour, the old secret is no longer valid.
Retry behavior
Failed deliveries are retried with exponential backoff:
| Attempt | Delay after failure |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
| After 7 | Marked 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
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_IDMCP tools
list_webhooks— list all webhook endpointscreate_webhook— register a new endpointget_webhook— get endpoint detailsupdate_webhook— update URL, events, or statusdelete_webhook— delete an endpointtest_webhook— send a test event to an endpointrotate_webhook_secret— rotate the signing secret (1-hour overlap)list_webhook_deliveries— list delivery attempts for an endpointretry_webhook_delivery— retry a failed or dead delivery
Limits
| Limit | Value |
|---|---|
| Max endpoints per user | 5 |
| Max retry attempts | 7 (~35 hours) |
| Delivery timeout | 10 seconds |
| Response body captured | 4096 characters |
| Auto-disable after | 7 days of only failures |