Webhook Integration
Flowmingo emits notifications whenever candidates move through invitation, interview, assessment, application or review workflows, and when a person approves or declines a change that an API key or an AI assistant asked for. This section explains how to create webhook endpoints, secure them with HMAC signatures, and understand the payloads you will receive so downstream systems (ATS, HRIS, Slack bots, etc.) stay in sync. If your tool cannot receive webhooks, read the same events by polling the events feed instead. To connect Flowmingo to Zapier, Make or n8n without code, follow the recipes in the API guide.
1. Create a webhook endpoint
Log in to your Flowmingo workspace, click your email at the bottom-left of the sidebar, then Settings, and open Integrations -> Webhooks. Click Create Endpoint and provide:
- Name: A label to identify this endpoint later.
- Endpoint URL: A public address that Flowmingo will POST events to (for example:
https://your-app.com/api/flowmingo-webhook). Usehttps: the payloads of some events contain candidate email addresses. See section 4.5 for which addresses are refused and what your address must do. - Description (optional).
- Event types: Subscribe to All Events (recommended) or Subscribe to Specific Events and pick the ones this endpoint should receive (see section 2).
On save, Flowmingo shows the Webhook Endpoint Created! window with the signing secret, which looks like whsec_ followed by 64 hexadecimal characters. It is shown once: store it securely. You need this value to verify X-Webhook-Signature. The first connectivity check is sent before you see this secret; read the note about it in section 3 if your receiver already checks signatures.
Managing endpoints:
- Edit / Delete: Return to Settings -> Integrations -> Webhooks, open an endpoint with Edit, change it and click Save Changes, or delete it.
- On/off switch: The switch beside an endpoint's name turns delivery on and off. An endpoint that Flowmingo switched off after repeated failures (section 4.3) is turned back on here.
- Regenerate Secret: Replaces the signing secret immediately. There is no period in which both secrets work, and a delivery that was already being prepared can still carry a signature made with the old one. See section 5.4 for what to do around it.
- Send Test Event: Sends a sample event to the endpoint, so you can check your receiver without waiting for a real one. Test deliveries carry
"is_test": true. A test payload is sample data, and it differs from a real payload in several ways (section 3). - Recent Deliveries (and View all): The outcome of recent deliveries to that endpoint. A delivery that failed all its attempts has Send again, and the endpoint's page offers Send again for all failed events; both work only while the endpoint is switched on (section 4.4).
2. Supported events
| Event | Sent when | Sub-event (field it comes from) |
|---|---|---|
invitation.status.update |
An invitation email is delivered, opened, clicked, fails or is marked as spam. | status |
invitation.lifecycle.update |
An invitation is created, sent or reminded. | status |
interview.status.update |
A candidate opens or starts an AI interview, or the interview is marked completed (which is not the moment the candidate finishes: see the interview status payload). | status |
interview.evaluation.update |
A new CV, interview or holistic evaluation result is stored. | evaluation_type |
assessment.completed |
A candidate completes a skill test, technical test, showcase or questionnaire, or the result of a recording evaluation or a CV evaluation is stored. Never for a practice, trial or demo run. | assessment_type |
application.created |
A candidate arrives on a hiring project. | none |
candidate.stage_changed |
An application moves to another stage. | none |
application.rejected |
An application is rejected through the API, or removed from a hiring project. | none |
submission.review.update |
A note, a rating or a shortlist mark is added to a result, or a rating or a shortlist mark is taken back with the undo route. | action |
approval.status.update |
A person decides a change that an API key or an AI assistant asked for and that waited for approval: approved, declined or, for the deletion of a hiring project only, failed. | status |
Subscribing to a parent event (for example invitation.status.update) receives all sub-events of that type. You can also subscribe to specific sub-events only (for example invitation_email_failed).
Which endpoints receive which events. An endpoint set to Subscribe to All Events receives every event type above, and every type added later. An endpoint with a list of events receives only those. The Select specific events list offers invitation.status.update, interview.status.update, interview.evaluation.update, assessment.completed and approval.status.update; the other five events are delivered to endpoints subscribed to All Events. The list offers assessment.completed as one choice that covers all six assessment types; read assessment_type in the payload to tell them apart. It offers approval.status.update the same way, as one choice that covers its three sub-events; read status.
Ignore event types you do not know, and answer
2xx. A receiver written for a few events can start receiving new ones. If it answers an error to an event type it does not recognise, every attempt to deliver such an event counts as a failure. An event is attempted up to 6 times over about a day, and 3 days of failed deliveries in a row switch the endpoint off (section 4.3), so a receiver that answers an error to every event it does not know is enough. Readevent_type, handle the ones you need, and acknowledge the rest.
A recorded practice attempt or try-out never sends a score. A candidate can try a practice attempt before the real one, and someone on your team can try out an interview, or preview a skill test or a technical test with Preview this yourself. For a practice attempt Flowmingo raises no initiated, started or completed status event, no interview.evaluation.update and no assessment.completed. For a try-out or a preview it raises no started or completed status event, no interview.evaluation.update and no assessment.completed. An initiated event is sent when an invitation's link is opened, so a try-out opened with an email that also holds an invitation to that interview can still raise one. Your endpoints never receive the score of a sitting that Flowmingo has recorded as practice or as a try-out, and the Recent Deliveries of an endpoint in Settings -> Integrations -> Webhooks do not list one. Flowmingo can only hold back what it has recorded as practice: a practice sitting that was not recorded as practice is sent like any other attempt.
3. Payload structure
Flowmingo wraps every event in a consistent envelope:
{
"schema_version": "1.0.0",
"event_type": "invitation.status.update",
"event_id": "3a74fa26-4b66-4534-ba76-073548c95239",
"timestamp": "2026-03-04T01:00:00.000Z",
"organization_id": 123,
"data": {
"...event specific fields..."
},
"is_test": true
}
event_ididentifies the event. It is the same on every delivery attempt of that event, and it is the id the events feed uses for it too.timestampis when the event was recorded, not when this delivery was sent. Use it to order events.is_testis only included for test deliveries.
The connectivity check every new endpoint gets. Right after you save a new endpoint, Flowmingo sends it one request whose event_type is endpoint.verification. It checks that the address is live. Normally the endpoint is on from the start and the request is sent in the background. When Flowmingo requires verification of new addresses, saving waits for your answer (up to about 10 seconds) and the endpoint receives none of its subscribed events until the check gets a 2xx (section 4.4); a test sent with Send Test Event can still reach it. The check is not an event, and Flowmingo can send another one later, so answer every one you receive:
{
"schema_version": "1.0.0",
"event_type": "endpoint.verification",
"endpoint_id": "9b2f4c1e-7a35-4d80-b6e1-3c5a8f0d2e47",
"verification_token": "whv_0123456789abcdef0123456789abcdef0123456789abcdef",
"timestamp": "2026-03-04T01:00:00.000Z"
}
It carries an endpoint_id and a verification_token, and has no event_id, no organization_id and no data. It is signed like an event (X-Webhook-Signature, section 5), and it adds a second header, X-Webhook-Verification, that holds the same token as the body. Answer 2xx; the reply does not need to hold anything. It is outside your subscription list, it is not kept in the events feed or in Recent Deliveries, it is not retried, and an error answer to it does not count towards the switch-off in section 4.3. Do not store it as an event.
The very first check can be sent before you have the signing secret. Flowmingo makes the secret when you save the endpoint and shows it to you only after the save has answered. When Flowmingo requires verification of new addresses, the save waits for the first check, so that check always comes before you have the secret: a receiver that already rejects signatures it cannot verify, such as the examples in section 5.2, cannot verify it and will answer it with an error, and the endpoint stays "waiting for verification" until a check gets a 2xx. Install the secret from the Webhook Endpoint Created! window in your receiver, then ask for the check again by saving the endpoint with a changed address (section 4.4). Deleting the endpoint and creating it again does not get around this, because the new endpoint gets a new secret that you again do not have when its first check is sent. Normally the endpoint is on from the start and the first check is sent in the background, so it may arrive before or after you have installed the secret; if your receiver answers it with an error, that changes nothing.
Invitation status payload
Event name: invitation.status.update
An event the email provider reported (here, delivered):
{
"interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"candidate_email": "alex@example.com",
"status": "invitation_email_delivered",
"reason": "delivered"
}
A failure that Flowmingo raises itself (here, an address that failed the check made before sending):
{
"interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"interview_name": "Sales Assessment",
"candidate_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"candidate_email": "alex@example.com",
"status": "invitation_email_failed",
"reason": "invalid_email"
}
candidate_id is often missing, and it is not always a candidate id. On events the email provider reports (delivered, opened, clicked, failed, spam) the key is normally left out, because the provider's report is not tied back to a candidate: identify the person by candidate_email together with interview_set_id. When a provider event does carry candidate_id, it can be the id of an invitation, not of the candidate, just as on a failure that Flowmingo raises itself. So a candidate_id on any of these events is an invitation id, and it does not match the candidate_id of an invitation.lifecycle.update or interview.* event. Do not make your receiver depend on this key.
reason is always present and is never null, and it is not a closed list. When the email provider reported the event, it is the provider's own word for what happened. Common ones are delivered, opened, clicked, bounced, bounce, dropped, blocked and spamreport, and you can meet others such as open, click, soft_bounce, hard_bounce, failed or error: use status for the outcome and treat reason as detail, and note that bounce and bounced are two separate words for a bounce. When Flowmingo raises the failure itself, reason is invalid_email (the address failed the check made before sending) or unsubscribed (the address was on the unsubscribe list). interview_name appears only on these two; it is absent on events the provider reports. A failure inside Flowmingo while the email is prepared does not send this event today: it shows on the invitation in the dashboard instead.
Possible status values:
invitation_email_delivered- Email Deliveredinvitation_email_opened- Email Openedinvitation_email_clicked- Link Clickedinvitation_email_failed- Email Failedinvitation_email_spam- Marked as Spaminvitation_email_accepted- reserved; nothing sends it today
Invitation lifecycle payload
Event name: invitation.lifecycle.update
{
"interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"interview_name": "Sales Assessment",
"candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"candidate_email": "alex@example.com",
"status": "invitation_sent"
}
status is invitation_created (the invitation exists), invitation_sent (the invitation email was queued for sending) or invitation_reminded (a reminder email was queued for sending). Queued means handed over for sending: it does not prove that the email left or was delivered. This event reports Flowmingo's own steps, while invitation.status.update reports what the email provider saw afterwards. It is not sent for practice invitations, or for a Matched Talent invitee who has not started yet.
Interview status payload
Event name: interview.status.update
{
"interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"interview_name": "Sales Assessment",
"candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"candidate_email": "alex@example.com",
"status": "started"
}
Possible status values:
initiated- Candidate opened the interview linkstarted- Interview Startedcompleted- Interview Completed
When completed arrives. Flowmingo does not send completed at the moment the candidate finishes answering. A job that runs every ten minutes can send it, once the candidate's improvement window has closed and any hold for the candidate's videos to be prepared has ended. It does so only for an eligible interview: one with a stored evaluation and valid answers, that is not a practice run, a try-out or a demo copy, and whose assessment was created by a person whose account has not been deleted. So it arrives after the window, not when the candidate stops, and an interview that does not qualify never sends it. The job looks at an interview only for about a day after its window closes. initiated and started are not held back, and the interview and holistic evaluation events wait for the same window (below).
Interview evaluation payload
Event name: interview.evaluation.update
{
"interview_set_id": "string",
"interview_name": "string",
"candidate_id": "string",
"candidate_email": "string",
"evaluation_type": "cv" | "interview" | "holistic",
"evaluation_score": "number | null",
"score_status": "scored" | "cannot_evaluate" | "not_scored",
"submission_id": "string",
"submission_url": "string"
}
Possible evaluation_type values:
cv- CV Evaluationinterview- Interview Evaluationholistic- CV + Interview Evaluation
evaluation_score and score_status. The score is a decimal out of 10 (for example 8.4). It is
null whenever there is no usable number, and score_status says why:
scored: a real score.cannot_evaluate: the AI ran and could not rate the attempt (too little to go on). This is not a zero, and it is not a failure on your side.not_scored: no score is available in this event. Do not read it as a low score or as a failure.
When it arrives. For evaluation_type interview and holistic this event is held back while the
candidate's improvement window is open, and re-sent by a job that runs every ten minutes once the window
closes, so it can land some minutes after the interview finished. A cv evaluation is not held.
submission_id is the id to pass to GET /company/integration/hiring/submissions/{id}/detail/v1 for the
full evaluation: the written summary, strengths, gaps, recommendations and per-criterion scores. See
Read results through the API.
Assessment completed payload
Event name: assessment.completed
One event for every assessment that has no status and result event of its own. It is sent for:
assessment_type |
Assessment |
|---|---|
skill_test |
Skill test |
technical_test |
Technical test |
showcase |
Showcase |
recording |
Recording evaluation |
questionnaire |
Questionnaire |
cv_evaluation |
CV evaluation |
For a technical test this event means the attempt has ended, not that its score is ready. The event carries no score for a technical test: score and score_percent are always null. Read the score through the API (see "A technical test event never carries a score" below).
AI interviews and role-plays do not send this event. They send only interview.status.update (with the status completed, which an eligible interview can send after the improvement window, not when the candidate finishes: see the interview status payload) and interview.evaluation.update (with the score), as they always have.
A CV evaluation and a recording evaluation send two events for one result, on purpose. Each sends interview.evaluation.update (as before; its evaluation_type is interview) and assessment.completed, both with the same submission_id. Let your receiver act on one of them: on assessment.completed if you want one event shape for all six assessment types, or on interview.evaluation.update if it already handles that event. An endpoint subscribed to both, or to All Events, normally receives both events for such a result; use submission_id to recognise the pair. There is one exception: if the submission was archived or deleted while it was being scored, assessment.completed is not sent and interview.evaluation.update still is (see "When it is sent" below), so do not wait for a counterpart that may never come.
A skill test:
{
"assessment_type": "skill_test",
"set_type": 3,
"interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"submission_id": "59065d06-a013-44c2-97d2-88e9d6b3c858",
"application_id": "2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3",
"invitation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "completed",
"score": 8.7,
"score_percent": 87,
"report_url": "https://team.flowmingo.ai/company/ai-interview-candidates?set_id=3f2504e0-4f89-41d3-9a0c-0305e82c3301&submissionId=59065d06-a013-44c2-97d2-88e9d6b3c858",
"completed_at": "2026-10-20T09:15:28.000Z",
"is_practice": false
}
A technical test: set_type is 5 and the score is always null:
{
"assessment_type": "technical_test",
"set_type": 5,
"interview_set_id": "8d6f1c3a-52e7-4b0e-9d14-6a3b7c2f9e10",
"submission_id": "e4b2a7d9-1c35-4f68-8a02-b9d3f6c1e547",
"application_id": "b7c2e9a4-3d18-4a65-b0f2-5e91c7d3a608",
"invitation_id": "0c9a5e71-6f24-4d83-9b1a-d2e48f7c3b95",
"status": "completed",
"score": null,
"score_percent": null,
"report_url": "https://team.flowmingo.ai/company/ai-interview-candidates?set_id=8d6f1c3a-52e7-4b0e-9d14-6a3b7c2f9e10&submissionId=e4b2a7d9-1c35-4f68-8a02-b9d3f6c1e547",
"completed_at": "2026-10-20T09:42:11.000Z",
"is_practice": false
}
| Field | Meaning |
|---|---|
assessment_type |
One of the six names above. |
set_type |
The same kind as a number: 2 CV evaluation, 3 skill test, 4 showcase, 5 technical test, 8 recording evaluation, 10 questionnaire. |
interview_set_id |
The assessment. |
submission_id |
The result. This is the id to pass to GET /company/integration/hiring/submissions/{id}/detail/v1 for the full result. For a technical test it is also how you read the score, which the event does not carry. |
application_id |
The candidate's application in the hiring project of the invitation (for a Showcase started from a link, the project of that link), or null. Always null for a CV evaluation and a recording evaluation. |
invitation_id |
The invitation, or null. Always null for a CV evaluation and a recording evaluation. |
status |
completed today. It says the attempt has ended; for a technical test it does not say the score is ready (see below). Ignore a value you do not know. |
score |
The score from 0 to 10 on the same scale as overall_score in the API, or null when the event carries none: always for a technical test, a showcase and a questionnaire, and when the AI could not rate a CV evaluation or a recording evaluation (see below). |
score_percent |
The score from 0 to 100. Only a skill test carries it; it is null for every other assessment, a technical test included (see below). |
report_url |
A link to the result in the Flowmingo dashboard. The reader has to be signed in to your workspace. null when there is none. |
completed_at |
When the attempt ended (ISO 8601). For a CV evaluation and a recording evaluation, when the result was recorded. |
is_practice |
Always false: a practice, trial or demo run never sends this event. |
The event carries ids, not personal data. There is no candidate name or email. To get them, call GET /company/integration/hiring/submissions/{submission_id}/detail/v1 with a key that has read_hiring and read_candidates (see the API guide).
What score and score_percent hold, by assessment:
assessment_type |
score |
score_percent |
|---|---|---|
skill_test |
The percentage divided by 10. | The percentage. |
technical_test |
null: the event carries no score (see below). |
null |
cv_evaluation, recording |
The AI's rating, 0-10. null when the AI could not rate it. |
null |
showcase |
null: the event carries no score. A recruiter's rating of a Showcase is sent as submission.review.update. |
null |
questionnaire |
null: the event carries no score and no pass or fail result. |
null |
A technical test event never carries a score. score and score_percent are null for every technical test, whether or not any code is still being graded. The event tells you that the attempt has ended: the candidate submitted it, ended it early, or the time ran out. It does not tell you that the score is ready. Flowmingo runs the code a candidate submits against its test cases in a sandbox, and that can still be running when the attempt ends, so the score can still change after the event. The event is sent once, and no second event follows when the score changes.
Read the score with GET /company/integration/hiring/submissions/{submission_id}/v1 or GET /company/integration/hiring/submissions/{submission_id}/detail/v1 (a key with read_hiring is enough for the score; see the API guide, section 5.5). A read made right after the event can show a provisional number, and nothing in the response, and no later event, says when the number has stopped changing. So do not store the first number you read as the result: read it when you need to use or show it, or read it again later and keep the latest value. The API gives a technical test no score_percent; multiply overall_score by 10 if you want a percentage (the stored rating is rounded to two decimals, so the percentage is an approximation, to the nearest tenth of a point). Do not use score: null to detect that grading is under way, because it is null for every technical test. A technical test event created before 20 Oct 2026 can carry a score under the earlier rule, and it keeps that score if it is read from the events feed or sent again: ignore it and read the result from the API.
When it is sent. Once for each result: the event takes its event_id from the result, so a second path to the same completion does not announce it again. It is never sent for a practice run, a trial run or a demo run. Results that were completed before this event existed are not sent. By assessment:
- Skill test: when the candidate submits, answers the last question, or runs out of time while still in the test. An attempt that Flowmingo closes afterwards because the candidate had walked away is not announced.
- Technical test: for every way an attempt ends: the candidate submits it, ends it early, or the time runs out. The event is a completion signal only: its
scoreandscore_percentarenull. - Showcase: when the Showcase is completed. The event is the same for every submission: it carries no take count and nothing else about how the candidate recorded it.
- Questionnaire: when the candidate submits their answers.
- CV evaluation and recording evaluation: when the evaluation result is stored, together with
interview.evaluation.update. If the submission has been archived or deleted by then, noassessment.completedis sent (interview.evaluation.updatestill is).
Application created payload
Event name: application.created
{
"application_id": "2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"com_job_post_id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"com_stage_id": "5b8d1e92-3a7c-4c60-b2f4-91e6a0d7c358",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
The five ids are always present (null when the application has none). While your organization has candidate sources switched on (the recruiter's Source and Attributed to screens), two more keys are added; without them the payload is the five ids and nothing else:
source_typenames the candidate's Source only when it is one of Flowmingo's 13 built-in Sources:flowmingo,job_link,invited,matched_talents,ai_interview_link,roleplay_link,showcase_link,skill_test_link,technical_test_link,chat_screening_link,manually_added,api_addedorunknown. It isnullfor a Source your own organization created (a tracking link can use one), and when the application has no Source.source_detailis the free text kept with the Source: a short label such asCV upload, the title of one of your job posts, the label of a tracking link, or a note your team wrote. It isnullwhen there is none. It can name a person.
It is sent when a candidate arrives on a hiring project: by applying on one of your job pages, by starting an AI interview, a role-play or a Showcase from a link that files them on the project, from an invitation that creates an application, when a recruiter adds candidates one by one or in bulk, and when the API adds one (POST /hiring/applications/v1). It is sent once for each application: the event takes its event_id from the application, so a second path to the same application does not announce it again. It is not sent for a Matched Talent invitee who has not started yet, for a candidate's practice attempt at an AI interview or a role-play, or for a try-out of an interview (Flowmingo files no application when a sitting starts as practice or as a try-out), and a way of reaching a project that is not listed here does not send it.
The event goes to your organization's endpoints. A job post that has no organization of its own is announced under the organization of its interview set, and failing that of its hiring project.
Candidate stage changed payload
Event name: candidate.stage_changed
{
"application_id": "2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"com_job_post_id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"com_stage_id": "5b8d1e92-3a7c-4c60-b2f4-91e6a0d7c358",
"com_stage_from_id": "9e7a4d31-6b02-4f85-8c19-3a5d70e2b6c4",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
com_stage_id is the stage the application moved to and com_stage_from_id the stage it came from (null or absent on older events). It is sent for a move in the dashboard and for move-stage and reject through the API. Moving a card into a Rejected stage in the dashboard sends this event and not application.rejected.
Application rejected payload
Event name: application.rejected
{
"application_id": "2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"com_job_post_id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"com_stage_id": "9e7a4d31-6b02-4f85-8c19-3a5d70e2b6c4",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
Sent when an application is rejected through the API (com_stage_id is then the Rejected stage it moved into) and when a recruiter removes it from a hiring project (com_stage_id is the stage it was in).
Submission review payload
Event name: submission.review.update
{
"submission_id": "59065d06-a013-44c2-97d2-88e9d6b3c858",
"interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"action": "rated",
"rating": 8,
"activity_id": "7241c51b-23a7-4072-b345-5484cd0b54af"
}
action is note_added, rated or shortlisted. rating appears for a rating and shortlisted (a boolean) for a shortlist change. The text of a note is never included. The event is sent for reviews made in the dashboard and through the API. Taking a rating or a shortlist mark back with the undo route (see Update your pipeline and reviews through the API) sends it again: action is rated with the rating that was put back (rating is null when there was none before) or shortlisted with the mark that was put back. Undoing a note that carries no rating sends nothing.
Approval status payload
Event name: approval.status.update
Some changes that an API key or an AI assistant asks for wait for a person to approve them at an approval link: an invitation, a reject, a move to a stage that emails the candidate, a removal of candidates from a hiring project and the deletion of a hiring project (see the approval routes in the API guide, section 9.7). This event tells your endpoint how the decision on such a change went. It does not carry the change itself.
{
"change_id": "4f1c7b5a-3d0e-4b8a-9e51-2a6c0d9f1b77",
"action": "invite",
"status": "approved",
"decided_at": "2026-11-11T08:00:01.482Z",
"approved_at": "2026-11-11T08:00:01.482Z",
"interrupted": false,
"outcome": null
}
All seven fields are always present. The sub-event is status.
| Field | Type | Meaning |
|---|---|---|
change_id |
text (UUID) | The id the approval route gave when the change was staged. It is the id that GET /company/integration/approvals/{id}/v1 takes. |
action |
text | invite, reject, move_stage, remove_candidates or delete_project. |
status |
text | approved, declined or failed. Ignore a value you do not know. |
decided_at |
time | When the decision was taken, in UTC to the millisecond, like 2026-11-11T08:00:01.482Z. For approved it is the time of the yes, and it is the same as approved_at. For declined and failed it is the time Flowmingo's database recorded the cancel or the close. |
approved_at |
time or null |
When a person said yes, in the same form. null for declined, and for a failed change that was closed before a yes counted. |
interrupted |
true or false |
Reserved. It is false in every event Flowmingo sends today. |
outcome |
null |
Reserved. It is null in every event Flowmingo sends today, and no count of what was done is sent. Read what was done from GET /company/integration/approvals/{id}/v1. Write your receiver so that it accepts null and ignores anything else. |
The event never names a candidate, a recipient, the person who decided, the key that asked, a project, an application, an approval link or any text. The event id and the time come in the usual envelope.
When it is sent.
approved: a person approved the change at the approval link and Flowmingo carried it out. It is sent for everyaction. For a change that names several candidates, read the status route for what was done to each. Anapprovedevent reports a change that was carried out.declined: a person cancelled the change on the approval page, before it expired.failed: sent only for the deletion of a hiring project (delete_project), when the deletion was closed without being carried out, for example because the project had changed since it was asked for. Flowmingo does not send afailedevent for any otheraction(an invitation, a reject, a move, a removal of candidates): for those, read the status route.
When it is not sent. There is no event when a change expires, including when a person's cancel arrives after the expiry; when the key that asked withdraws the change; when a new request replaces a pending one; when a change is taken back with the undo route; or for changes made in the Copilot. A note, a rating or a shortlist mark needs no approval and sends no approval.status.update.
Treat it as a hint, and read the status route. Flowmingo makes this event once, after the decision has been recorded. If it could not be made, for example because a service restarted at that moment, it is not made again later, so you do not get it. Once made, it is delivered like every other event (section 4). So it can be missing, and it is not an authorisation: read GET /company/integration/approvals/{id}/v1 with the key that asked before you tell anyone that something happened or act on it. A new change that you stage after an approved event still needs a person's yes.
Who receives it. This event describes a decision on a change that one of your API keys or your AI assistant asked for. It is delivered to every endpoint of your organization that is switched on and subscribed to All Events or picked this event, and it is read from the events feed (GET /company/integration/events/v1, with event_type=approval.status.update) by every key of your company that can read the whole company's hiring data, not only by the key that asked. It carries the change's id, what was asked for, the decision and its times, and no names.
Note: payloads gain fields over time. Read the keys your integration needs and ignore the rest; a new key is not a breaking change. On real deliveries
datacarries notimestampof its own: use the envelope's. (A Send Test Event payload can include adata.timestamp; do not build a parser that needs it.)
A Send Test Event payload is not a copy of a real one. It is sample data (all-zero ids, the address test@example.com, the interview name Test Interview) with "is_test": true, and it differs from a real payload in ways that go beyond a data.timestamp:
invitation.status.updatehas noreason, although a real one always has it, and it carries aninterview_name, which real events from the email provider do not.application.rejectedcarries areasonwith the valuetest. No realapplication.rejectedevent has areason.- The sub-event you pick sets
status(orevaluation_type, oraction, orassessment_type). If you leave it empty, no sub-event is chosen for you (not even a random one) and that field is left out of the payload, with exceptions: anassessment.completedtest falls back to a skill test, and anapproval.status.updatetest to the statusapproved. approval.status.updatecarries sample counts inoutcome({ "done": 2, "skipped": 0, "failed": 0 }for the default test), althoughoutcomeisnullin every real event. Itschange_idis all zeros and itsactionisinvite. The picker has no sub-events for this event, so a test sent from the dashboard has the statusapproved.- The values are samples: the scores, the ratings and the links are made up.
Use a test to check your address and your signature check, and write your parser against the payloads in this guide.
4. Delivery
4.1 What to expect
- Limited retries, and repeats are possible. Flowmingo tries an event up to 6 times (section 4.2), so delivery is not guaranteed, and the same event can also be delivered more than once, for example when your endpoint did the work but answered too slowly. Keep the
event_idvalues you have handled and skip repeats, and recover anything you missed from the events feed (section 4.4). - Not in order. An event that had to be retried arrives after events that happened later. Order by the envelope's
timestamp. - Answer fast, once the event is safe. Deliveries time out at 10 seconds, and a timeout counts as a failure. Verify the signature, store or queue the event, then answer
2xx, and do the slow processing afterwards. A2xxacknowledges receipt and normally stops the automatic retries. Duplicates are still possible, so deduplicate byevent_id. If you could not save the event, answer an error so Flowmingo can retry within its retry limits. Any2xxanswer acknowledges the event. - Only the status counts. Any
2xxacknowledges the event, whatever your reply holds. If your server has sent a2xxstatus and the rest of the reply then stalls or is cut off, the event still counts as delivered. Flowmingo reads at most the first 16 KB of a reply. - About thirty days of history. Events and their delivery logs are kept for about 30 days.
4.2 Retries
A delivery attempt fails when your endpoint answers with anything other than 2xx (a redirect included), does not answer within 10 seconds, or cannot be reached. Flowmingo then tries again, with longer waits each time: up to 6 attempts in total, spread over about a day. The first retry comes about 2 minutes after the first failure, then about 20 minutes, 2 hours, 7 hours and 14 hours after the attempt before it, each wait with up to 30 seconds added. The last attempt is therefore about 23 hours after the first, and an event is given up on roughly a day after it first failed. After the 6th attempt the event is left in an error state for that endpoint and Flowmingo does not try it again on its own. A person can send it again by hand with Send again (section 4.4). Failed attempts feed the switch-off in section 4.3 through how long the endpoint has been failing, not through how many attempts failed.
4.3 An endpoint that keeps failing is switched off
When every delivery to an endpoint has failed for 3 days in a row, Flowmingo switches the endpoint off and stops sending to it, so a dead address does not collect failures forever. The 3 days run from the first failed attempt, and the switch-off happens at the first failed delivery after they have passed. Any single
2xxto a real event ends the run (a successful Send test event does not), and the next failure starts the clock again from zero. So does a day with no failed attempt at all: an endpoint that is sent nothing is not counted as failing while it is quiet. An endpoint that is sent very few events may therefore never reach 3 days.
At the first failed delivery after a day and a half of failures, Flowmingo sends a warning email to the Owners and Admins of the company (and to the person who created the endpoint while they are still a member), up to ten people. It says since when the endpoint has been failing, what your server answered (the HTTP status, in plain words), and the day after which a failed delivery switches the endpoint off. One successful delivery of a real event clears the warning. It is a service notice, so an unsubscribe from product emails does not stop it.
Flowmingo emails up to ten people when this happens: the Owners of the company first, then the person who created the endpoint (while they are still a member), then the Admins. Only members who have a company-level role and an email address on file are counted, not people who only have access to one hiring project. This is best effort: if Flowmingo cannot read the member list at that moment, nobody is emailed, so look at the endpoint's card too. A product-email unsubscribe does not stop it. The email says the endpoint was switched off because it kept failing for 3 days, since when, what Flowmingo saw (for example the HTTP status your server answered, or that it timed out) and that events which happen while it is off are not sent to it. The endpoint's card in Settings -> Integrations -> Webhooks shows the same, with the date it was switched off. Events that happen while the endpoint is off are not sent to it later, and a delivery waiting for a retry is closed, not sent, if the endpoint is still switched off when the retry falls due. A delivery closed like that can be sent again by hand once the endpoint is back on (section 4.4). An event that happened while the endpoint was off and was never queued for it cannot: read it from the events feed. Fix the problem, then turn the endpoint back on with its switch in Settings -> Integrations -> Webhooks. Turning an endpoint from off to on starts the clock again from zero, so one failed attempt does not switch it off again straight away. Saving changes to an endpoint that is already on leaves the run of failures as it is. If deliveries stop arriving, check that switch before assuming no events occurred.
4.4 Catching up after an outage
Flowmingo does not replay an event on its own once it has been given up on. A person can send it again by hand, in Settings -> Integrations -> Webhooks:
- Send again, beside a delivery in Recent Deliveries (and View all), sends that one event to the endpoint again. It is offered for a delivery that failed all its attempts, and for one that was closed because the endpoint was switched off when it fell due.
- Send again for all failed events (last 7 days), on the endpoint's page, sends again every delivery queued in the last 7 days that ended in an error: one that failed all its attempts, or one that was closed because the endpoint was off.
A resend starts again from the first attempt (up to 6, section 4.2) and carries the same event_id as before, so deduplicate as always. Both are refused while the endpoint is switched off or still waiting for verification. Switch a switched-off endpoint back on first. An endpoint that is waiting for verification cannot be switched on by hand: see "A new endpoint can wait for verification" below. They need the same permission as managing webhooks, and they reach only deliveries that are still kept (about 30 days, section 4.1).
Two things cannot be sent again: an event that happened while the endpoint was off, because no delivery was ever queued for it, and an event older than the history Flowmingo keeps. For the first, read the event from the events feed, GET /company/integration/events/v1, while it is still kept: the feed holds the events of about 30 days and uses the same event_id values (see the API guide). The second cannot be recovered at all: Flowmingo deletes the event itself and its delivery records on the same schedule, so an event older than about 30 days is gone from Recent Deliveries and from the events feed alike. The feed needs the read_hiring scope and a key whose creator has access to all hiring projects (API guide, section 7): a key created by someone limited to some projects, even a list that names every project today, is refused with 403.
A new endpoint can wait for verification. When Flowmingo requires verification of new addresses, a new endpoint stays "waiting for verification" and receives none of its subscribed events until its connectivity check (section 3) gets a 2xx answer; a test sent with Send Test Event can still reach it. Its switch is refused until then, so you cannot turn it on by hand. The first check is sent while the endpoint is being saved, before the signing secret is shown to you, so a receiver that rejects signatures it cannot verify will have failed it. That is expected. Copy the secret from the Webhook Endpoint Created! window and install it in your receiver (the hex part only, section 5), make sure your address answers 2xx to the check, then ask for the check again: edit the endpoint and save it with a changed address. While Flowmingo requires verification, a changed address starts a new check, signed with the secret you now hold; saving the same address again starts none, and when Flowmingo does not require verification no new check is sent. Do not delete the endpoint and create it again to get a new check: the new endpoint gets another new secret that you do not have when its first check is sent, so the same thing happens again. If you lost the secret before you could install it, the endpoint's Regenerate Secret (section 1) gives a new one: install that one, then change the address.
4.5 Which addresses are refused
Webhook addresses must be public and must start with http or https (use https). When you save an endpoint, Flowmingo refuses localhost, names that end in .local or .internal, and addresses in private, loopback, link-local or cloud-metadata ranges. Any public port is allowed.
What your endpoint must do. Use a public HTTPS address with reliable DNS. Flowmingo looks the name up again for every delivery, and the attempt fails if the name does not resolve or if any address it leads to is private. The endpoint must accept a POST and answer with a 2xx within the 10 seconds of section 4.1; a failed attempt is retried (section 4.2). If Flowmingo cannot reach your server, Recent Deliveries on the endpoint shows each attempt and the reason it failed.
Credentials written into the address (https://user:password@your-app.com/) are not sent. Check the signature (section 5) to know a request is from Flowmingo.
Redirects are treated as failures, not followed. Point the endpoint at its final URL: an address that answers with a redirect (adding www., or http to https) never receives a delivery, and the attempt counts as failed.
5. Signature verification
Every delivery includes an X-Webhook-Signature header:
X-Webhook-Signature: t=1760951730,v1=c5968e38a9d69572df9af234b4369cc208a3107386724dbacddb392b1b2799c0
tis the UNIX timestamp (seconds) at which this delivery attempt was signed. A retry is signed again, so itstis new.v1is the hexadecimal HMAC-SHA256 of the textt, a full stop and the raw request body:<t>.<body>.- The signing key is the hex part only: strip the
whsec_prefix. Flowmingo displays the secret aswhsec_<hex>, but signatures are computed with<hex>alone, used as the text it is (do not decode it from hex). Signing with the fullwhsec_...string produces a different digest and every verification fails. - The connectivity check of section 3 (
endpoint.verification) is signed the same way and adds a header of its own,X-Webhook-Verification, which holds itsverification_token.
5.1 What to check
- Read the raw body before any JSON parser touches it. Re-serialising parsed JSON changes the bytes and the signature no longer matches.
- Compute the HMAC and compare it with
v1using a constant-time comparison. - Reject the request when
tis more than five minutes from your clock, in either direction. This stops someone replaying a captured request later. Keep your server clock correct (NTP). - Answer
400when verification fails. When it passes, store or queue the event, then answer2xx, before you do any slow work (section 4.1).
5.2 Verifier code
These verifiers do all four checks, and handle a malformed header without crashing.
import crypto from 'node:crypto'
const SECRET = process.env.FLOWMINGO_WEBHOOK_SECRET // the hex part only, without the "whsec_" prefix
const TOLERANCE_SECONDS = 5 * 60
export function verifySignature(rawBody, header, nowSeconds = Date.now() / 1000) {
if (typeof header !== 'string') return false
const t = /(?:^|,)\s*t=(\d+)/.exec(header)?.[1]
const v1 = /(?:^|,)\s*v1=([0-9a-fA-F]+)/.exec(header)?.[1]
if (!t || !v1) return false
if (Math.abs(nowSeconds - Number(t)) > TOLERANCE_SECONDS) return false
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.`).update(rawBody).digest()
const received = Buffer.from(v1, 'hex')
return received.length === expected.length && crypto.timingSafeEqual(received, expected)
}
With Express, keep the body raw for this route:
import express from 'express'
const app = express()
app.post('/flowmingo-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verifySignature(req.body, req.get('X-Webhook-Signature'))) return res.sendStatus(400)
let event
try {
event = JSON.parse(req.body.toString('utf8'))
} catch {
return res.sendStatus(400)
}
// A connectivity check has an endpoint_id and no event_id: there is nothing to keep.
if (event.event_type !== 'endpoint.verification') {
try {
await saveEvent(event) // store or queue it first, so that it cannot be lost
} catch {
return res.sendStatus(500) // Flowmingo will try again
}
}
res.sendStatus(200) // answered once the event is safe: do the slow work later, from your queue
})
// Replace this with a write to your database or queue. Keep it quick, and let it throw if the write fails.
// Skip an event_id you have already stored: the same event can arrive more than once.
async function saveEvent(event) {
console.log(event.event_type, event.event_id)
}
import hashlib
import hmac
import os
import time
SECRET = os.environ["FLOWMINGO_WEBHOOK_SECRET"] # the hex part only, without the "whsec_" prefix
TOLERANCE_SECONDS = 5 * 60
def verify_signature(raw_body, header, now=None):
if not isinstance(header, str):
return False
parts = {}
for item in header.split(","):
key, _, value = item.strip().partition("=")
parts[key] = value
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not (t.isascii() and t.isdigit()) or len(t) > 12 or not v1:
return False
if abs((time.time() if now is None else now) - int(t)) > TOLERANCE_SECONDS:
return False
expected = hmac.new(SECRET.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), v1.encode())
With Flask, read the raw bytes with request.get_data():
from flask import Flask, request
app = Flask(__name__)
@app.post("/flowmingo-webhook")
def flowmingo_webhook():
if not verify_signature(request.get_data(), request.headers.get("X-Webhook-Signature")):
return "", 400
event = request.get_json(silent=True)
if not isinstance(event, dict):
return "", 400
# A connectivity check has an endpoint_id and no event_id: there is nothing to keep.
if event.get("event_type") != "endpoint.verification":
try:
save_event(event) # store or queue it first, so that it cannot be lost
except Exception:
return "", 500 # Flowmingo will try again
return "", 200 # answered once the event is safe: do the slow work later, from your queue
def save_event(event):
# Replace this with a write to your database or queue. Keep it quick, and let it raise if the write fails.
# Skip an event_id you have already stored: the same event can arrive more than once.
print(event["event_type"], event["event_id"])
<?php
function verify_flowmingo_signature(string $rawBody, ?string $header, string $secret, int $tolerance = 300): bool
{
if ($header === null) {
return false;
}
$t = null;
$v1 = null;
foreach (explode(',', $header) as $item) {
$pair = explode('=', trim($item), 2);
if (count($pair) !== 2) {
continue;
}
if ($pair[0] === 't') {
$t = $pair[1];
}
if ($pair[0] === 'v1') {
$v1 = $pair[1];
}
}
if ($t === null || $v1 === null || !ctype_digit($t)) {
return false;
}
if (abs(time() - (int) $t) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, $v1);
}
$secret = getenv('FLOWMINGO_WEBHOOK_SECRET'); // the hex part only, without the "whsec_" prefix
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null;
if (!verify_flowmingo_signature($rawBody, $header, (string) $secret)) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
if (!is_array($event)) {
http_response_code(400);
exit;
}
// A connectivity check has an endpoint_id and no event_id: there is nothing to keep.
if (($event['event_type'] ?? '') !== 'endpoint.verification') {
try {
save_event($event); // store or queue it first, so that it cannot be lost
} catch (Throwable $e) {
http_response_code(500); // Flowmingo will try again
exit;
}
}
http_response_code(200); // answered once the event is safe: do the slow work later, from your queue
// Replace this with a write to your database or queue. Keep it quick, and let it throw if the write fails.
// Skip an event_id you have already stored: the same event can arrive more than once.
function save_event(array $event): void
{
error_log(($event['event_type'] ?? '') . ' ' . ($event['event_id'] ?? ''));
}
5.3 A test vector
Use this to check your verifier. It uses the verifier's time check, so pass 1760951730 as the current time when you test, or the signature will be rejected as too old.
secret (hex part): 3b1f0c9d5e7a42c8a6d1f8e2b4c7095e1d3a6f8b0c2e4a6d8f1b3c5e7a9d0f21
t: 1760951730
raw body: {"event_id":"cca11cbd-00fd-4e16-ba60-75625ee9d196","event_type":"assessment.completed"}
signed text: 1760951730.{"event_id":"cca11cbd-00fd-4e16-ba60-75625ee9d196","event_type":"assessment.completed"}
v1: c5968e38a9d69572df9af234b4369cc208a3107386724dbacddb392b1b2799c0
5.4 Rotating the secret
Regenerate Secret replaces the secret at once. There is no overlap, and a delivery that was already being prepared can still be signed with the previous secret. Update your verifier to the new secret as soon as Flowmingo shows it. Events that fail verification in the meantime are answered with an error and retried (retries run for about a day, section 4.2), but do not assume that retries will recover every event: if your server keeps failing for 3 days the endpoint is switched off (section 4.3), and an event given up on is not sent again by Flowmingo on its own (a person can use Send again, section 4.4). After a rotation, check that the endpoint is still switched on, and read what you missed from the events feed (section 4.4).
6. Webhook Issues
Signature verification failures: Use the raw request body (not parsed JSON), the hex secret without
whsec_, and thetvalue from the same header. If you regenerated the secret, your verifier still has the old one. Test with the vector in section 5.3.A new endpoint stays "waiting for verification": when Flowmingo requires verification of new addresses, the first connectivity check is sent before you see the signing secret, so a receiver that already checks signatures answers it with an error. Install the secret, then save the endpoint with a changed address to start a new check (section 4.4). Deleting the endpoint and creating it again does not help: it gets a new secret that you again do not have when its first check is sent.
Missing events: Check that the endpoint URL is public and correct, that its switch is on (section 4.3), and that it is subscribed to the event types you expect (section 2). Recent Deliveries on the endpoint shows what Flowmingo tried and what your server answered. Read
GET /company/integration/events/v1to see whether the event happened at all. It needs theread_hiringscope and a key whose creator has access to all hiring projects (API guide, section 7).Duplicate events: The same event can be delivered more than once. Skip an
event_idyou have already handled (section 4.1).Unexpected event types: Acknowledge them with
2xxand ignore them (section 2).Retry behavior: see Retries in section 4.2: up to 6 attempts spread over about a day, and the endpoint is switched off after 3 days of failed deliveries in a row (section 4.3). Answer with any 2xx to acknowledge receipt.
Respond within 10 seconds. Deliveries time out at 10s, and a timeout counts as a failure. Store or queue the event, answer
2xx, and do your processing afterwards; an endpoint that works synchronously and answers in 12s will fail every delivery and eventually be switched off.Redirects are treated as failures, not followed. Point the endpoint at its final URL: an address that 301s to a canonical host (adding
www., or http to https) never receives a delivery.