Outgoing Webhooks
Outgoing webhooks send event notifications to your URL when things happen in Monotree. Configure each endpoint and the events it should subscribe to in the Open API → Outgoing Webhooks tab, or programmatically via the Manage Outgoing Webhooks API.
Available events
| Event | Fires when |
|---|---|
monotree.post.created | A post is created. |
monotree.comment.created | A comment is added to a post. |
monotree.comment.updated | A comment on a post is edited. |
monotree.comment.deleted | A comment on a post is deleted. |
monotree.announcement.published | An announcement is published. |
monotree.formresponse.created | A form response is submitted. |
monotree.formresponse.updated | A form response's status, deadline, hidden/archived flag, or assignees change. Does not fire on initial creation (use formresponse.created) or for comments (use formresponse.comment.created). Filterable by form. Whistleblower forms never dispatch. |
monotree.formresponse.comment.created | A public comment is added to a form response. Filterable by form. Whistleblower forms never dispatch. Internal staff notes do not trigger webhooks. |
monotree.formresponse.comment.deleted | A public comment on a form response is deleted. Filterable by form. Whistleblower forms never dispatch. |
monotree.formresponse.alarm_triggered | A form response matched a configured field alarm rule. One delivery per matched rule, idempotent per response + rule pair. Filterable by form. Whistleblower forms never dispatch. |
monotree.user.registered | A user accepts an invite and registers. |
monotree.onboarding.completed | A user completes onboarding. |
Payload
Every delivery shares the same envelope — a unique id, the event name, the API version, a Unix timestamp, and an event-specific payload:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event": "monotree.post.created",
"version": "v1",
"timestamp": 1711270800,
"payload": {
"id": 123,
"body": "Post content here",
"wall_id": 1,
"wall_name": "General",
"author": { "id": 1, "name": "John Doe", "email": "john@example.com", "avatar_url": "https://images.monotree.com/avatars/john-doe.jpg" },
"created_at": "2026-03-22T10:00:00+00:00",
"updated_at": "2026-03-22T10:00:00+00:00"
}
}Form response payload
monotree.formresponse.created and monotree.formresponse.updated carry the full form response, including every answered field:
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"event": "monotree.formresponse.created",
"version": "v1",
"timestamp": 1716190200,
"payload": {
"id": 4521,
"form_id": 12,
"form_title": "Daily Fridge Check",
"is_anonymous": false,
"status": "todo",
"respondent": {
"id": 88,
"name": "Jane Doe",
"email": "jane@example.com",
"type": "employee",
"avatar_url": "https://images.monotree.com/avatars/jane-doe.jpg",
"groups": [
{ "id": 3, "name": "Kitchen", "type": "department", "created_at": "2026-01-10T08:00:00+00:00" }
],
"hired_at": "2025-09-01",
"is_away": false,
"last_active_on": "2026-05-20",
"registered_at": "2025-09-01T09:12:00+00:00",
"created_at": "2025-09-01T09:00:00+00:00"
},
"department": { "id": 3, "name": "Kitchen", "type": "department", "created_at": "2026-01-10T08:00:00+00:00" },
"field_responses": [
{
"value": "9",
"field": { "id": 41, "title": "Fridge temperature", "type": "number" }
},
{
"value": [
{ "id": 7, "title": "Needs cleaning" },
{ "id": 9, "title": "Restock required" }
],
"field": { "id": 42, "title": "Issues found", "type": "choices" }
},
{
"value": [
{
"id": 88,
"name": "Jane Doe",
"email": "jane@example.com",
"type": "employee",
"avatar_url": "https://images.monotree.com/avatars/jane-doe.jpg",
"groups": [],
"hired_at": "2025-09-01",
"is_away": false,
"last_active_on": "2026-05-20",
"registered_at": "2025-09-01T09:12:00+00:00",
"created_at": "2025-09-01T09:00:00+00:00"
}
],
"field": { "id": 43, "title": "Responsible person", "type": "users" }
}
],
"media": [
{ "id": 55, "type": "image", "mime_type": "image/jpeg", "filename": "fridge.jpg", "size": 182034, "url": "https://…/fridge.jpg", "width": 1280, "height": 960, "created_at": "2026-05-20T07:30:00+00:00" }
],
"created_at": "2026-05-20T07:30:00+00:00",
"updated_at": "2026-05-20T07:30:00+00:00"
}
}Notes on the top-level fields:
respondentisnullwhen the response is anonymous.departmentisnullwhen the respondent has no department.statusisnullunless the form uses response statuses — otherwise one oftodo,in_progress,done.medialists the files uploaded with the submission — attached to the response as a whole, not to a specific field (see Forms). Signedurls for anonymous-response files expire ~5 minutes after dispatch; re-fetchGET /formresponses/{id}for a fresh link.
The field_responses array holds one entry per answered field. The shape of value depends on field.type:
| Field type | value |
|---|---|
text, date, datetime, time, number, rating, boolean | The submitted value. |
choices | Array of selected choices, each { id, title }. |
users, department_users | Array of selected users — same shape as respondent. |
Form response comment payload
monotree.formresponse.comment.created and monotree.formresponse.comment.deleted carry the public comment:
{
"id": "1f3a2b6c-2d4e-4a1b-9c8d-7e6f5a4b3c2d",
"event": "monotree.formresponse.comment.created",
"version": "v1",
"timestamp": 1716190800,
"payload": {
"id": 901,
"body": "Could you re-check the freezer reading?",
"form_response_id": 4521,
"form_id": 12,
"author": {
"id": 5,
"name": "Sam Owner",
"email": "sam@example.com",
"type": "employee",
"avatar_url": "https://images.monotree.com/avatars/sam-owner.jpg",
"groups": [],
"hired_at": "2024-02-01",
"is_away": false,
"last_active_on": "2026-05-20",
"registered_at": "2024-02-01T09:00:00+00:00",
"created_at": "2024-02-01T09:00:00+00:00"
},
"media": [
{ "id": 77, "type": "image", "mime_type": "image/jpeg", "filename": "freezer.jpg", "url": "https://cdn.monotree.dk/…/freezer.jpg", "width": 1200, "height": 800, "created_at": "2026-05-20T08:00:00+00:00" }
],
"created_at": "2026-05-20T08:00:00+00:00",
"updated_at": "2026-05-20T08:00:00+00:00"
}
}Notes:
- Only public comments fire these events. Internal staff notes never dispatch.
medialists the comment's attachments (images, videos, documents); it is[]when the comment has none.authorisnullwhen the comment was written by the respondent on an anonymous form — the respondent's identity is suppressed. Comments from managers/owners are always identified.
Signature verification
Every delivery includes two headers:
| Header | Value |
|---|---|
Timestamp | Unix timestamp at the time of delivery. |
Signature | HMAC-SHA256(timestamp + "." + raw_json_body, your_webhook_secret) as a hex string. |
Verify both:
- Recompute the HMAC using your secret and compare it to the
Signatureheader in constant time. - Reject requests whose
Timestampis more than 5 minutes old to prevent replay attacks.
const crypto = require('crypto');
function verify(req, secret) {
const ts = req.headers['timestamp'];
const sig = req.headers['signature'];
const body = req.rawBody; // the raw, unparsed JSON string
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${body}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
return true;
}Authentication
Each webhook can also be configured with one of: Bearer token, Basic Auth, custom API key header, or arbitrary custom headers — useful when your receiver enforces its own auth on top of the signature.
Retries
Failed deliveries (any non-2xx response, or a timeout > 10 seconds) are retried with exponential backoff. Delivery logs are visible in the Outgoing Webhooks tab.