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.

Every delivery is signed. Before you trust a payload, check it against Signature verification.

Available events

EventFires when
monotree.post.createdA post is created.
monotree.comment.createdA comment is added to a post.
monotree.comment.updatedA comment on a post is edited.
monotree.comment.deletedA comment on a post is deleted.
monotree.announcement.publishedAn announcement is published.
monotree.formresponse.createdA form response is submitted.
monotree.formresponse.updatedA form response's status, deadline, or hidden flag changes, assignees are added or removed, or the response is deleted, restored, or permanently deleted — check the payload's deleted_at (always present, null while the response exists) to detect deletions. 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.createdA 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.deletedA public comment on a form response is deleted. Filterable by form. Whistleblower forms never dispatch.
monotree.formresponse.alarm_triggeredA 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.registeredA user accepts an invite and registers. Not filterable.
monotree.onboarding.completedA user completes onboarding. Once per employee, only while onboarding is switched on. Not filterable.
monotree.achievement.awardedA user is given an achievement, automatically (completion or tenure trigger), by hand in the CMS or through POST /achievements/{id}/awards. Fires only for published achievements while the achievements feature is on; never for CMS re-evaluation backfills. Filterable by achievement.
monotree.announcement.confirmedAn employee confirms they have read a confirmable announcement. Once per employee per announcement — there is no un-confirm, so a confirmation is announced exactly once. Not filterable.

Payload

Every delivery shares the same envelope — a unique id, the event name, the API version, the sending customer, a Unix timestamp, and an event-specific payload:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "event": "monotree.post.created",
  "version": "v1",
  "customer": "acme",
  "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"
  }
}

customer is the sending customer's Monotree identifier (for example acme), the same value on every event including the CMS test event, so one receiver can serve several Monotree customers and pick the right secret before verifying.

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",
  "customer": "acme",
  "timestamp": 1716190200,
  "payload": {
    "id": 4521,
    "form_id": 12,
    "form_title": "Daily Fridge Check",
    "form_due_at": "2026-06-01",
    "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"
    },
    "assignees": [
      {
        "id": 5,
        "name": "Sam Doe",
        "email": "sam@example.com",
        "type": "employee",
        "avatar_url": "https://images.monotree.com/avatars/sam-owner.jpg",
        "groups": [
          { "id": 3, "name": "Kitchen", "type": "department", "created_at": "2026-01-10T08:00:00+00:00" }
        ],
        "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"
      }
    ],
    "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",
    "deleted_at": null
  }
}

Notes on the top-level fields:

  • respondent is null when the response is anonymous.
  • assignees lists the staff members currently assigned to handle the response — [] when unassigned.
  • form_due_at is the parent form's due date (YYYY-MM-DD) — null when the form has none. Changing a form's due date does not fire formresponse.updated; read it from the form endpoints when it matters.
  • department is null when the respondent has no department.
  • status is null unless the form uses response statuses — otherwise one of todo, in_progress, done.
  • deleted_at is always present and null while the response exists. A formresponse.updated delivery with deleted_at set means the response was deleted — on a permanent delete the related rows are pruned first, so that delivery's field_responses and assignees arrays may be empty.
  • media lists the files uploaded with the submission — attached to the response as a whole, not to a specific field (see Forms). Signed urls for anonymous-response files expire ~5 minutes after dispatch; re-fetch GET /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 typevalue
text, date, datetime, time, number, rating, booleanThe submitted value.
choicesArray of selected choices, each { id, title }.
users, department_usersArray 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",
  "customer": "acme",
  "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 Doe",
      "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.
  • media lists the comment's attachments (images, videos, documents); it is [] when the comment has none.
  • author is null when the comment was written by the respondent on an anonymous form — the respondent's identity is suppressed. Comments from managers/owners are always identified.

Achievement award payload

monotree.achievement.awarded carries the award — the same object as GET /achievements/{id}/awards with both achievement and user embedded:

{
  "id": "9b2f0c1e-6a3d-4e8f-b1c2-3d4e5f6a7b8c",
  "event": "monotree.achievement.awarded",
  "version": "v1",
  "customer": "acme",
  "timestamp": 1754212361,
  "payload": with both achievement and user embedded:
  {
    "id": 4711,
    "awarded_at": "2026-08-01T00:00:00+02:00",
    "is_manually_added": true,
    "department": { "id": 41, "name": "Copenhagen", "type": "department" },
    "assigned_by": { "id": 5, "name": "Sam Doe", "email": "sam@example.com", ... },
    "achievement": { "id": 12, "title": "Employee of the month", "emoji": "👑", "type": "employee_of_the_month", "visibility": "public", "awarded_users_count": 37, ... },
    "user": { "id": 501, "name": "Jane Doe", "email": "jane@example.com", "groups": [ ... ], ... },
    "created_at": "2026-08-03T09:12:41+00:00",
    "updated_at": "2026-08-03T09:12:41+00:00"
  }
}

