Flowmingo emits notifications whenever candidates move through invitation, interview, or evaluation workflows. 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.
1. Create a webhook endpoint
Log in to your Flowmingo workspace and navigate to Settings -> Webhooks. Click Create Endpoint and provide:
- Name: A label to identify this endpoint later.
- URL: A publicly accessible HTTPS endpoint you control (for example:
https://your-app.com/api/flowmingo-webhook). - Events: Event types this endpoint should receive.
- Description (optional).
On save, Flowmingo returns a webhook secret. Store it securely. You need this value to verify X-Webhook-Signature.
Managing endpoints:
- Update/Delete: Return to Settings -> Webhooks, open an endpoint, and edit or delete it.
- Regenerate Secret: Rotate the signing secret and update your verifier before removing the old secret.
- Send Test: Trigger a test webhook to validate your endpoint without waiting for a real event.
2. Supported events
| Event | Description | Typical trigger |
|---|---|---|
invitation.status.update |
Invitation email lifecycle updates (accepted, delivered, opened, clicked, failed). | Notification service after Flowmingo sends interview invitations. |
interview.status.update |
Candidate starts/completes an interview. | Candidate interview emits state changes. |
interview.evaluation.update |
New CV/interview/holistic evaluation results are stored. | Evaluation processor finishes scoring. |
Subscribing to a parent event (e.g., invitation.status.update) receives all sub-events of that type. You can also subscribe to specific sub-events only (e.g., invitation.status.update.invitation_email_failed).
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
}
is_test is only included for test deliveries.
Invitation status payload
Event name: invitation.status.update
{
"interview_set_id": "019bcb66-2d6e-795a-9770-148355e51c51",
"candidate_id": "019bcb66-66d9-781e-96cc-e4a2b74c7fce",
"candidate_email": "alex@example.com",
"status": "invitation_email_delivered",
"reason": "delivered"
}
reason is always present and is never null — it mirrors the provider signal (delivered, opened,
clicked) or names the failure (invalid_email, unsubscribed, bounce, dropped). interview_name
appears only on failures we raise ourselves (invalid_email, unsubscribed); it is absent on the
provider-reported delivered / opened / clicked events.
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
Interview status payload
Event name: interview.status.update
{
"interview_set_id": "019bcb66-2d6e-795a-9770-148355e51c51",
"interview_name": "Sales Assessment",
"candidate_id": "019bcb66-66d9-781e-96cc-e4a2b74c7fce",
"candidate_email": "alex@example.com",
"status": "started"
}
Possible status values:
initiated- Candidate opened the interview linkstarted- Interview Startedcompleted- Interview Completed
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 exists yet.
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.
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 payload does include adata.timestamp; do not build a parser that needs it.)
Retries
If your endpoint returns non-2xx or fails, Flowmingo retries with exponential backoff: up to 6 attempts
in total, starting ~2 minutes after the first failure and doubling each time, capped at 2 hours between
attempts. After the 6th the event is left in an error state and is not retried again.
An endpoint that keeps failing is disabled automatically. After 15 consecutive failed deliveries we deactivate it and stop sending, so a dead URL does not accumulate forever. Any single
2xxresets the counter. If deliveries stop arriving, check the endpoint's status under Settings -> Webhooks before assuming no events occurred.
4. Signature verification
Every delivery includes an X-Webhook-Signature header:
X-Webhook-Signature: t=1732619345,v1=3f2fdd0af4...
tis the UNIX timestamp (seconds).v1is an HMAC-SHA256 digest oftimestamp + '.' + 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. Signing with the fullwhsec_...string produces a different digest and every verification fails.
Example middleware (Node.js/Express) that validates the signature using the raw request body:
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.FLOWMINGO_WEBHOOK_SECRET!;
function verifySignature(rawBody: Buffer, header: string | string[] | undefined) {
if (!header || Array.isArray(header)) return false;
const parts = header.split(',');
const timestamp = parts.find((p) => p.startsWith('t='))?.split('=')[1];
const signature = parts.find((p) => p.startsWith('v1='))?.split('=')[1];
if (!timestamp || !signature) return false;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody.toString('utf8')}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}
Reject requests if signature verification fails or if the timestamp is outside your allowed skew window.
Webhook Issues
Signature verification failures: Ensure you're using the raw request body (not parsed JSON) when computing the HMAC signature. The timestamp must match exactly what's in the header.
Missing events: Check that your endpoint URL is correct, HTTPS, and publicly accessible. Verify the endpoint is active and subscribed to the correct event types.
Retry behavior: see Retries in section 3 — up to 6 attempts with exponential backoff, then the endpoint auto-disables after 15 consecutive failures. Answer with any 2xx to acknowledge receipt.
Respond within 10 seconds. Deliveries time out at 10s, and a timeout counts as a failure. Acknowledge first and do your processing afterwards; an endpoint that works synchronously and answers in 12s will fail every delivery and eventually be disabled.
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.