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
| 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, 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.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. Not filterable. |
monotree.onboarding.completed | A user completes onboarding. Once per employee, only while onboarding is switched on. Not filterable. |
monotree.achievement.awarded | A 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.confirmed | An 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. |
monotree.bot_reply.created | An employee answers a bot: any message they send in their 1:1 chat with the bot, or a reply to a bot message in any chat. Not filterable — check payload.bot.id. |
monotree.completion.created | An employee completes a course, a journey or a handbook. One delivery per completion. Onboarding is not included (use monotree.onboarding.completed). Filterable by completion type. |
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",
"deadline_at": null,
"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:
respondentisnullwhen the response is anonymous.assigneeslists the staff members currently assigned to handle the response —[]when unassigned.form_due_atis the parent form's due date (YYYY-MM-DD) —nullwhen the form has none. Changing a form's due date does not fireformresponse.updated; read it from the form endpoints when it matters.deadline_atis the response's own deadline (YYYY-MM-DD), the date staff should have handled it by —nullwhen none is set. Setting, changing or removing it firesformresponse.updatedwith the new value.departmentisnullwhen the respondent has no department.statusisnullunless the form uses response statuses — otherwise one oftodo,in_progress,done.deleted_atis always present andnullwhile the response exists. Aformresponse.updateddelivery withdeleted_atset means the response was deleted — on a permanent delete the related rows are pruned first, so that delivery'sfield_responsesandassigneesarrays may be empty.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",
"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.
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.
Form response alarm payload
monotree.formresponse.alarm_triggered carries two objects: the form response under payload.response, and the alarm that fired under payload.triggered_alarm. Neither has the shape of the other form response events. payload.response is not the object monotree.formresponse.created sends: it is the older webhook format, where every embedded object and list is wrapped in { "data": ... }, the respondent is user rather than respondent, and the whole form definition is embedded under form:
{
"id": "3d5b7f90-1c2e-4a6b-8d0f-2e4c6a8b0d1f",
"event": "monotree.formresponse.alarm_triggered",
"version": "v1",
"customer": "acme",
"timestamp": 1716190201,
"payload": {
"response": {
"id": 4521,
"is_anonymous": false,
"is_respondent_form_owner": false,
"is_archived": false,
"is_hidden": false,
"respondent_comment_access": true,
"created_at": "2026-05-20T07:30:00+00:00",
"deadline_at": null,
"status": { "id": "todo", "title": "To-do" },
"form": {
"data": {
"id": 12,
"title": "Daily Fridge Check",
"description": "Check every fridge before opening.",
"fields": { "data": [ { "id": 41, "title": "Fridge temperature", "type": "number", ... }, ... ] },
...
}
},
"field_responses": {
"data": [
{
"value": 9,
"field": {
"data": {
"id": 41,
"title": "Fridge temperature",
"type": "number",
"minimum": null,
"maximum": null,
"unit": "°C",
"comment": null,
"is_required": true,
"placeholder": null,
"created_at": "2026-01-10T08:00:00+00:00",
"updated_at": "2026-01-10T08:00:00+00:00",
"deleted_at": null,
"meta": { "data": { "type": "FormField" } }
}
},
"meta": { "data": { "type": "FormFieldResponse" } }
}
]
},
"user": {
"data": {
"id": 88,
"type": "default",
"first_name": "Jane",
"last_name": "Doe",
"full_name": "Jane Doe",
"email": "jane@example.com",
"avatar_url": "https://images.monotree.com/avatars/jane-doe.jpg",
"deleted_at": null,
"is_away": false,
"meta": { "data": { "type": "User" } }
}
},
"selected_department": null,
"meta": { "data": { "type": "FormResponse" } }
},
"triggered_alarm": {
"id": 310,
"form_field_alarm_id": 17,
"form_field_response_id": 9921,
"form_field_id": 41,
"field_title": "Fridge temperature",
"field_type": "number",
"operator": "greater_than",
"value": "8",
"triggered_at": "2026-05-20T07:30:01+00:00",
"alarm": {
"data": {
"id": 17,
"form_field_id": 41,
"name": "Fridge too warm",
"operator": "greater_than",
"value": "8",
"notify_user_ids": [5],
"notify_group_ids": [3],
"notify_form_owners": true,
"is_active": true,
"position": 0,
"created_at": "2026-01-10T08:05:00+00:00",
"updated_at": "2026-01-10T08:05:00+00:00",
"meta": { "data": { "type": "FormFieldAlarm" } }
}
},
"meta": { "data": { "type": "FormResponseAlarm" } }
}
}
}Notes on payload.response:
- The response's own fields sit at the top:
id, the flags,created_atanddeadline_at(YYYY-MM-DDornull). There is noform_id; read it fromform.data.id. statusis an object,{ id, title }, with a translatedtitle. On a form without response statuses,idis the string"null".useris the respondent, withfull_nameinstead ofname, andnullwhen the response is anonymous.selected_departmentis{ "data": { id, name, type } }when the form asks the respondent to pick a department, otherwisenull.form.dataholds the form'sid,type,title,description,instructions,respondent_identityanddeleted_at, and embeds itsfields(with theirchoices), its creator asuser, itsowner_users,owner_groupsandgroups. Each list is wrapped in{ "data": [ ... ] }.field_responses.dataholds one entry per answered field.valueis the submitted value, typed by the field: a number fornumberandratingfields,trueorfalseforbooleanfields, an ISO 8601 string for dates and datetimes, and a string for text and time fields. Forchoicesit is a list of{ id, title, created_at, updated_at }, and forusersanddepartment_usersa list of users shaped likeuser.data. The field is underfield.data.- Every embedded object carries a
metaobject naming its type. It holds nothing else you need. - There is no
assignees,mediaordeleted_at. FetchGET /formresponses/{id}withpayload.response.idwhen you need them, or when you would rather work with the shape the other events use.
Notes on payload.triggered_alarm:
idis the triggered alarm itself: one per response and rule, never reused.form_field_alarm_idis the rule andform_field_response_idthe answer that matched it.operatoris one ofequals,not_equals,contains,not_contains,greater_than,less_than,before,after(text, boolean, number, rating, date and time fields),includes_choice,excludes_choice(choice fields),includes_userandexcludes_user(user fields).valueis meant to be shown to people, and what it holds depends on the operator:includes_choiceandincludes_user: the titles of the choices, or the full names of the users, in the response that matched the rule, joined with,(for example"Needs cleaning, Restock required"). Not IDs.excludes_choiceandexcludes_user: the titles or names of all the rule's choices or users, joined with,. None of them is in the response; that is why the alarm fired.- Every other operator: the rule's threshold as stored, as a string (
"8"for a number rule, a date for a date rule). The answer that crossed it is inresponse.field_responses.
alarm.datais the rule as configured: itsname,operatorand rawvalue(for choice and user rules that is the stored list of IDs, as a string), who it notifies, and whether it is active.
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": {
"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_byis 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_atis the awarded day as midnight in the customer server's timezone, with its offset — read the date in that offset.created_atandupdated_atare the instants the award was recorded and last changed.- Personal achievements dispatch too; check
achievement.visibilitybefore 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.
typeis alwaysannouncementhere, 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_atis 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=.
Bot reply payload
monotree.bot_reply.created carries the employee's message, the employee, the bot, and the bot message being answered. Use it to act on answers to messages your integration sent, for example an employee accepting a shift offered by a direct message:
{
"id": "4e1d2c3b-5a69-4f78-8e9d-0a1b2c3d4e5f",
"event": "monotree.bot_reply.created",
"version": "v1",
"customer": "acme",
"timestamp": 1759316561,
"payload": {
"id": 5530,
"room_id": 240,
"text": "Yes, I can take it",
"user": { "id": 501, "name": "Jane Doe", "email": "jane@example.com", "groups": [ ... ], ... },
"bot": { "id": 873, "name": "Shift Bot", "avatar_url": "https://images.monotree.com/avatars/shift-bot.png", "created_at": "2026-06-01T09:00:00+02:00" },
"reply_to": {
"id": 5512,
"room_id": 240,
"text": "Can you cover Tuesday 14:00–22:00?",
"bot_user_id": 873,
"pinned_at": null,
"pinned_until": null,
"created_at": "2026-10-01T09:00:00+02:00"
},
"created_at": "2026-10-01T09:12:41+02:00"
}
}Notes:
- Two kinds of message fire the event: any message an employee sends in their 1:1 chat with a bot (the chat direct messages arrive in), and a reply to a bot message in any chat, such as a group chat an incoming webhook posts to as its bot. A plain message in a group chat, without replying to the bot, does not fire it.
reply_tois the bot message being answered: the one the employee replied to, or, when they just wrote in the 1:1 chat, the bot's most recent message there before theirs. It isnullwhen the bot has not written in the chat yet. When the bot has sent several messages and the employee did not reply to a specific one,reply_tois the latest, which may not be the one they meant.useris the same objectGET /users/{id}returns,botthe same asGET /bots/{id}, andreply_tothe message shape returned byPOST /users/{id}/messages.- Only employees fire the event. Messages from bots, including the bot's own, and chat events such as members joining never do.
textisnullwhen the employee sent only a photo or file. Attachments are not included in the payload.- Edited and deleted messages do not fire the event again.
- Not filterable: an endpoint subscribed to the event receives answers to every bot. Use
payload.bot.idto pick out your own.
Completion payload
monotree.completion.created carries the completion, with the employee and the completed item embedded. The item is the same object its own endpoint returns (GET /courses/{id}, GET /journeys, GET /handbooks/{id}), keyed by the completion's type:
{
"id": "2a6f8c1d-3b4e-4d5f-9a70-1b2c3d4e5f60",
"event": "monotree.completion.created",
"version": "v1",
"customer": "acme",
"timestamp": 1759655561,
"payload": {
"id": 90412,
"type": "course",
"completed_at": "2026-10-05T11:12:41+02:00",
"user": { "id": 501, "name": "Jane Doe", "email": "jane@example.com", "groups": [ ... ], ... },
"course": { "id": 34, "title": "Food safety basics", "is_published": true, "chapters_count": 6, "groups": [ ... ], ... }
}
}Notes:
typeiscourse,journeyorhandbook, and names the key the completed item sits under: a journey completion has"type": "journey"and ajourneyobject, a handbook completion"type": "handbook"and ahandbookobject.- The event fires the moment the employee finishes: they read the last chapter or pass the last quiz, complete the last step of a journey, or a manager marks or approves the course as completed. A completion that Monotree records later without notifying anyone, for example when it catches up on content finished earlier, is not sent.
- Completions that Monotree imports or fills in on a customer's behalf, such as training records migrated from another system, are never sent. Read those from the academy statistics.
- A journey can complete for many employees at the same moment without any of them doing anything: when it is published, when an item is added to or removed from it, or when group membership changes. Each of those completions is sent, so expect a burst of deliveries after such a change.
- Filterable by type: pick one or more of courses, journeys and handbooks when creating the webhook, or pass the type tokens under
filtersin the management API. Without a filter the endpoint receives all three. - One completion is one delivery per endpoint. Finishing the last course of a journey completes both, so it sends two deliveries: one with
type: courseand one withtype: journey. - Only journeys an employee works through themselves are sent. Onboarding has its own event,
monotree.onboarding.completed, and is never sent as a journey here. A journey in a chain that Monotree skips for an employee who has no access to it is not sent either. payload.idis the completion and is never reused, so it is a stable idempotency key. When a completion is cleared or reset (an expired certificate, a manager clearing it) and the employee completes the item again, that is a new completion with a newidand a new delivery.completed_atis the moment of completion, in the customer server's timezone with its offset.- If an achievement is triggered by the same course, journey or handbook,
monotree.achievement.awardedstill fires as before. An endpoint subscribed to both events receives both, so subscribe to the one you act on. - Completions from before the endpoint existed are not replayed. Read current state from
GET /users/{id}/journeysand the academy statistics.
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.registeredhas the same payload withoutonboarding_completed_at.registered_atis the moment the user registered.onboarding_completed_atis the moment the employee completed onboarding, in the customer server's timezone with its offset. The envelopetimestampis when the delivery was sent.- Onboarding completes once per employee, so
monotree.onboarding.completedfires 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:
| 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
A failed attempt (a non-2xx response other than 404 or 410, 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.
A 404 Not Found or 410 Gone response is treated as a receiver that is gone: the delivery ends with that first attempt and is not retried. If your receiver is only temporarily unavailable (a deploy, maintenance, a route change), answer with a 5xx such as 503 Service Unavailable so the retries apply.