Notes:

  • Fires for automatic awards (completion and tenure triggers) and for awards handed out in the CMS or through the API (assigned_by is then the company's own user, type: customer), only while the achievement is published and the achievements feature is enabled. CMS re-evaluation backfills never dispatch.
  • Filterable by achievement — pick one or more achievements when creating the webhook to receive only their awards.
  • awarded_at is the awarded day as midnight in the customer server's timezone, with its offset — read the date in that offset. created_at and updated_at are the instants the award was recorded and last changed.
  • Personal achievements dispatch too; check achievement.visibility before showing the award to colleagues.

Announcement confirmation payload

monotree.announcement.confirmed carries the confirmation, with the announcement and the employee embedded. The announcement is the same object GET /announcements/{id} returns, keyed by the confirmation's type:

{
  "id": "1c7a4f2e-88b0-4f3a-9c11-2de5b6a7c901",
  "event": "monotree.announcement.confirmed",
  "version": "v1",
  "customer": "acme",
  "timestamp": 1758441161,
  "payload": {
    "id": 8842,
    "type": "announcement",
    "created_at": "2026-09-21T09:12:41+02:00",
    "user": { "id": 501, "name": "Jane Doe", "email": "jane@example.com", "groups": [ ... ], ... },
    "announcement": { "id": 456, "title": "Fire drill briefing", "is_confirmable": true, "confirmations_count": 12, ... }
  }
}

Notes:

  • Only announcements marked confirmable in the CMS can be confirmed, and only they fire this event. Company events can be made confirmable too, but those confirmations neither fire an event nor appear in the API yet.
  • type is always announcement here, and names the key the confirmed item sits under.
  • Confirming is once per employee per announcement, and there is no way to un-confirm, so payload.id (the confirmation) is a stable idempotency key that is never reused.
  • created_at is the moment the employee confirmed, in the customer server's timezone with its offset.
  • Confirmations made before the endpoint existed are not replayed, and a delivery that fails three times is not redelivered — catch up with GET /confirmations?since=.

User payloads

monotree.user.registered and monotree.onboarding.completed carry the user, the same object GET /users/{id} returns. monotree.onboarding.completed adds onboarding_completed_at:

{
  "id": "7b3e9d14-2f6a-4c8e-9a51-0c4d8e2f7a93",
  "event": "monotree.onboarding.completed",
  "version": "v1",
  "customer": "acme",
  "timestamp": 1758542591,
  "payload": {
    "id": 501,
    "name": "Jane Doe",
    "first_name": "Jane",
    "last_name": "Doe",
    "title": null,
    "email": "jane@example.com",
    "type": "default",
    "avatar_url": "https://images.monotree.com/avatars/jane-doe.jpg",
    "is_manager": false,
    "groups": [
      { "id": 41, "name": "Copenhagen", "type": "department", "is_manager": false, "created_at": "2026-01-15T10:00:00+00:00" }
    ],
    "hired_at": "2026-09-01",
    "date_of_birth": null,
    "is_away": false,
    "last_active_on": "2026-09-22T12:03:11.000000Z",
    "registered_at": "2026-09-01T08:30:00+02:00",
    "created_at": "2026-08-28T10:00:00+02:00",
    "onboarding_completed_at": "2026-09-22T14:03:11+02:00"
  }
}

Notes:

  • monotree.user.registered has the same payload without onboarding_completed_at. registered_at is the moment the user registered.
  • onboarding_completed_at is the moment the employee completed onboarding, in the customer server's timezone with its offset. The envelope timestamp is when the delivery was sent.
  • Onboarding completes once per employee, so monotree.onboarding.completed fires at most once per user. It does not fire while the onboarding feature is switched off.

Signature verification

Every delivery — retries included — is signed afresh: the Timestamp header is the time of that attempt and the signature covers it, while the timestamp inside the body keeps the original dispatch time. Verify against the header. Every delivery includes two headers:

HeaderValue
TimestampUnix timestamp at the time of delivery.
SignatureHMAC-SHA256(timestamp + "." + raw_json_body, your_webhook_secret) as a hex string.

Verify both:

  1. Recompute the HMAC using your secret and compare it to the Signature header in constant time.
  2. Reject requests whose Timestamp is 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

A failed attempt (any non-2xx response, or no response within 3 seconds) is retried 1 minute and again 10 minutes after the first attempt; every attempt is signed with a fresh Timestamp. After the third failure the delivery is marked failed and is not redelivered automatically. Delivery logs, attempt by attempt, are visible in the Outgoing Webhooks tab.