API Key Integration
Flowmingo has a REST API for your own systems. With an API key you can create hiring projects and assessments, read a project and edit its name and the words of its job post, publish and boost a job post, invite candidates (now or at a time you choose) and pause and resume an invitation's automatic reminders, hand over recordings of interviews you ran yourself to be scored, read scores and AI evaluations, read the transcript and the recording links of a finished interview, read counts of what was recorded during a finished Skill Test or Technical Test, ask a question about your hiring data in plain words, make and manage tracking links (the custom share links of Settings), read your email sequences and how they are doing, update your hiring pipeline, and poll the same events that webhooks deliver. This guide covers all 77 operations of the public API. If you would rather be told when something happens than ask, use webhooks.
- Base URL:
https://apis.flowmingo.ai/company/integration. A path such asGET /me/v1in this guide meansGET https://apis.flowmingo.ai/company/integration/me/v1. - One environment: the public API has one environment, production. Every call acts on your real data, and every key starts with
fl_live_. To try a call safely, invite yourself first (see section 1.4). There is no sandbox or test mode: section 1.5 says what that means and how to test without reaching real candidates. - Machine-readable description: the OpenAPI document (version 3.0) is at
https://flowmingo.ai/openapi.json. It is a static file that lists the 77 operations of this guide, each with its summary, the scope it needs, its parameters, its request body and its response shape, and thex-api-keyheader. Import it into Postman, Insomnia or any API client or no-code tool that reads OpenAPI. Its server ishttps://apis.flowmingo.ai/company, so its paths start with/integration/. We refresh the file when the API changes; if it and this guide ever differ, this guide is right.
1. Quick start: your first invitation in 5 minutes
You need an Owner or Admin account (only they can create API keys) and one AI interview or role-play in your workspace (make one in the dashboard, or through the API: section 3.6). The samples use curl; any HTTP client works the same way.
1.1 Create a key with the scopes you need
- Sign in at
https://team.flowmingo.ai, click your email at the bottom-left of the sidebar, then Settings. - Open Integrations -> API Keys and click Create New Key.
- Type a Name. The name is required.
- Tick the permissions (scopes) the key needs. For this quick start tick Read hiring data (
read_hiring) and Invite candidates (invite_candidates). Tick Read candidates (read_candidates) as well if you want candidate names and email addresses in results. A key with no scope ticked can do almost nothing, so always tick at least one. - Click Create secret key.
- Copy the secret from the API Key Created! window now. It is shown once and looks like
fl_live_abcd12345.n8G3zPNw9gWkK1Kj.
Keep it in an environment variable for the next steps:
export BASE='https://apis.flowmingo.ai/company/integration'
export FLOWMINGO_API_KEY='fl_live_abcd12345.n8G3zPNw9gWkK1Kj'
1.2 Check the key
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" "$BASE/me/v1"
{
"api_key_id": "0b0a9f3e-6c2d-4c1a-9a47-5d3e8b7f2a10",
"organization_id": 4217,
"scopes": ["read_hiring", "invite_candidates"],
"expires_at": null
}
A 200 with your scopes means the key works. A 401 means the key was not accepted (section 11 explains each message).
1.3 Find an assessment and look at results
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" "$BASE/hiring/interview-sets/v1?limit=5"
{
"data": [
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"title": "Senior Backend Engineer - Async Interview",
"position": "Senior Backend Engineer",
"status": 1,
"assessment_type": "ai_interview",
"set_type": 1,
"interview_duration": 20,
"cfg_lingual_ids": [290],
"project_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"created_at": "2026-10-01T09:30:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 5
}
Copy the id of an assessment whose assessment_type is ai_interview or role_play and whose status is 1 (published): the simplest kinds to send through the invite route. An invitation to a draft, inactive or archived assessment is created but not emailed (section 4.5). Section 4.1 says what a key whose creator is limited to some projects can send to. A Skill Test, a Technical Test or a Showcase can be sent through it too, with a few extra rules (sections 4.2, 4.6 and 4.8). Then list its results:
export SET_ID='3f2504e0-4f89-41d3-9a0c-0305e82c3301' # replace with your own id
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" "$BASE/hiring/submissions/v1?com_interview_set_id=$SET_ID&limit=5"
The answer is { "data": [...], "total": n, "page": 1, "limit": 5 }. A candidate you have invited but who has not started yet is listed too, with status 5 and submission_at null (section 5.2 describes every field).
1.4 Send one invitation, to yourself first
curl -X POST "$BASE/interview/candidate/invite/v1" \
-H 'Content-Type: application/json' \
-H "X-API-Key: $FLOWMINGO_API_KEY" \
--data @- <<EOF
{
"com_interview_set_ids": ["$SET_ID"],
"candidates": [{ "name": "Your Name", "email": "you@example.com" }],
"invitation_message": "Testing the Flowmingo API: please ignore this invitation."
}
EOF
Replace you@example.com with your own address. This creates a real invitation and, for a published assessment, makes it eligible for an email to be queued. That is not a promise: the sender can still skip an invitation (for example when the candidate already has a result on that assessment, or the address failed our address check). Queued means handed over for sending, not delivered: the email can leave before or after the answer, and it can be delayed. For a draft, an inactive or an archived assessment nothing is emailed and the result carries warnings (section 4.5); a result without warnings still does not prove that the email was delivered. The answer is 201 with a results array that holds the invitation that was created (section 4.5 shows the full response). Once that works, read section 2.3 to give a production key only the scopes it needs, and section 8 for how errors, limits and retries behave. If you use Zapier, Make or n8n instead of code, section 10 has two step-by-step recipes.
1.5 Trying the API safely
The API has no sandbox, no test mode and no test keys. Every key starts with fl_live_, every call acts on your real account, and nothing is simulated: an invitation to a published assessment can send a real email, a project made through the API creates a real job post that can become public after review, and whatever costs something in production costs the same when you are only testing. These habits keep a test away from real candidates and real data:
- Test in a separate company account. A key reaches only the organization that owns it (section 2.9), and the limits of section 8.5 are counted for each organization and each key. A second company account that you make for testing therefore keeps its candidates, projects, keys and counts apart from your production account. Create the key for it in that account (section 2.2).
- Invite only addresses you control, such as your own (section 1.4). An invitation to a published assessment can send a real email to the address (sending and delivery are not guaranteed: section 1.4).
- To watch an invitation without an email going out, schedule it far ahead and cancel it. For an AI interview or a role-play,
send_ataccepts a time from 2 minutes to 90 days from now (section 4.9). An invitation scheduled 60 days ahead and cancelled (section 4.9.1) before it is due is not emailed. The scheduled invite itself creates or uses a candidate record and, when a project is named, can add the candidate to its board: check the entry of each candidate inresults(section 4.5). Creating a real invitation (not a practice invitation) can also produce aninvitation.lifecycle.updateevent with the statusinvitation_created(see the webhook guide), and, for a candidate who already has a Flowmingo account, Flowmingo also attempts an in-app notice at once (section 4.9.2). Neither effect waits forsend_at. Use addresses you control here too. - Pause the reminders of a test invitation that you do send (section 4.10), so that the candidate is not chased: Reminder 1 would be due 2 days after the invitation email went out and Reminder 2 two days after that. The pause never holds the invitation email back, so you can call it right after the invite call. Leave it paused: if you or a teammate resumes it after a reminder's due day, that reminder is skipped, and if before, it goes (section 4.10.1). It works for an AI interview or a role-play invitation; an invitation to another kind of assessment is refused with
409(section 4.10). - Clean up what a test makes. A project made through the API creates a real job post that can become publicly available after the automatic content check (section 3.5.2). If you do not want it public, unpublish it (section 3.7.3) or archive the project (section 3.7.5) promptly. Deleting a project through the API needs a person to approve it (section 9.7.2). Assessments made with the AI use your generation budget (section 3.6.11), and a Skill Test, a Technical Test or a Showcase that your organization has set up as sponsored can use your organization's wallet when a candidate takes it (sections 4.6.7 and 4.8.3).
2. Request an API key
2.1 Open the API Keys panel
- Sign in at
https://team.flowmingo.ai. - Click your email at the bottom-left of the sidebar, then Settings.
- In the Settings window open Integrations -> API Keys.
Only Owners and Admins can create, edit or delete API keys. A Member cannot.
2.2 Create a key
- Click Create New Key.
- In the Create New API Key window, type a Name. The name is required. The Description is optional.
- Tick the permissions (scopes) the key needs. Tick at least one: see section 2.3.
- Click Create secret key.
You will receive a one-time secret with the format fl_live_<prefix>.<secret>, for example fl_live_abcd12345.n8G3zPNw9gWkK1Kj. Copy and store it immediately: Flowmingo keeps only a hash, and cannot show it again. After creation the key is masked in the list (for example fl_live_abcd12345.****).
A key acts with the permissions of the person who creates it, not with a company-wide login. Read section 2.9 before you create a key for a shared integration.
An organization can hold at most 50 active keys. Turned-off and deleted keys do not count. Creating a 51st, or turning a turned-off key back on when 50 are active, answers 409 with the code api_key_limit_reached: turn off or delete a key you no longer use, or write to contact@flowmingo.ai (section 8.5).
2.3 Choose scopes
A scope is a permission. A route that needs a scope answers 403 (API key missing required scope: <scope>) when the key does not carry it. There are eleven:
| Scope | Label in the dashboard | What it opens |
|---|---|---|
read_hiring |
Read hiring data | The read routes for job posts (including whether a job post is open to the public, section 3.7.1), assessments, projects (including one project by its id, section 3.5.13) and stages, applications, upcoming invitations, results with their AI evaluations, and the overview counts (section 5), and POST /data/ask/v1, which answers a question about your hiring data in plain words (section 5.11), and the events feed (section 7), and, together with read_candidates, the list of your tracking links (section 3.8.3). Candidate names and email addresses are left out unless the key also has read_candidates. With read_candidates as well, it opens the transcript and the recording links of one interview (section 5.10). |
read_candidates |
Read candidates | The candidate list and single-candidate routes (name, email and CV link), and the names and email addresses in results, applications and events. Together with read_hiring, the transcript and the recording links of one interview (section 5.10) and the list of your tracking links (section 3.8.3); together with manage_tracking_links, the sources a link can credit (section 3.8.2). |
write_pipeline |
Manage pipeline | The seven routes that change your pipeline and reviews: add an application, move a stage, reject, add a note, rate, shortlist, add to your collection (section 6). Also the approval routes in section 9.7: a reject, a removal of candidates from a hiring project (section 9.7.1), and normally a move to a stage that emails the candidate, wait for a person to approve; any other move happens at once; one route takes a change back, but not a removal. |
invite_candidates |
Invite candidates | POST /interview/candidate/invite/v1 (section 4), which can also schedule when an AI interview or role-play invitation is sent, POST /interview/candidate/invites/{id}/cancel/v1, which cancels a scheduled one (section 4.9), POST /interview/candidate/invites/{id}/stop-reminders/v1, which pauses the automatic reminders of one AI interview or role-play invitation, and POST /interview/candidate/invites/{id}/resume-reminders/v1, which lets them go again (section 4.10). Also POST /approvals/invite/v1, which stages an invitation for a person to approve (section 9.7). |
create_set |
Create interview sets | POST /interview/set/v1 (section 3). It works only while the creator of the key is an Owner or an Admin (during the grace period after they leave, the role they left with normally counts: sections 2.9 and 2.10). |
create_project |
Create hiring projects | POST /hiring/projects/v1 (section 3.5). The same call can make the project's assessments, which needs no other scope. Also POST /hiring/projects/{id}/update/v1, which edits a project's name, role and description and the words of its job post (section 3.5.14; its answer shows the project only to a key that also holds read_hiring), the four routes that publish, unpublish and boost a job post and open or close a hiring project (section 3.7), and POST /approvals/delete-project/v1, which stages the deletion of a hiring project for an Owner or an Admin to approve (section 9.7.2). It works only while the creator of the key is an Owner or an Admin (section 2.9). |
create_assessment |
Create assessments | The six POST /assessments/<type>/v1 routes, which make one assessment each with the AI, and GET /assessments/requests/{idempotency_key}/v1 (section 3.6). Opt-in: a key does not gain it automatically, it is never implied by create_set or create_project, and each assessment it makes costs AI generation. It works only while the creator of the key is an Owner or an Admin (section 2.9). |
submit_recordings |
Submit interview recordings | The two routes that hand over recordings of interviews you ran yourself to be transcribed and scored: POST /interview/recording/uploads/v1 and POST /interview/recording/v1 (section 4.7). Opt-in: a key does not gain it automatically, and it is never implied by invite_candidates or create_set. Evaluating recordings is part of the Scale plan and above. |
manage_tracking_links |
Manage tracking links | The routes that make, change, switch off and delete a tracking link (a custom share link of Settings) and GET /tracking-links/sources/v1, which lists the sources a link can credit (section 3.8). Opt-in: a key does not gain it automatically. Making a link needs the Growth plan. It works only while the creator of the key is an Owner or an Admin (section 2.9). Listing the links needs read_hiring and read_candidates, and listing the sources needs read_candidates as well: the text people typed into a link and the names of sources can name an applicant or a referrer. |
read_sequences |
Read email sequences | The four routes that read your email sequences: the library, one sequence, the numbers of one sequence and what switching one on would do (section 5.12). Opt-in: a key does not gain it automatically. It reads only: nothing in the API creates or changes a sequence or switches one on or off. No route needs read_candidates. |
read_integrity |
Read integrity signals | GET /hiring/submissions/{id}/integrity/v1: counts of what a candidate's browser, camera and microphone recorded during one finished Skill Test or Technical Test, with what was being watched (section 5.13). Observations only: Flowmingo does not judge them. Opt-in: a key does not gain it automatically. It also needs read_hiring and read_candidates, and a key whose creator is currently an Owner or an Admin. |
GET /me/v1 needs no scope. A key with no scope can call GET /me/v1 and is refused (403) by every route that reads or changes your data, except that it can still read and withdraw the approval requests it made earlier (section 9.7). A key made with nothing ticked looks valid and does nothing new. The dashboard lets you create one, so tick at least one scope.
Combinations that people use:
- Reporting into a spreadsheet or BI tool:
read_hiring, plusread_candidatesif the report needs names. Reading the transcript or the recording links of an interview needs both (section 5.10). - Starting assessments from a form or an ATS:
invite_candidatesalone. - Syncing stages with an ATS:
read_hiringandwrite_pipeline. - Opening a role from your own system:
create_projectto create the hiring project (the same call can make its assessments, section 3.5.5), to edit it and the words of its job post (section 3.5.14) and to publish or boost its job post (section 3.7),create_assessmentto make further assessments with the AI (section 3.6) orcreate_setto build one from your own questions (section 3.1), andinvite_candidatesto invite into it. - Handing over recordings of interviews you ran yourself:
submit_recordingsfor the two recording routes,create_setto make the recording evaluation they are scored against (section 4.7.1), andread_hiringto read the scores. - Managing tracking links from your own system:
manage_tracking_linksto make, change, switch off and delete links (making one needs the Growth plan),read_hiringandread_candidatesto list them, andread_candidatesto list the sources (section 3.8). - Reading how your email sequences are doing:
read_sequencesalone (section 5.12). - Reading what was recorded during a Skill Test or a Technical Test:
read_integritywithread_hiringandread_candidates(section 5.13). - An AI assistant (MCP):
read_hiringandread_candidates, pluswrite_pipelineandinvite_candidatesif it may act. Addcreate_projectandcreate_assessmentonly if you want it to create projects and assessments and to publish, close or boost a job post: they are opt-in at the MCP sign-in (a client must ask for them by name), the tools act at once (exceptdelete_project, which only asks a person to approve deleting a project, section 9.7.2), and the key's creator must be an Owner or an Admin. The assistant does not usecreate_setorsubmit_recordings.
Candidate names and email addresses require
read_candidates.read_hiringalone returns ids, scores and evaluations. Withoutread_candidates,candidate_name,firstname,lastnameandcandidate_emailandsource_detailare removed from events.GET /me/v1shows what your key holds.
2.4 Edit, rotate or revoke
Use Edit on a key to change its name, description, scopes and status (Active or Disabled). The secret itself cannot be changed. A change to scopes or status applies from the next request made with the key.
Taking access away is always allowed: remove a scope, turn the key off, rename it. Giving a key more access (adding a scope, or turning a disabled key back on) is allowed only to the person who created it or to an Owner, and only while that creator is still in the company and is an Owner or an Admin. If the creator has left, take the key over first (section 2.10).
There is no rotate action. To rotate a key, create a new one, switch your integration to it, then delete the old one. An organization that already has 50 active keys cannot create the new key first (section 2.2): turn off or delete a key you no longer use to make room. Deleting a key revokes it from the next request made with it. A deleted key answers 401 Invalid API key, and so does a disabled one, except a key turned off because its creator left, which answers 403 with the code key_creator_left (section 2.10).
Keys created in the dashboard do not expire (expires_at is null). Revoke a key you no longer need by deleting it.
If the person who created a key leaves the company, the key normally stops after seven days unless an Owner or an Admin takes it over (at once, if no other Owner is left in the company). See section 2.10.
2.5 Security notes
Store API keys in a secret manager (AWS Secrets Manager, GCP Secret Manager, 1Password, etc.). Never embed keys in frontend code, logs, or version control. Always send the key in a header, never in a URL or in the request body: URLs end up in logs and browser history.
A key acts with the permissions of the person who created it (section 2.9). If that person is limited to some projects, the key has the same limit: the projects they can open and the interview sets they created. A key made by an Owner, or by someone with access to all hiring projects, reaches all of your organization's hiring data, so create it only for an integration you trust with that. Notes, ratings, shortlists and stage moves made with a key are recorded under the name of the person who created it. Sending a Skill Test, a Technical Test or a Showcase goes out on behalf of that person too and uses the hiring projects they can open (section 4.6.2). Publishing and boosting a job post act as that person too (section 3.7).
2.6 Authenticate your requests
Send the key in the X-API-Key header:
X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj
The Authorization header carries the same key too, in these forms, for tools that cannot set a custom header:
Authorization: ApiKey fl_live_abcd12345.n8G3zPNw9gWkK1KjAuthorization: Bearer fl_live_abcd12345.n8G3zPNw9gWkK1Kj. This is accepted when the value starts withfl_, which every Flowmingo key does. Any other bearer value, such as a session token, is not an API key and answers401 API key is required.Authorization: Basic <base64>, with the key as the user name and an empty password. Some applicant-tracking tools can send a key only this way. It is read as a key only when the user name starts withfl_.
A tool that only offers "Bearer token" authentication therefore works without a custom header.
2.7 Check a key
To check a key without side effects, call the introspection endpoint. It needs no scope, so any valid key can verify itself:
curl -s -H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
https://apis.flowmingo.ai/company/integration/me/v1
{
"api_key_id": "0b0a9f3e-6c2d-4c1a-9a47-5d3e8b7f2a10",
"organization_id": 4217,
"scopes": ["read_hiring", "read_candidates", "write_pipeline", "invite_candidates", "create_set", "create_project", "create_assessment", "submit_recordings"],
"expires_at": null,
"features": { "ask_data": true }
}
A 200 proves the key is accepted and shows which organization and scopes it carries. features says which optional abilities the key has right now: ask_data is true only when your company has asking questions of your hiring data switched on and the key carries read_hiring (section 5.11). It is a hint for your client, not a permission: each route checks the key again on every call. It does not list the projects the key can open: that is its creator's list (see section 2.9). A key whose creator left and whose grace period is over (section 2.10), whose creator can open no hiring project, or that has no creator on record answers 403 with the reason instead of 200. A 503 means the permissions of the key could not be checked just now: send the request again. Anything else: see section 11, Troubleshooting.
2.8 When a key was last used
The Last used column in the key list shows when the key last made a request. Every request that presents a valid key counts, whatever it does: reads, writes, the events feed, and calls made through an AI assistant connected with that key. A request refused because the key is unknown, disabled, deleted or expired does not count. The time is brought up to date about once an hour, so a key you used a minute ago can still read "Never" for a while.
2.9 Whose permissions a key carries
A key does what the person who created it can do, and no more.
- Reach. If the creator has access to all hiring projects (an Owner always does), so has the key, and it reaches the whole organization. If the creator is limited to some projects, the key has that limit: the projects they can open and the interview sets they created. An Admin who was given projects one by one is limited to that list, even if it names every project today. Lists, counts and averages leave out what the key cannot reach.
- Their own interview sets. For an interview set the creator made, the key can read its results and its candidates, add notes, ratings and shortlist marks to those results, add those candidates to the talent collection, and file recordings into a recording evaluation they made. Ownership alone does not authorise an invite: a limited key can invite only into a set that has a live, switched-on link to one of the creator's projects.
- Their role, for creating things. In the dashboard only an Owner or an Admin can create an interview set, a hiring project or an assessment, and a key follows that rule on the three scopes that do these:
create_set,create_projectandcreate_assessment. If its creator has been made a Member, a request that needs one of them is refused with a403before anything is created (section 11). Forcreate_setthe role they left with normally counts during the grace period after they leave (section 2.10). Nothing else about the key changes: its other scopes still work, inside the creator's projects. The other five scopes (read_hiring,read_candidates,write_pipeline,invite_candidatesandsubmit_recordings) have no rule of this kind, because a Member can do the same things in the dashboard. The three opt-in scopes for tracking links, email sequences and integrity signals differ:manage_tracking_linkshas a rule of its own, that its creator must be an Owner or an Admin (section 3.8),read_sequenceshas none, except that its activation preview is answered only for an Owner or an Admin (section 5.12.4), andread_integrityneeds a key whose creator is currently an Owner or an Admin (section 5.13). - It follows its creator. If the creator's access changes later, the key's reach changes with it. You do not need to issue a new key.
- The key's own settings only narrow. A scope you leave off a key (for example
read_candidates, so no candidate names) takes something away. No setting gives a key more than its creator has.
The response code tells you what was refused: 403 with the code key_out_of_reach for something the key cannot reach, or 404 where the record is not visible to the key. Each route's section states the errors it can answer; branch on the code, not on the status alone. The events feed (section 7) needs a key whose creator has access to all hiring projects.
2.10 When the person who created a key leaves
A key carries its creator's permissions and nothing of its own, so it cannot outlive its creator's access. After the creator leaves, the key keeps working for a grace period: seven days, or until the date chosen when the member was removed with a delayed deletion (see below). So that your integration does not stop without warning:
- Unless item 6 applies, every Owner and every Admin of the company gets an email that names the keys, who created them and the date they stop. The person who removed the member, when someone did, is not emailed.
- During the grace period the key normally keeps working with the access the creator had on the day they left. Several things differ. Creating a hiring project or an assessment stops at once, because those routes ask for the person's role again and a person who has left holds none (sections 3.5 and 3.6.1); creating an interview set carries on with the role they left with (section 3.1). Sending a Skill Test or a Technical Test (section 4.6) also stops at once and answers
404Project ... not found, because those two go out as the person and read their access today. Editing the words of a job post (section 3.5.14) and pausing or resuming an invitation's reminders (section 4.10) read the person's access today too: a body with ajob_postpart, even an empty{}, answers403with the codekey_out_of_reachbefore anything is written, whatever role the creator left with (the same key can still edit the project's own fields in a body without one), and the stop-reminders and resume-reminders calls answer404with the codeinvitation_not_foundfor every invitation. The tracking-link routes (section 3.8), the email-sequence routes (section 5.12) and the integrity route (section 5.13) refuse such a key during the grace period too, normally with a403, and the code is normallykey_creator_lefton the tracking-link routes andkey_creator_in_graceon the email-sequence routes. And a key whose creator could open no hiring project when they left has nothing to carry on with, so it stops at once, and the email lists it as already stopped. - An Owner or an Admin can take the key over in Settings -> Integrations -> API Keys: the key shows a "Take over" button. From then on the key belongs to that person and works with their access (an Admin's access is the Admin's own projects, so the key can become narrower). Its scopes stay as they are.
- When you take a key over you can ask for a new secret. The person who left may still hold the old one, so a new secret is the safer choice. If you ask for one, the old secret stops working when the take-over is saved, and you see the new one once. Anything that used the old secret, including an AI assistant connected through MCP, must be given the new one or connected again. If you do not ask for one, the old secret keeps working, now with your access.
- If nobody takes the key over, it normally stops when the grace period ends (a creator who is re-invited is the exception). Every request then answers
403with the codekey_creator_left, except for a key whose delayed deletion has run: that key is deleted and answers401like any deleted key, and cannot be taken over. - If no other Owner is left in the company when the creator leaves (Admins do not count for this), the key gets no grace period: it is turned off at once, and nobody is emailed, even if Admins remain. Requests answer
403with the codekey_creator_leftand a message that says the key was turned off, as long as the secret is right (a wrong secret still gets401 Invalid API key). An Owner or an Admin can take the key over and turn it back on in Settings, or you can create a new key.
Only an Owner or an Admin can take a key over. Taking over a key whose creator is still in the company is normally refused with 409: ask them to hand it over, or create a new key. A key that has already stopped can always be taken over. A creator who is still in the company but can open no hiring project (for example an Admin whose project access was taken away) has a key that has stopped working (the 403 about hiring projects in section 11). Settings shows that key as stopped, and an Owner or an Admin can take it over to bring it back.
Removing someone in the dashboard. When an Owner or an Admin removes a team member who has keys, the Remove screen asks what to do with them: hand them to an Owner or an Admin and keep the secrets, hand them to the person who is removing the member and issue new secrets, or delete them (at once, or after 7, 14 or 30 days). The keys keep working until the chosen date. They are then deleted unless someone takes them over, the person has come back, or a key was turned off in the meantime (delete such a key in Settings); where no other Owner is left, a delayed deletion becomes an immediate stop and the keys are not deleted (deleting at once is not affected). Settings -> Integrations -> API Keys shows what became of each key, and you can delete a key there. With no choice, the keys follow the normal grace period above. This is a screen of the dashboard, not a route of this API.
A key you gave to an applicant-tracking tool. A key you paste into a tool such as Recruitee or Zoho Recruit follows the same rules, so create it as an Owner. The tool's list of tests and its sends follow the creator's permissions (section 2.9). When the creator leaves and the grace period runs out, the tool's calls to Flowmingo start failing with the 403 in section 11, until an Owner or an Admin takes the key over or you give the tool a new key. A key that was deleted answers 401 and cannot be taken over: create a new key and update the tool.
3. Create assessments and hiring projects through the API
There are three ways to create things with the API:
| You want | Use | Scope | Section |
|---|---|---|---|
| An assessment that the AI writes from a job title: an AI Interview, a CV Evaluation, a Skill Test, a Showcase, a Technical Test or a Role-play | POST /assessments/<type>/v1, one route for each kind |
create_assessment |
3.6 |
| A hiring project with its job post, and optionally its first assessments in the same call | POST /hiring/projects/v1 |
create_project |
3.5 |
| An interview or CV set with questions and criteria that you write yourself | POST /interview/set/v1 |
create_set |
3.1 to 3.4 |
Sections 3.1 to 3.4 describe the third: use POST /interview/set/v1 to programmatically provision an interview set (interview or CV set) along with its questions, requirements, and access list. To put the set in a new hiring project, create the project first (section 3.5).
Creating is not the same as opening the role to candidates. Once a project and its job post exist, section 3.7 shows how to publish and unpublish the job post, boost it, and open or close the project. Section 3.8 shows how to make tracking links for a job post, a set of job posts or your careers page, to see where applications come from.
3.1 Endpoint & environments
| Environment | Settings URL | API |
|---|---|---|
| Production | https://team.flowmingo.ai |
https://apis.flowmingo.ai/company/integration/interview/set/v1 |
Method: POST. Required API key scope: create_set.
The key acts with its creator's permissions (section 2.9). It can create interview sets only while its creator is an Owner or an Admin, the same people who can create a set in the dashboard. During the grace period after the creator left (section 2.10), the role they left with normally counts. If the creator has since become a Member, the request is refused with a 403 and nothing is created (see section 11).
3.2 Request payload
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | Yes | Max 1024 chars. Display title for the interview set. |
position |
string | Optional | Max 1024 chars. The job title / role being hired for. |
set_type |
int | Optional | 1 = AI interview (default), 2 = CV evaluation. Must be a JSON number. The endpoint also accepts 3, 4, 5, 7, 8 and 10, but this body only writes the title, questions, criteria and access list: the papers, tasks and problems those kinds need are built in the dashboard, so create them there. The exception is 8, a recording evaluation: its criteria guide how the recordings you hand over through the API are scored, so you can make it here (section 4.7.1). |
question_mode |
string | Required for an AI interview (set_type 1) unless iai_questions holds an interview question |
How the AI interview gets its questions: realtime (the product's "Let AI decide during interview") or predefined ("I'll set the questions"). See "Choose how an AI interview gets its questions" below. Any other set_type ignores it, whatever it holds. |
description |
string | Optional | Free-text description. |
interview_duration |
int | Optional | Duration in minutes. The longest allowed depends on your plan: 20 on Free, 30 on Starter, 60 on Growth and Scale, and no plan limit on Enterprise. A longer value is refused with a 400 ("Your plan allows up to N minutes"). Ignored for CV sets. |
cfg_lingual_ids |
int[] | Optional | Languages the interview may be conducted in. 290 = English, 204 = Hindi (India); see 3.2.4 below. Omit it and English is applied. |
number_of_retakes |
int | Optional | Number of allowed retakes for candidates. Ignored for CV sets. |
priority |
int | Optional | A whole number stored on the assessment as its priority. The assessment lists of this API still order by creation time, newest first. |
project_id |
UUID | Optional | If provided, the interview set is attached to this project. It must be an active hiring project of your organization, otherwise the request is refused with a 404 and nothing is created. Find its id as in section 5.7. For a key whose creator is limited to some projects it must also be one of theirs (see 2.9). Any project outside theirs, another organization's included, is refused with a 403 before the project is looked up. A project of theirs that is archived still answers 404. A project that has been deleted is no longer theirs, so it answers 403 (a creator who can open every project gets 404 for it). |
iai_questions |
array | Required for a predefined AI interview; optional otherwise |
Interview questions (see 3.2.1). With question_mode predefined an AI interview needs at least one entry whose question_mode is interview (the default): a request with none, or with only pre_screening entries, is refused with a 400 and the code interview_questions_required, and nothing is created. With question_mode realtime you may send none and Flowmingo prepares them. Not used by a CV evaluation. Their order is the order of the array: they are numbered 1, 2, 3 and any priority you send is ignored. An entry with neither a title nor a content is skipped, and does not count as a question. |
iai_requirements |
array | Optional | Evaluation requirements, the criteria the candidate is evaluated against (see 3.2.2). Their order is the order of the array: they are numbered 1, 2, 3 and any priority you send is ignored. An entry with neither a title nor a content is skipped. |
com_accesses |
array | Optional | Candidates who may open the assessment when it is restricted to listed candidates (see 3.2.3). |
external_id |
string or null |
Optional | Your own id for the assessment: plain text, 1 to 128 characters, stored exactly as you send it (section 3.2.5). |
idempotency_key |
string or null |
Optional | A token you choose, 8 to 128 characters from A-Z a-z 0-9 . _ : -, that makes a retry safe (section 3.2.6). Without it, sending the same request twice makes two assessments. |
Choose how an AI interview gets its questions. POST /interview/set/v1 creates the assessment already live, so an AI interview (set_type 1, the default) must say who decides what the AI interviewer asks. Send question_mode:
realtime, the product's "Let AI decide during interview": the AI chooses the interview questions for you. Send no interview question and Flowmingo prepares them from the role, the description and the criteria (iai_requirements, the first 20 are used) you send, and saves them with the assessment. The request then takes longer, typically 10 to 30 seconds, so give it a generous timeout. If you do send interview questions of your own they are kept, and no more are prepared. Yourpre_screeningquestions are kept (Flowmingo only numbers them, in the order you send them).predefined, the product's "I'll set the questions": the AI interviewer asks the interview questions you send iniai_questions, in your order, as many as the interview length allows. Send at least one entry whosequestion_modeisinterview(the default).
question_mode is required whenever the request holds no interview question. A request that names no question_mode but does hold an interview question is predefined, so a request that already works keeps working. A request with neither is refused:
{ "code": "question_mode_required", "message": "Choose how this AI interview gets its questions. Send \"question_mode\": \"realtime\" (Let AI decide during interview: Flowmingo prepares the interview questions from the role and the criteria) or \"question_mode\": \"predefined\" (I'll set the questions: send at least one interview question in \"iai_questions\", whose \"question_mode\" is \"interview\")." }
A predefined request with no interview question is refused with the code interview_questions_required:
{ "code": "interview_questions_required", "message": "An AI interview needs at least one interview question, and this request has none, so there would be nothing for the AI interviewer to ask. Add at least one interview question (an iai_questions entry whose question_mode is interview) and send the request again." }
Both are a 400 and nothing is created. If Flowmingo cannot prepare the questions for a realtime request just now, the answer is a 503 with the code question_generation_failed, nothing is created, and you can send the same request again (or switch to predefined and send your own interview questions). Pre-screening questions (question_mode pre_screening inside iai_questions) are asked on the candidate's form, not by the interviewer, so they never count as interview questions. The rule applies only to AI interviews: a CV evaluation (set_type 2) and the other kinds need no question_mode and ignore the field whatever it holds. For an AI interview, any value other than realtime or predefined is a 400 that lists the two that are allowed (null counts as not sent). AI interviews you created earlier are not affected.
Numbers must be JSON numbers. set_type, interview_duration, number_of_retakes and priority are refused with a 400 when they arrive as text, even numeric text: "interview_duration": "30" answers interview_duration must be a whole number (a JSON number, not text). Send 30, not "30".
3.2.1 iai_questions[] item
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | Optional | Max 5000 chars. |
content |
string | Optional | Max 5000 chars. The question prompt. |
hint |
string | Optional | Max 2000 chars. |
details |
string | Optional | Max 10000 chars. Long-form context / instructions. |
question_mode |
string | Optional | One of pre_screening, interview. Defaults to interview. Mapped server-side to cfg_question_mode_id. |
question_type |
string | Pre-screening only | One of text, checkbox, radio, file; defaults to text. Sets cfg_question_type_id on a pre_screening question. Ignored on an interview question — the AI interviewer asks it aloud, it has no input type, and a type is exactly what turns a question into a pre-screening card on the candidate's screen. |
priority |
int | Optional | Ignored. The questions are numbered 1, 2, 3 in the order of the array. |
options |
array | Optional | Choice options: { id, title, titles, content, contents, priority }. |
attributes |
object | Optional | Arbitrary metadata. |
external_id |
string or null |
Optional | Your own id for this question: plain text, 1 to 128 characters (section 3.2.5). It comes back in results.iai_questions (section 3.4). |
3.2.2 iai_requirements[] item
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | Optional | Max 5000 chars. |
content |
string | Optional | Max 5000 chars. Requirement description. |
hint |
string | Optional | Max 2000 chars. |
cfg_importance_id |
int | Optional | Importance level — see below. |
tal_skill_ids |
int[] | Optional | Skill IDs linked to this requirement. |
priority |
int | Optional | Ignored. The criteria are numbered 1, 2, 3 in the order of the array. |
attributes |
object | Optional | Arbitrary metadata. |
external_id |
string or null |
Optional | Your own id for this criterion: plain text, 1 to 128 characters (section 3.2.5). It comes back in results.iai_requirements (section 3.4). |
cfg_importance_id values. These are configured IDs, not a 1-4 scale, and the lowest ID is the
weakest importance. Importance sets how heavily the requirement counts toward the candidate's
evaluation score. The ID selects the importance, and the importance carries the weight:
cfg_importance_id |
Importance | Weight in the evaluation score |
|---|---|---|
244 |
Good To Have | 1x |
245 |
Important | 2x |
272 |
Very Important | 3x |
273 |
Must Have | 4x |
If omitted, or if the value is not one of the four above, the requirement is stored as 244
(Good To Have).
3.2.3 com_accesses[] item
A new assessment is open to anyone who has its link, so this list only matters once the assessment is restricted to listed candidates. It is a list of candidates, not of your teammates, and it does not change which teammates can see the assessment. Duplicate addresses are stored once.
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | Yes | The candidate's email address. It must be a valid address, max 250 chars. |
fullname |
string | Optional | Max 500 chars. The name shown beside the address. It defaults to the address. |
firstname, lastname |
string | Optional | Max 250 chars each. Accepted, but not stored. |
priority, attributes |
number, object | Optional | Accepted, but not stored. |
3.2.4 cfg_lingual_ids values
Like cfg_importance_id, these are configured IDs rather than a sequence, so the two you are most
likely to need are listed here:
cfg_lingual_ids |
Language |
|---|---|
290 |
English |
204 |
Hindi (India) |
Send them as an array of integers, e.g. [290] or [290, 204]. The array is the set of languages a
candidate is allowed to conduct the interview in.
If you omit the field, English is applied for you. That default is best-effort, so send [290]
explicitly if your integration depends on the language being set deterministically. CV sets
(set_type: 2) have no interview, so the field is ignored for them.
Need an ID for a language that is not listed? Ask us rather than guessing — a value that is not a real configuration ID is stored as-is and then resolves to nothing.
Evaluation report language is a separate setting that this endpoint's documented contract does not cover, so do not rely on setting it here. If none is ever stored, reports come out in English. Once a language has been stored for a set it is fixed — a later attempt to change it is ignored rather than rejected, because reports already written are never regenerated.
3.2.5 Your own ids on the assessment, its questions and its criteria
external_id can be sent on the body (the assessment), on each entry of iai_questions and on each entry of iai_requirements. The rules are those of section 8.8: optional, plain text of 1 to 128 characters, stored exactly as you send it, scoped to your company, not unique, and fixed when the assessment is created. A value that breaks them is a 400 with no code and the message "external_id must be plain text of 1 to 128 characters with no control characters. It is stored exactly as sent." Nothing is created.
The answer returns them (section 3.4): results.external_id for the assessment, and results.iai_questions and results.iai_requirements for the questions and criteria. GET /hiring/interview-sets/v1 returns the assessment's external_id on every row, and GET /hiring/interview-sets/v1?external_id= finds it. No other route reads a question or a criterion back. A copy of the assessment made in the dashboard starts without ids. Never pair the returned rows with your input by their position in the list: on the call that makes the assessment they come in the order you sent, but the repeat of a keyed request lists the questions and criteria as they are now (section 3.2.6). Matching by external_id works only for the entries whose ids you supplied and that are distinct in the list. An entry you gave no id, an entry whose id is also on another entry (ids are not unique) and a question Flowmingo prepared for a realtime interview (it was never in your input) cannot be matched that way. If you need to match them, give every question and criterion its own distinct external_id.
When you send an idempotency_key (section 3.2.6), the external_id is part of the request: the same key with a different external_id is a 409.
3.2.6 Retrying safely: idempotency_key
Without a key this route has no protection against a duplicate: sending the same request twice makes two assessments. Send an idempotency_key to make a retry safe. Here it is optional (the project route and the six assessment routes require one). It is a token you choose, 8 to 128 characters from A-Z a-z 0-9 . _ : -, for example the id of the requisition in your own system plus a suffix; anything else is a 400 with the message "idempotency_key must be 8 to 128 characters from A-Z a-z 0-9 . _ : -". It belongs to your organization, whichever of your API keys sends it, and it is remembered for good.
| Situation | Answer |
|---|---|
| The first call with the key | 201, as always, and idempotent_replay false in the body. |
| The same key with the same request, after the first call made the assessment | 200 and idempotent_replay true: the assessment the first call made, read now. Nothing is created, the AI is not asked (not even for a realtime interview), and the assessment is attached to the project_id of the body if the first call could not attach it. |
| The same key while the first call is still working: two identical requests at the same moment, or a repeat that arrives early | 409 idempotency_request_in_progress, with a Retry-After header of 10 seconds. One assessment and one AI call are made. Send the same request again after the wait and you get the assessment. |
| The same key with a different request | 409 idempotency_key_reused. |
| The same request from a key whose creator cannot open that assessment (see below) | 409 idempotency_key_reused, with the same message, which reveals nothing about the assessment. |
| The assessment the key made has been deleted | 409 interview_set_was_deleted, for good: a deleted assessment's key never makes another. Use a new key. |
An idempotency_key together with addons |
400 idempotency_key_not_supported_with_addons, before anything is reserved. Send the request without one of them. |
- "The same request" is the body exactly as you sent it, less the key. The order of the keys does not matter; any other difference counts (the
external_id, theproject_id, one word of a question). A repeat is compared with the body of the first call, not with the assessment as it looks now. - Who can get the assessment back. A repeat is answered only if the key's creator has the same reach over the assessment as
GET /hiring/interview-sets/v1gives: they can open every project (an Owner, or an Admin with access to all), or the assessment is linked to a project they can open, or they made it themselves. An assessment made by a request that carries anidempotency_keycounts as made by the person who created the API key, so a repeat from the same API key is not refused for this reason. An assessment made withoutproject_idis visible to the keys of its maker and to no one else who is limited to some projects. - A request that fails before the assessment is saved (a
400, a plan limit, a503question_generation_failed, an unexpected error) does not use its key up: send it again at once. One that is refused before the key is reserved (a403for a project outside the creator's reach, a404for a project that does not exist) never touches the key. - A call that is cut off (a restart, for example) keeps its key busy: identical repeats get
409idempotency_request_in_progressuntil the reservation is ten minutes old, and then the next identical request takes it over and makes the assessment. A different body under that key is409idempotency_key_reusedfor good. - A repeat returns the assessment as it is now, with any edit made in the dashboard, not as it was first made. It does not repeat the steps after the save, which is why
addonsis refused with a key. The answer has the shape of section 3.4. That includes the listsresults.iai_questionsandresults.iai_requirements: on the call that makes the assessment they come in the order you sent them, and on a repeat they are the questions and criteria the assessment holds now, in their current order, so someone who has added, removed or reordered some in the dashboard changes what the repeat lists. Never pair a returned row with your input by its position. Matching byexternal_idworks only for the entries whose ids you supplied and that are distinct in the list: an entry with noexternal_id, an entry whoseexternal_idis also on another entry, and a question Flowmingo prepared for arealtimeinterview cannot be matched that way. Give every question and criterion its own distinctexternal_idif you need to match them. - The key lives in a list of its own. The same token can also be used on the six assessment routes, and
GET /assessments/requests/{idempotency_key}/v1does not see a key made here: it answers404assessment_request_not_found. - The key is not the
external_id. The key protects a retry and is remembered for good; the id tags the record and can repeat (section 8.8).
3.3 Sample request
curl -X POST https://apis.flowmingo.ai/company/integration/interview/set/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"title": "Senior Backend Engineer - Async Interview",
"set_type": 1,
"question_mode": "predefined",
"description": "Async interview for senior backend candidates.",
"interview_duration": 20,
"number_of_retakes": 1,
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"iai_questions": [
{
"title": "System design",
"content": "Walk us through how you would design a rate limiter.",
"question_mode": "interview",
"question_type": "text",
"priority": 1
}
],
"iai_requirements": [
{
"title": "5+ years backend experience",
"cfg_importance_id": 273,
"priority": 1
}
],
"com_accesses": [
{ "email": "candidate@example.com", "fullname": "Jane Doe" }
]
}'
import fetch from 'node-fetch'
const response = await fetch('https://apis.flowmingo.ai/company/integration/interview/set/v1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'fl_live_abcd12345.n8G3zPNw9gWkK1Kj'
},
body: JSON.stringify({
title: 'Senior Backend Engineer - Async Interview',
set_type: 1,
question_mode: 'predefined',
interview_duration: 20,
number_of_retakes: 1,
project_id: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
iai_questions: [
{
title: 'System design',
content: 'Walk us through how you would design a rate limiter.',
question_mode: 'interview',
question_type: 'text',
priority: 1
}
],
iai_requirements: [{ title: '5+ years backend experience', cfg_importance_id: 273, priority: 1 }],
com_accesses: [{ email: 'candidate@example.com', fullname: 'Jane Doe' }]
})
})
const data = await response.json()
console.log(data)
import requests
response = requests.post(
"https://apis.flowmingo.ai/company/integration/interview/set/v1",
headers={
"Content-Type": "application/json",
"X-API-Key": "fl_live_abcd12345.n8G3zPNw9gWkK1Kj",
},
json={
"title": "Senior Backend Engineer - Async Interview",
"set_type": 1,
"question_mode": "predefined",
"interview_duration": 20,
"number_of_retakes": 1,
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"iai_questions": [
{
"title": "System design",
"content": "Walk us through how you would design a rate limiter.",
"question_mode": "interview",
"question_type": "text",
"priority": 1,
}
],
"iai_requirements": [
{"title": "5+ years backend experience", "cfg_importance_id": 273, "priority": 1}
],
"com_accesses": [{"email": "candidate@example.com", "fullname": "Jane Doe"}],
},
)
print(response.json())
<?php
$payload = [
'title' => 'Senior Backend Engineer - Async Interview',
'set_type' => 1,
'question_mode' => 'predefined',
'interview_duration' => 20,
'number_of_retakes' => 1,
'project_id' => 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
'iai_questions' => [
[
'title' => 'System design',
'content' => 'Walk us through how you would design a rate limiter.',
'question_mode' => 'interview',
'question_type' => 'text',
'priority' => 1,
],
],
'iai_requirements' => [
['title' => '5+ years backend experience', 'cfg_importance_id' => 273, 'priority' => 1],
],
'com_accesses' => [
['email' => 'candidate@example.com', 'fullname' => 'Jane Doe'],
],
];
$ch = curl_init('https://apis.flowmingo.ai/company/integration/interview/set/v1');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
payload := map[string]any{
"title": "Senior Backend Engineer - Async Interview",
"set_type": 1,
"question_mode": "predefined",
"interview_duration": 20,
"number_of_retakes": 1,
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"iai_questions": []map[string]any{
{
"title": "System design",
"content": "Walk us through how you would design a rate limiter.",
"question_mode": "interview",
"question_type": "text",
"priority": 1,
},
},
"iai_requirements": []map[string]any{
{"title": "5+ years backend experience", "cfg_importance_id": 273, "priority": 1},
},
"com_accesses": []map[string]any{
{"email": "candidate@example.com", "fullname": "Jane Doe"},
},
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest(
"POST",
"https://apis.flowmingo.ai/company/integration/interview/set/v1",
bytes.NewReader(body),
)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", "fl_live_abcd12345.n8G3zPNw9gWkK1Kj")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
}
require 'net/http'
require 'json'
require 'uri'
uri = URI('https://apis.flowmingo.ai/company/integration/interview/set/v1')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request['X-API-Key'] = 'fl_live_abcd12345.n8G3zPNw9gWkK1Kj'
request.body = {
title: 'Senior Backend Engineer - Async Interview',
set_type: 1,
question_mode: 'predefined',
interview_duration: 20,
number_of_retakes: 1,
project_id: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
iai_questions: [
{
title: 'System design',
content: 'Walk us through how you would design a rate limiter.',
question_mode: 'interview',
question_type: 'text',
priority: 1
}
],
iai_requirements: [
{ title: '5+ years backend experience', cfg_importance_id: 273, priority: 1 }
],
com_accesses: [
{ email: 'candidate@example.com', fullname: 'Jane Doe' }
]
}.to_json
response = http.request(request)
puts response.body
3.3.1 Sample request: let the AI decide
The same endpoint with question_mode realtime and no interview question of your own. Flowmingo prepares the interview questions from the description and the criteria you send, so the more precise they are, the better the questions. The pre_screening question is kept as you send it.
curl -X POST https://apis.flowmingo.ai/company/integration/interview/set/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"title": "Senior Backend Engineer - AI screening",
"position": "Senior Backend Engineer",
"question_mode": "realtime",
"description": "We are hiring a senior backend engineer to own our payments services.",
"interview_duration": 20,
"iai_questions": [
{
"title": "Location",
"content": "Are you based in or willing to relocate to Bengaluru?",
"question_mode": "pre_screening",
"question_type": "text"
}
],
"iai_requirements": [
{ "title": "5+ years backend experience", "content": "Has run production services.", "cfg_importance_id": 273 }
]
}'
3.4 Sample response
The endpoint returns a results envelope, with status 201. A request that carries an idempotency_key also gets idempotent_replay, and the repeat of one is answered 200 (section 3.2.6).
Treat the response as an acknowledgement, not a confirmation.
resultsdescribes the set itself and lists the ids of what was saved:iai_questionsandiai_requirementshold one{ "id", "external_id" }for each question and each criterion. It does not return the other fields you sent (their text, their options, the access entries), and it does not repeatproject_id. It also does not show when a value was stored differently from how you sent it: acfg_importance_idoutside the four valid IDs, for example, is stored as244, and aproject_idthat could not be attached leaves no trace in the answer. To confirm that the set was linked to your project, find it inGET /hiring/interview-sets/v1and read itsproject_ids. To check its questions and criteria, open the assessment in the dashboard.
{
"results": {
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"title": "Senior Backend Engineer - Async Interview",
"set_type": 1,
"status": 1,
"interview_duration": 20,
"number_of_retakes": 1,
"external_id": "REQ-48213",
"iai_questions": [
{ "id": "a7b8c9d0-1234-4abc-8def-1234567890ab", "external_id": "Q-SYSTEM-DESIGN" }
],
"iai_requirements": [
{ "id": "b8c9d0e1-2345-4bcd-9ef0-234567890abc", "external_id": null }
]
}
}
The fields shown are the ones to rely on; more may be present. Put id in com_interview_set_ids when you invite candidates (section 4).
external_idis your id for the assessment, exactly as you sent it, ornull.iai_questionsandiai_requirementslist the questions and the criteria that were saved, in the order of your arrays (they are numbered 1, 2, 3). On the repeat of a keyed request they are the ones the assessment holds now, in their current order (section 3.2.6): never pair them with your input by position. Anexternal_idmatches a row only where you sent one that no other entry in the list carries; an entry with no id, an entry with a repeated id and a question Flowmingo prepared cannot be matched. An entry you sent with neither atitlenor acontentwas skipped and has no item. For arealtimeinterview the questions Flowmingo prepared are listed too, withexternal_idnull. Eachexternal_idis the one you sent for that entry, ornull.idempotent_replay, besideresults, is there only when you sent anidempotency_key:falsefor the call that made the assessment,truefor a repeat (section 3.2.6).
3.5 Create a hiring project
A hiring project is where an assessment, its candidates and the job post come together. Create one through the API when your own system opens a role. The same call can make the project's assessments (section 3.5.5), set the job post's details yourself (section 3.5.6) and store your own ids on the project, its job post and its stages (section 3.5.11); you can also attach assessments later with project_id (sections 3.2 and 3.6) and invite candidates into it (section 4). To find the id of a project that already exists, see section 5.7; if the answer to a create call never reached you, read the call back by its idempotency_key (section 3.5.12). Once a project exists, you can read it by its id (section 3.5.13) and change its name, its role, its description and the words of its job post (section 3.5.14).
| Environment | API |
|---|---|
| Production | https://apis.flowmingo.ai/company/integration/hiring/projects/v1 |
Method: POST. Required API key scope: create_project.
It acts as the person who created the key, and that person must be an Owner or Admin of your organization at the moment of each call. The role is read again on every call, so a key whose creator was demoted stops working here at once, and so does a key whose creator has left the organization, during the grace period that follows (section 2.10): each answers 403 with the code project_create_requires_admin. After the grace period the key is refused before it reaches this route (section 2.10). If the role cannot be confirmed just now the answer is 503 with project_create_role_unconfirmed, and nothing is created. A key with no creator on record is refused with a 403 before anything runs (section 11). If the creator is an Admin who is limited to some projects, Flowmingo then tries to add the new project to that person's access, as the dashboard does, so the key can use it (section 2.9). If that step fails the project still exists; give the person access to it in the dashboard.
3.5.1 Request payload
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | The name of the project, as your team sees it in the dashboard. 1 to 255 characters, without <, > or a NUL character (U+0000). It is not unique: two projects can share a title. |
position |
string | Yes | The role being hired for. 1 to 255 characters, without <, > or a NUL character. It becomes the title of the job post. |
description |
string or null |
Optional | The job description as text, up to 15000 characters, without a NUL character. The job post is built from it (section 3.5.2). Leaving it out, sending null and sending blank text all mean no description. |
idempotency_key |
string | Yes | A token you choose for this project, 8 to 128 characters from A-Z a-z 0-9 . _ : -, for example the id of the job in your own system (section 3.5.3). |
assessments |
list of text or null |
Optional | Which assessments to make with the project. Leave it out (or send null) for a CV Evaluation and an AI Interview, send [] for none, or name any of cv_evaluation, ai_interview, skill_test, showcase, technical_test and role_play, each at most once (section 3.5.5). |
job_post |
object or null |
Optional | Details of the job post that you set yourself, by name instead of leaving them to the AI: job_functions, seniority_levels, employment_types, workplace_types, locations and salary (section 3.5.6). It also takes external_id, your own id for the job post (section 3.5.11). |
external_id |
string or null |
Optional | Your own id for the project, for example the requisition number in your ATS: plain text, 1 to 128 characters, stored exactly as you send it (section 3.5.11; the rules are in section 8.8). It is not the idempotency_key. |
stages |
list of objects or null |
Optional | Your own ids for the project's default stages, by stage name: up to 4 entries such as { "name": "Rejected", "external_id": "STAGE-REJECTED" } (section 3.5.11). |
Text is trimmed before it is checked, except an external_id, which is kept exactly as you send it. Only these eight fields are used. The settings the dashboard offers when it creates a project (file_link, attributes, filters, priority, cv_score, status, source, location, locations) are refused with 400 and the code project_field_not_supported when they carry a value, so you never believe one was applied; a null in one of them is ignored. Any other top-level property that is not in the table is ignored without an error (a misspelt setting is not reported, and the camelCase externalId is not read), but a key inside job_post, or inside an entry of stages, that the route does not take is refused (sections 3.5.6 and 3.5.11), and constructor, __proto__ and prototype count as keys there. At the top level of the body those three names are still ignored. A number, a boolean or a list where the text belongs is refused, not turned into text.
3.5.2 What is created
- An active hiring project with the source
api, owned by your organization and created by the person who created the key. It appears in the dashboard like any other project. It carries yourexternal_idwhen you sent one (section 3.5.11). - Its four default stages, in order: Unassigned, Shortlisted, In Progress, Rejected. Each carries the id you gave it in
stages, or none (section 3.5.11). - A job post drafted by the same AI that drafts one in the dashboard: from your
descriptionwhen you send one (it also reads the location, level and employment type out of the text), or from thepositionalone when you do not, which gives a weaker post. The post is saved active with a public link, titled with theposition, with no screening and no automatic invitation. It is not boosted to job boards. The post goes live within about a minute. The AI content check reviews the posts that are switched on and not yet checked once a minute, so the post is normally approved soon after the call (review_statusapproved,public_statelive), its public address opens, and after the next job board sync, a few minutes later, it shows next to your company's other jobs. Read the publication to see where it stands (section 3.7.1). To close the page, unpublish the post (section 3.7.3). No location is guessed from where your server is. Details you send injob_postreplace what the AI read (section 3.5.6), and the AI's own age, gender, marital status and nationality filters are always cleared. It carries thejob_post.external_idyou sent, never the project's. - A CV Evaluation and an AI Interview, unless you name other assessments or send
assessments: [](section 3.5.5). They are made after the project exists, from thepositionand thedescription, and attached to the project.
All or nothing. The call drafts the job post first, before anything is saved, and then saves the project, its four stages, the job post and the record of your call together: either all of them exist or none does. A project without its job post is never left behind. The assessments are made after that and are not part of it: a project is never lost to an assessment that fails (section 3.5.5).
A call that omits assessments now also makes a CV Evaluation and an AI Interview. It waits up to about 45 seconds for them, where a call that makes none answers in a few seconds, and the assessments use the generation budget of section 3.6.11. Send assessments: [] to create the project alone. To attach other assessments, name them in assessments or create them later with project_id (sections 3.2 and 3.6), then invite candidates (section 4).
When the AI cannot draft the job post, nothing is created. A model call sometimes fails or takes too long. The call then answers 503 with the code job_post_generation_failed and a Retry-After header (in seconds), creates no project, and does not count against the project limits (it is still a request against the per-key limit of section 8.5). Send the same request again with the same idempotency_key. If it keeps failing, shorten or simplify the description.
The answer still carries job_post_error, and it is always null now. It only reads a reason (job_post_generation_failed, job_post_save_failed or job_post_interrupted) for a project that was created before 22 October 2026, when the post was drafted after the project was saved; when you read such a project again with its old key, the reason is still shown.
3.5.3 Sending the same request twice
idempotency_key is required, and it belongs to your organization, whichever of your API keys sends it.
- Same key, same
title,position,description,assessments,job_postand ids (external_id,job_post.external_idandstages): you get the project the first call created, withidempotent_replaytrue. The status is201when the call made no assessments and200when it made some (202while one is still being made: section 3.5.5). No second project is created, and the repeat does not count against the project limits below. A repeat can start or resume the project's assessments (section 3.5.5), and that takes units from the generation budget (section 3.6.11). The answer is read fresh, so it shows the project as it is now, including any edit made in the dashboard, and itsmissing_details(section 3.5.7) reflect those edits. The ids are read from the saved rows, so a repeat returns exactly the ids the first call stored. This is how you get the id back when the answer to the first call was lost; it is not a way to look projects up (section 5.7). - Same key, a different
title,position,description, set of assessments,job_postblock or id:409with the codeidempotency_key_reused, and the message "This idempotency_key was already used with a different title, position, description or set of assessments, or with different job post details (the job_post block), or with different external ids (external_id, job_post.external_id or stages). Use a new key for a different project, or send the original body to get the original project back. Nothing was created." Use a new key for a different project. The order of the assessments, and the order and letter case of the names in the listsjob_functions,seniority_levels,employment_typesandworkplace_types, do not make a different request. Leavingassessmentsout (or sendingnull), sending[]and sending a list are three different requests. - The ids are part of the request. An id counts exactly as you sent it (
REQ-1andreq-1are two ids), the order of the entries ofstagesdoes not matter, andnullis the same as leaving an id out. An id added to a repeat, an id dropped from it, another id, and an id moved between the project, the job post and a stage are each a different request, so a409. A request that carries no id is compared exactly as before, so a call you made earlier still repeats. A project made before ids existed can be repeated only without any: the same key with a body that now carries an id is a409. - A key is remembered for good. It is never expired, so the same key with the same body returns the same project (reported as
deletedif someone has since deleted it) and with an edited body is always a409. If your own id is the key (for example the id of a job) and the job can be reopened with changes, add a revision to it, such asats-job-48213-r2. - A call that fails with a
5xxor times out: send it again with the same key. The project may already exist, and then you get it back; if it does not, the repeat simply creates it. After a503job_post_generation_failednothing was created: send the same request again after theRetry-Afterwait (about 30 seconds; about 90 seconds when the AI took too long to answer, because that attempt is still running on our side and a repeat before it ends is answered409idempotency_request_in_progress). - Two calls with the same key at the same moment: one project is created. The other call is answered
409idempotency_request_in_progresswhile the first is still working (a job post takes a few seconds to draft). Wait a few seconds and send the same request again to get the project. If your organization is at its limit (section 3.5.4), the second call can instead be answered429: send it again with the same key.
3.5.4 Limits
Each organization can create 10 projects per minute and 300 per UTC day, counted across all its keys. A call past a limit answers 429 with the code project_create_limit_reached, and nothing is created. The message says whether to try again in a minute (the per-minute limit) or after 00:00 UTC (the daily one). A call that creates nothing (a 503 for the job post, a refusal) gives its place back. If the limit cannot be checked just now the call answers 503 project_create_limit_unavailable and creates nothing: it never creates a project it could not count.
These limits count projects. The assessments a call makes also take units from the generation budget of section 3.6.11, which the standalone assessment routes share: when that budget is spent the project is still created and the assessment is reported as failed (section 3.5.5).
3.5.5 Make the assessments in the same call
assessments says which assessments the call makes for the project. Each one is published, attached to the project and ready to send to candidates, the way the matching route of section 3.6 leaves it. The call needs only the create_project scope, not create_assessment.
| You send | The call makes |
|---|---|
assessments left out, or null |
A CV Evaluation and an AI Interview. |
[] |
No assessment. The answer carries assessments: []. |
| A list | Exactly those, reported in the order you list them. Any of cv_evaluation, ai_interview, skill_test, showcase, technical_test and role_play, each at most once. |
- They are made in parallel, with the project's
positionas the job title and itsdescriptionas the job description, and with the dashboard's defaults. For options such as your own criteria, languages or durations, create the assessment with its own route and the project'sidasproject_id(section 3.6). The call normally writes one list of criteria and shares it among the CV Evaluation, the AI Interview and the Role-play you named; if that attempt fails, each of them writes its own. - A project holds one CV Evaluation. A request is refused with
409project_already_has_cv_evaluationwhen the project already has one, so if you namecv_evaluationhere (or leaveassessmentsout), do not create another one for the project later. The check is not atomic: do not create two CV Evaluations for the same project at the same moment. Requests that follow one another are refused. - The
positionmust fit every kind you name. Every assessment takes a job title of at least 2 characters, and a Skill Test, a Showcase and a Technical Test take at most 200 (a project'spositioncan be longer). One that does not fit fails that assessment only, with the codeassessment_compose_invalid_input. - The call waits up to about 45 seconds for the assessments, and the status says how far they got:
| Situation | Status |
|---|---|
Every assessment is final (ready or failed) |
201 for the call that created the project, 200 for a repeat of it (idempotent_replay true). |
Any assessment is still pending |
202 with a Retry-After header (10 seconds), for a first call and a repeat alike. |
You sent assessments: [] |
201, also for a repeat. |
- A Technical Test is not waited for. It takes 3 to 9 minutes, so a Technical Test that starts is
pendingin the first answer. One that cannot start (for example because the generation budget is spent) is reportedfailedat once. The call answers202only while some entry ispending. - To read the result, send the same request again with the same
idempotency_key, until no entry ispending. A repeat returns the project, reports every assessment as it stands, and never makes an assessment twice. An entry that failed withretryabletrueis tried again by the repeat (unless the repeat sent aPreferthat shortens the wait: see the next bullet). - To wait less, send a
Preferheader. It is the header of the assessment routes (section 3.6.2) and it shortens the wait for the assessments only.Prefer: respond-asyncanswers as soon as every assessment can be reported, or after about two seconds if any is still being made (a202withRetry-After, as above; one that is already finished is reported as it stands), andPrefer: wait=20answers when they are all final or after about 20 seconds (up to two seconds more; a longer wait is cut to the usual wait of about 45 seconds). It does not shorten the job post's draft, which comes first and usually takes 2 to 4 seconds (up to 45), nor sign-in and validation, so give a caller that times out early a few seconds more than its own network needs. The answer carriesPreference-Applied(respond-asyncorwait=<seconds>) when the call made assessments and the header was applied; without the header, or with one that cannot be read, nothing changes. It is not part of the request, so the sameidempotency_keycan be sent again with a different preference (or none). What it leaves unfinished carries on: send the same request again, with or withoutPrefer, to read where it has got to. A call whosePrefershortens the wait does not start a failed assessment again: it reports the entryfailed, with its code, so that a caller who polls does not spend generation budget on a restart it cannot see (in a rare race, or if the record cannot be read, it can still be started again). To try a failure withretryabletrueagain, send the same request withoutPrefer. - A project made before 22 October 2026 keeps replaying as it was made. A repeat of its call makes no assessment and answers with the fields it always had (none of
assessments,partial_failureandmissing_details), and the same key with a non-emptyassessmentslist or ajob_postblock is a409(a different request). A project made from that date on gets the behavior described in this section. - A project is never lost to an assessment. The assessments are made after the project exists. One that fails is reported in its entry with a code and
partial_failureistrue, and the call is still a success.
The answer gains these fields:
| Field | What it holds |
|---|---|
assessments |
One entry for each kind you asked for, in the order you asked. |
assessments[].kind |
cv_evaluation, ai_interview, skill_test, showcase, technical_test or role_play. |
assessments[].status |
ready, pending or failed. |
assessments[].assessment |
When ready: the assessment exactly as its own route answers it (section 3.6.4 onward), its id included, without idempotent_replay. Its external_id is null: this call gives the assessments no id of yours. To give one, make the assessment with its own route (section 3.6.4). |
assessments[].failure |
When failed: code, message and retryable. retryable true means that sending this request again with the same idempotency_key can finish the assessment; false means an unchanged repeat is not expected to resolve it without a corrective step (section 3.6.12). |
partial_failure |
true when at least one entry failed. A pending entry is not a failure. |
The codes in failure are those of section 3.6.12, and assessment_compose_invalid_input (the project's position or description does not fit that kind; make it with its own route and a job title that fits). The generation budget of section 3.6.11 applies: when it is spent, or generation is busy, an entry fails with assessment_create_limit_reached or assessment_capacity_busy and retryable true, and the project is created all the same. An assessment that someone deletes in the dashboard is reported as failed with assessment_was_deleted by a later repeat, and is not made again.
3.5.6 Set the job post's details yourself
By default the AI that drafts the job post reads the location, level, employment type and the rest out of your description. job_post lets you set those details yourself, so a value that matters to matching and filtering is the one you meant. Every value is given by name, in the English words the dashboard shows (there is no route that lists Flowmingo's internal ids). Every key is optional: what you send wins over what the AI read, and what you leave out keeps the AI's value. These keys set the job post's details, not its words: to put your own title and description on the job post, create the project and then edit it (section 3.5.14).
| Key | Takes | At most | Example |
|---|---|---|---|
job_functions |
list of names | 8 | ["Software Engineering"] |
seniority_levels |
list of names | 3 | ["Mid-Senior level"] |
employment_types |
list of names | 6 | ["Full-time"] |
workplace_types |
list of names (On-site, Remote, Hybrid) |
4 | ["Hybrid"] |
locations |
list of places | 20 | [{ "country": "Singapore" }] |
salary |
an object, or null |
{ "min": 6000, "max": 9000, "currency": "SGD", "period": "monthly" } |
| You send | The job post gets |
|---|---|
| A non-empty list | That list, in place of the AI's. |
[] |
The list cleared. |
The key left out, or null in a list |
The AI's value. |
salary as an object |
That pay, in place of the AI's as a whole (a part you leave out is empty). |
salary: null |
No pay shown (the dashboard reads Negotiable), and any pay the AI found is discarded. |
salary left out |
The AI's pay. |
job_post also takes external_id, your own id for the job post (section 3.5.11). It is not a detail: a block that holds only external_id leaves every detail to the AI and looks no name up.
How names are matched. Use the names the dashboard shows. Letter case does not matter, nor does the difference between a space, -, _ and / (Full-Time, full_time and FULL TIME are one name), nor the kind of apostrophe (Côte d'Ivoire and Côte d’Ivoire are one name). Accents do matter (Zurich is not Zürich). Matching never guesses a near miss (no plural, synonym or spelling correction), but it accepts more than the displayed name: a country by its full name, a stored alias or its two-letter code, and a state or city by its full name. A name listed twice (Sales, SALES) counts once. A name that matches nothing is refused with 400 job_post_name_unknown, and its message lists the accepted names (the closest ones, with a count of the rest, when the list is too long to print), so you can correct the name and send the request again. Nothing is created.
Places. Each place is looked up exactly, a country, then its state, then its city.
| Field | Meaning |
|---|---|
country |
Required. The English name of the country or its two-letter code (Singapore, SG). |
state |
The state, province or region inside the country. Where a country lists its cities directly (Vietnam and Singapore do), send that level as state. |
city |
The city. It needs state, because city names repeat across a country: a city without a state is refused with 400 and no code. |
address |
A street address or a note as plain text, at most 500 characters, without < or >. The address is trimmed, and runs of whitespace, line breaks included, become single spaces before it is saved. |
- A place is saved once. The same place named twice with the same address is one place; with two different addresses it is refused with
400job_post_value_invalid, so put both addresses in oneaddress, or leave one entry out. - A name that matches two places under the same parent is refused with
400job_post_location_ambiguous. Leave that level out and put the rest inaddress. - When every workplace type of the post is
Remote, the places are saved at country level and the addresses are dropped (the dashboard then reads "Hiring In" with the countries). This applies to the places you send, and to the workplace types you send or, if you send none, the AI's.
Salary. min and max are JSON numbers (not text) from 0 up to, but not including, 10,000,000,000,000, with at most two decimals; send at least one of them, and max must not be below min. When you send an amount you must also send currency and period, and nothing is assumed:
| Field | Accepted |
|---|---|
currency |
USD, SGD, VND, EUR, GBP, INR, AUD, MYR, IDR, PHP or THB. Letter case does not matter. |
period |
annual, monthly or hourly. Letter case does not matter. |
What can never be set. A job post made through the API cannot filter on age, gender, marital status, nationality or religion, in any spelling: a key of that kind inside job_post is refused, and the AI's own age, gender, marital status and nationality filters are always cleared, with a job_post block or without one. Any other key that is not in the table above (or external_id), at any level of the block, is refused too, and constructor, __proto__ and prototype are refused like any other key.
| Status | code |
When | What to do |
|---|---|---|---|
400 |
job_post_name_unknown |
A name matches nothing. | Correct it using the names in the message, and send the request again. |
400 |
job_post_location_ambiguous |
A place name matches two places under one parent. | Leave that level out and use address. |
400 |
job_post_value_invalid |
A salary that cannot be used (no currency or period, an unknown one, max below min, a negative amount, a third decimal, neither min nor max), or one place named twice with two different addresses. |
Fix the value named in the message. |
400 |
job_post_field_not_supported |
A key the block does not take, or one of age, gender, marital status, nationality and religion. | Remove it. |
400 |
(no code) | The block has the wrong shape: it is not an object, a list is too long, a name is blank or contains < or >, an amount is text, an address contains < or > or is over 500 characters, or a city has no state. |
Fix the shape named in the message. |
503 |
job_post_catalogue_unavailable |
The names could not be checked just now. A Retry-After header says how long to wait. |
Send the same request again with the same idempotency_key. |
On every row nothing is created, and the call does not count against the project limits (the coded ones say "Nothing was created." in the message). The block is read only when the project is created: the same idempotency_key with a different job_post is a 409 (section 3.5.3), and to change a job post afterwards you edit it in the dashboard. The one exception is the job post's own title and description, which the edit route of section 3.5.14 changes, while the job post is switched on; the details of this section are changed only in the dashboard.
For example, a project alone, with its job post set by name:
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/projects/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"title": "Customer Success Manager - Singapore",
"position": "Customer Success Manager",
"description": "We are hiring a customer success manager to look after our largest accounts and keep them renewing...",
"idempotency_key": "ats-job-48214",
"assessments": [],
"job_post": {
"job_functions": ["Customer Service"],
"seniority_levels": ["Mid-Senior level"],
"employment_types": ["Full-time"],
"workplace_types": ["Hybrid"],
"locations": [{ "country": "Singapore" }],
"salary": { "min": 6000, "max": 9000, "currency": "SGD", "period": "monthly" }
}
}'
The answer is 201:
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Customer Success Manager - Singapore",
"position": "Customer Success Manager",
"status": "active",
"source": "api",
"created_at": "2026-10-22T04:30:00.000Z",
"external_id": null,
"stages": [
{ "id": "b0b0b0b0-abcd-4ef0-8123-456789abcdef", "name": "Unassigned", "external_id": null },
{ "id": "1d2c3b4a-5f6e-4d7c-9b8a-0a1b2c3d4e5f", "name": "Shortlisted", "external_id": null },
{ "id": "2e3d4c5b-6a7f-4e8d-8c9b-1b2c3d4e5f60", "name": "In Progress", "external_id": null },
{ "id": "3f4e5d6c-7b8a-4f9e-a0dc-2c3d4e5f6071", "name": "Rejected", "external_id": null }
],
"job_post": { "id": "c0ffee00-dead-4bee-8f00-0123456789ab", "status": "active", "external_id": null },
"job_post_error": null,
"idempotent_replay": false,
"missing_details": [],
"assessments": [],
"partial_failure": false
}
A name that matches nothing answers 400 before anything is created:
{
"statusCode": 400,
"code": "job_post_name_unknown",
"message": "job_post.workplace_types[0]: `Teleport` is not a workplace type we know. Accepted: On-site, Remote, Hybrid. Use a name the dashboard shows; case and the separators - _ / are not significant. Nothing was created.",
"timestamp": "2026-10-22T04:30:00.000Z",
"path": "/integration/hiring/projects/v1"
}
3.5.7 What the job post still lacks: missing_details
Every answer to a project made from 22 October 2026 on carries missing_details, a list of the details the saved job post still lacks, so your system can see at once whether the AI found everything it needs in your description (and whether the job_post you sent covered the rest).
| Entry | The job post lacks |
|---|---|
description |
A description: nothing a reader can see is left once markup is removed. |
workplace_types |
A workplace type (on-site, remote or hybrid). |
employment_types |
An employment type. |
locations |
A place. Not reported when every workplace type of the post is Remote; a post with no workplace type is not remote-only, so locations is listed whenever it has no place. |
seniority_levels |
A seniority level. |
job_functions |
A job function. |
- The entries come in this order, and only the ones that are missing are listed.
[]means nothing is missing.nullmeans it cannot be told: the project has no job post, or the check could not be completed just now. - The salary is never listed: a post with no pay reads as Negotiable.
- It is read from the job post as it is now, on every call. A repeat of the call after someone completed the post in the dashboard shows the shorter list.
job_postis read only when the project is created, so you cannot add a missing detail by sending the same request again with a different block (that is a409). Complete the post in the dashboard, or create the next project with the details injob_post. A missingdescriptioncan also be filled with the edit route (section 3.5.14), which sets the words of the job post.
For example, a description that names no place gives "missing_details": ["locations"].
3.5.8 Sample request
This call names no assessments, so it also makes a CV Evaluation and an AI Interview. Add "assessments": [] to create the project alone; section 3.5.6 has a sample that sets the job post by name.
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/projects/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"title": "Support Engineer - Q4 hiring",
"position": "Support Engineer",
"description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets.",
"idempotency_key": "ats-job-48213"
}'
const res = await fetch('https://apis.flowmingo.ai/company/integration/hiring/projects/v1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'fl_live_abcd12345.n8G3zPNw9gWkK1Kj'
},
body: JSON.stringify({
title: 'Support Engineer - Q4 hiring',
position: 'Support Engineer',
description: 'We are hiring a support engineer for our payments platform...',
idempotency_key: 'ats-job-48213'
})
})
const project = await res.json() // 201; 202 while an assessment is still being made: send the same request again
// project.id is the project_id for the assessment routes (section 3.6) and for POST /interview/set/v1
3.5.9 Sample response
The answer is 201: both assessments were ready within the wait (criteria is cut to two of its seven entries here).
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Support Engineer - Q4 hiring",
"position": "Support Engineer",
"status": "active",
"source": "api",
"created_at": "2026-10-22T04:18:44.051Z",
"external_id": null,
"stages": [
{ "id": "b0b0b0b0-abcd-4ef0-8123-456789abcdef", "name": "Unassigned", "external_id": null },
{ "id": "1d2c3b4a-5f6e-4d7c-9b8a-0a1b2c3d4e5f", "name": "Shortlisted", "external_id": null },
{ "id": "2e3d4c5b-6a7f-4e8d-8c9b-1b2c3d4e5f60", "name": "In Progress", "external_id": null },
{ "id": "3f4e5d6c-7b8a-4f9e-a0dc-2c3d4e5f6071", "name": "Rejected", "external_id": null }
],
"job_post": { "id": "c0ffee00-dead-4bee-8f00-0123456789ab", "status": "active", "external_id": null },
"job_post_error": null,
"idempotent_replay": false,
"missing_details": ["locations"],
"assessments": [
{
"kind": "cv_evaluation",
"status": "ready",
"assessment": {
"id": "7d2c1c3e-5b1f-4f55-9a3b-0c9a2f7e8d11",
"type": "cv_evaluation",
"name": "Support Engineer – CV Evaluation, 22 Oct '26",
"job_title": "Support Engineer",
"external_id": null,
"status": "active",
"ready": true,
"report_language": "en",
"criteria": [
{
"id": "1f0e0f0a-6b6c-4c1c-9a3a-0a0a0a0a0a0a",
"title": "Customer support and ticket triage",
"details": "Help payments-platform customers by reviewing, prioritizing, and responding to support tickets.",
"importance": "must_have"
},
{
"id": "2a1f1a1b-7c7d-4d2d-8b4b-1b1b1b1b1b1b",
"title": "Clear written communication",
"details": "Write understandable, helpful answers to customers about their issues.",
"importance": "must_have"
}
],
"criteria_count": 7,
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"linked": true,
"complimentary_cv_evaluation": { "enabled": true, "notice_acknowledged": true }
}
},
{
"kind": "ai_interview",
"status": "ready",
"assessment": {
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"type": "ai_interview",
"name": "Support Engineer — AI Interview, 22 Oct '26",
"job_title": "Support Engineer",
"external_id": null,
"status": "active",
"ready": true,
"duration_minutes": 20,
"interview_languages": ["English"],
"report_language": "en",
"criteria": [
{
"id": "4d3e5f6a-8b9c-4d1e-a2f3-4b5c6d7e8f90",
"title": "Customer support and ticket triage",
"details": "Help payments-platform customers by reviewing, prioritizing, and responding to support tickets.",
"importance": "must_have"
},
{
"id": "5e4f6a7b-9c0d-4e2f-b3a4-5c6d7e8f9a01",
"title": "Clear written communication",
"details": "Write understandable, helpful answers to customers about their issues.",
"importance": "must_have"
}
],
"criteria_count": 7,
"questions": { "interview": 13, "pre_screening": 2 },
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"linked": true
}
}
],
"partial_failure": false
}
The fields shown are the ones to rely on; more may be added (section 8.7). status is a word on this route (a project reads active, draft, inactive, archived or deleted; a job post reads active, pending, inactive, archived, approved, rejected or expired), while the read routes return the same states as numbers. The answer to a repeated key shows the project as it is now: a renamed stage, a job post deleted in the dashboard (job_post null; job_post_error stays null when the post was created, and shows the original reason when its creation had failed), or status deleted for a project someone has since deleted. missing_details here says the AI found no place in the description (section 3.5.7). The job post is active in this answer. Its AI content check runs about a minute later, and its page is then live (section 3.5.2).
3.5.10 Errors on this route
| Status | code |
What to do |
|---|---|---|
400 |
(no code) | A field is missing, too long, or not text, or idempotency_key is not 8 to 128 characters from the allowed set; assessments is not a list of the six kinds, names one twice or names more than six; job_post has the wrong shape (section 3.5.6); an external_id breaks the rules of section 8.8, or stages has the wrong shape (section 3.5.11). message names the field. Nothing was created. |
400 |
project_field_not_supported |
The body carries a dashboard-only setting. Remove it. |
400 |
api_key_has_no_acting_user |
The key has no user and no creator. A key with no creator on record is normally refused first, with a 403 (section 11). Create the key from a signed-in account. |
400 |
job_post_name_unknown, job_post_location_ambiguous, job_post_value_invalid, job_post_field_not_supported |
A value in job_post cannot be used (section 3.5.6). The message explains the problem; for an unknown name it lists the accepted names (or the closest ones). Nothing was created: correct it and send the request again. |
400 |
stage_name_unknown, stage_name_duplicated, stage_field_not_supported |
An entry of stages names a stage a new project does not have, names one twice, or carries a key other than name and external_id (section 3.5.11). The message lists the four stage names. Nothing was created: correct it and send the request again. |
403 |
(no code), or key_creator_left, key_reaches_nothing, key_no_creator |
The key lacks the create_project scope, or has expired (no code), or its creator has left (section 2.10), can open no hiring project, or is not on record (the codes; section 11). |
403 |
project_create_requires_admin |
The person who created the key is not an Owner or Admin of the organization (any more). |
409 |
idempotency_key_reused |
The key was used with a different title, position, description, set of assessments, job_post block or id (external_id, job_post.external_id, stages) (section 3.5.3). |
409 |
idempotency_request_in_progress |
A call with this key is still being worked on (or its AI attempt has not ended yet). Wait a few seconds, then send the same request again. |
429 |
project_create_limit_reached |
10 a minute or 300 a day reached. Try again in a minute, or after 00:00 UTC; the message says which. |
503 |
job_post_generation_failed |
The AI could not draft the job post. Nothing was created. Send the same request again with the same key after the Retry-After wait. |
503 |
job_post_catalogue_unavailable |
The names in job_post could not be checked just now. Nothing was created. Send the same request again with the same key after the Retry-After wait. |
503 |
project_create_limit_unavailable, project_create_role_unconfirmed |
A check could not be completed. Nothing was created. Try again in a moment. |
An assessment that cannot be made is not an error of this route: the project is created and the entry says why (section 3.5.5).
3.5.11 Your own ids on the project, its job post and its stages
The call can store ids of your own on three things it creates, so you can find them again by the number your own system uses. The rules are those of section 8.8: optional, plain text of 1 to 128 characters, stored exactly as you send them, scoped to your company, not unique, and fixed when the project is created.
| You send | It is the id of | It comes back as |
|---|---|---|
external_id |
The project | external_id in the answer, on each row of GET /hiring/projects/v1, and as project.external_id in the status read (section 3.5.12). GET /hiring/projects/v1?external_id= finds it. |
job_post.external_id |
The job post this call creates | job_post.external_id in the answer, and external_id on GET /hiring/job-posts/v1 and GET /hiring/job-posts/{id}/v1. GET /hiring/job-posts/v1?external_id= finds it. |
stages[].external_id |
The default stage named in the same entry | stages[].external_id in the answer, and external_id on each row of GET /hiring/projects/{projectId}/stages/v1. There is no filter. |
stages. A list of at most four entries, each exactly { "name": ..., "external_id": ... }, with both keys in every entry (an entry whose external_id is null is refused). [], null and leaving stages out mean no stage ids. The name is one of Unassigned, Shortlisted, In Progress and Rejected, spelled as stages[].name spells them in the answer: case matters, nothing is trimmed, and the stage's machine type is not accepted. Name each stage once. A stage you do not list gets null. Any other key in an entry, such as color or the camelCase externalId, is refused, not ignored.
The answer. external_id on the project, on every entry of stages and on job_post is always there: your id, or null when you gave none. The ids are written together with the rows and read back from them, so a repeat (section 3.5.3) returns exactly what the first call stored. The assessments the call makes get no id (section 3.5.5). A project you duplicate in the dashboard starts without ids, and so does its job post.
A job_post block that holds only external_id sets no detail: the AI's values stay (section 3.5.6). The camelCase job_post.externalId is refused as a key the block does not take.
The ids are part of the request. The same idempotency_key with another id, an id added or an id dropped is a 409 (section 3.5.3).
| Status | code |
When |
|---|---|---|
400 |
(no code) | An external_id breaks the rules of section 8.8. The message is "external_id must be plain text of 1 to 128 characters with no control characters. It is stored exactly as sent." Or stages has the wrong shape, and the message says how: "stages must be a list of stages with their ids, or left out", "stages can name at most 4 stages (there are 4 default stages)", "each entry of stages must be an object with a name and an external_id", "each entry of stages needs a stage name" (or "... as text"), or "each entry of stages needs an external_id". |
400 |
stage_name_unknown |
A name is not one of the four. The message lists them. |
400 |
stage_name_duplicated |
The same stage is named twice. |
400 |
stage_field_not_supported |
An entry carries a key other than name and external_id (constructor, __proto__ and prototype are such keys). |
On every row nothing is created and nothing is reserved, no AI is asked and the call does not count against the project limits (section 3.5.4). The check for a key that is not allowed inside stages or job_post runs first, right after the key itself is accepted, so a body with several faults answers stage_field_not_supported or job_post_field_not_supported before any other 400. A body that carried such a key used to be accepted, with the key dropped; a repeat of such an earlier call is now this 400, not a replay. For example:
{
"statusCode": 400,
"code": "stage_name_unknown",
"message": "stages[0].name `Hired` is not a default stage of a new project. The stages are: Unassigned, Shortlisted, In Progress, Rejected (spelled exactly like this, case matters). Nothing was created.",
"timestamp": "2026-11-02T04:30:00.000Z",
"path": "/integration/hiring/projects/v1"
}
A project with ids, made alone (assessments: []):
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/projects/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"title": "Support Engineer - Q4 hiring",
"position": "Support Engineer",
"description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets.",
"idempotency_key": "ats-job-48213",
"assessments": [],
"external_id": "REQ-48213",
"job_post": { "external_id": "POST-48213" },
"stages": [
{ "name": "Unassigned", "external_id": "STAGE-NEW" },
{ "name": "Rejected", "external_id": "STAGE-REJECTED" }
]
}'
The answer is 201:
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Support Engineer - Q4 hiring",
"position": "Support Engineer",
"status": "active",
"source": "api",
"created_at": "2026-11-02T04:18:44.051Z",
"external_id": "REQ-48213",
"stages": [
{ "id": "b0b0b0b0-abcd-4ef0-8123-456789abcdef", "name": "Unassigned", "external_id": "STAGE-NEW" },
{ "id": "1d2c3b4a-5f6e-4d7c-9b8a-0a1b2c3d4e5f", "name": "Shortlisted", "external_id": null },
{ "id": "2e3d4c5b-6a7f-4e8d-8c9b-1b2c3d4e5f60", "name": "In Progress", "external_id": null },
{ "id": "3f4e5d6c-7b8a-4f9e-a0dc-2c3d4e5f6071", "name": "Rejected", "external_id": "STAGE-REJECTED" }
],
"job_post": { "id": "c0ffee00-dead-4bee-8f00-0123456789ab", "status": "active", "external_id": "POST-48213" },
"job_post_error": null,
"idempotent_replay": false,
"missing_details": [],
"assessments": [],
"partial_failure": false
}
Later, find the project by your own number with GET /hiring/projects/v1?external_id=REQ-48213 (section 8.8).
3.5.12 Read a create call back by its key
GET /hiring/projects/requests/{idempotency_key}/v1 says what became of a create call, from the idempotency_key you sent with it. Use it when the answer never reached you (a timeout, a dropped connection) and you want the project's id without sending the body again. It only reads: it creates nothing, starts nothing, and does not count against the project limits (section 3.5.4).
Required API key scope: create_project, the scope of the create call. The route acts as the same person: the person who created the key must still be an Owner or Admin of the organization on every read. The {idempotency_key} is the key of the create call, 8 to 128 characters from A-Z a-z 0-9 . _ : -. Spaces at either end of it are ignored, the same way the create call ignores them: a call sent with " ats-job-48213 " is found by reading ats-job-48213, or the same text with its spaces (written %20 in a URL). A space inside the key is never valid. Any key of your organization can read a call that another key made, as long as the project is one this key can open (section 2.9).
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" "$BASE/hiring/projects/requests/ats-job-48213/v1"
{
"idempotency_key": "ats-job-48213",
"status": "created",
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"project": {
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Support Engineer - Q4 hiring",
"external_id": "REQ-48213",
"deleted": false,
"archived": false
},
"job_post": { "state": "created", "failure_code": null },
"created_at": "2026-11-02T04:18:44.051Z"
}
| Field | What it holds |
|---|---|
idempotency_key |
The key you asked about. |
status |
created when the project and its job post are saved: every call made now reads created. pending (the job post was still being built) and job_post_failed (the project was saved and its job post was not) can only be read for a project created before 22 October 2026. |
project_id |
The id of the project the call made. Use it as project_id or com_project_id in the other routes (section 5.7). It is there also after the project was deleted. |
project |
The project as it is now: id, title, external_id (your id, or null), deleted and archived. If the project's row is gone altogether, title and external_id are null and deleted is true. |
job_post |
How the job post went: state (created, pending or failed) and failure_code (job_post_generation_failed, job_post_save_failed or job_post_interrupted when state is failed, otherwise null). |
created_at |
When the project was saved, as an ISO 8601 time in UTC. |
A record that is still pending after 5 minutes reads as created when its job post was saved (a post, or a project, deleted since still counts as saved), and otherwise as job_post_failed with job_post_interrupted. The read does not write that down; the next repeat of the create call does. The read does not report the project's assessments: for where each one has got to, send the create request again (section 3.5.5).
What an unknown key can mean. The record of a call is written in the same step as the project. A call that is still running (it drafts the job post before it saves anything), or that ended before its project was saved (a 503 for a job post that could not be drafted, a 429, a 409, a crash), has no record yet. A 404 for a key you did send therefore does not prove that no call was made, and it can also mean the project was not saved (yet). If you are not sure, send the same create request again with the same idempotency_key: you get the project the first call made, or the project is made now (section 3.5.3).
Deleted projects and reach. project.deleted is true once the project has been deleted, and the record of the call stays, with the project's title and external_id. Only a key whose creator can open every project (an Owner, or an Admin with access to all projects) reads it that way. For a creator who is limited to some projects, a deleted project is no longer one of theirs, so the same request reads as the 404 below, not as deleted true. An archived project reads archived true.
| Status | code |
What it means | What to do |
|---|---|---|---|
404 |
project_request_not_found |
One message for four reasons: no call of your organization used this key; the key is not 8 to 128 characters from the allowed set (once the spaces at its ends are removed); the key belongs to another organization; or the call's project is one the key's creator cannot open. The message is "No hiring project request with this idempotency_key was found that this API key can read. A create call that is still running, or that ended before its project was saved, has no record yet, so an unknown key can also mean the project was not saved (yet)." It never says that nothing was created. | Check the key. If you cannot tell whether the call was saved, send the create request again (above). |
404 or 400 |
(no code) | The web layer answers two keys itself, before the route runs, so neither carries a code: an empty key (.../projects/requests//v1) is not a route and answers 404 with a message that begins Cannot GET, and a key with an invalid percent-escape (for example %ZZ) answers 400 with a message that begins Failed to decode param. |
Send a real key. |
403 |
(no code) | The key lacks the create_project scope, or has expired (section 11). |
Tick the scope, or use a key that has not expired. |
403 |
key_reaches_nothing |
The person who created the key can open no hiring project at all (for example the last project they were given was deleted), so the key is refused before the route runs, as on every route (section 11). | Give the creator project access, or take the key over (section 2.10). |
403 |
project_create_requires_admin |
The person who created the key is no longer an Owner or Admin of the organization. The message talks of creating hiring projects, although this route only reads. | Use a key made by an Owner or Admin. |
503 |
project_create_role_unconfirmed |
The role of the person who created the key could not be confirmed just now. Nothing was read. | Try again in a moment. |
How it fits sections 5.7 and 8.6. Section 5.7 lists where a project's id comes from. This read is one more source, for one case: you hold the idempotency_key of the create call and need the id. It is not a way to search projects: there is no project-list filter by idempotency_key. If you also sent an external_id, GET /hiring/projects/v1?external_id= finds the project (section 8.8), but that list is read from a replica and a project made a moment ago can take a few seconds to appear; this read does not wait for a replica. When a create call times out (section 8.6), read this route first: with status created you have the id and need send nothing; with a 404, send the create request again with the same key and the same body.
3.5.13 Read one hiring project
GET /hiring/projects/{id}/v1 returns one hiring project by its Flowmingo id: its name, role and status, its job post (the words it displays, the words as they are stored before the content check could reword them, and whether its public page is open), and the assessments linked to it. It only reads. Required API key scope: read_hiring. Section 5.7 shows how to find a project's id. The id may be written in capital or small letters; the answer gives it in small letters. The read comes from the primary database, so an edit you have just made (section 3.5.14) shows at once, which the project list of section 5.7, read from a replica, does not promise. After a timeout or a 503 on an edit, this read is how you find out whether your words landed: compare job_post.sent_title and job_post.sent_description (of the project's newest job post) with what you sent, after removing leading and trailing blanks from it (see the table below). The answer to an edit carries this same view of the project only for a key that also holds read_hiring (section 3.5.14).
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" \
"$BASE/hiring/projects/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/v1"
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Support Engineer - Q4 hiring",
"position": "Support Engineer",
"status": "active",
"description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets.",
"external_id": "REQ-48213",
"created_at": "2026-11-02T04:18:44.051Z",
"updated_at": "2026-11-02T04:18:44.051Z",
"job_post": {
"id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"status": "active",
"public_state": "live",
"external_id": "POST-48213",
"title": "Support Engineer",
"description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets.",
"sent_title": "Support Engineer",
"sent_description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets.",
"review": "approved",
"differs_from_sent": false
},
"assessments": [
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"assessment_type": "ai_interview",
"status": 1,
"status_label": "active",
"external_id": null
}
]
}
| Field | What it holds |
|---|---|
id, title, position |
The project, its name and the role being hired for (position is null when none is stored). |
status |
active, draft or archived, the words POST /hiring/projects/{id}/status/v1 takes (section 3.7.5), or other for any other state. |
description |
The project's own copy of the job description, exactly as stored (what you sent when you created the project, or in an edit), or null when there is none. It is not the text the job post shows: that is job_post.description. |
external_id |
Your own id for the project (section 3.5.11), or null. |
created_at, updated_at |
ISO 8601 times in UTC. updated_at is when the project's own row last changed. It is informational: no call asks for it as a condition (section 3.5.14). |
job_post |
The project's job post, or null when it has none. If an older project has several, the newest. Its fields are in the next table. |
assessments |
The assessments linked to the project, oldest first, at most 50. Each has id, assessment_type, status (a number) with status_label (the same in words), and external_id. assessment_type is ai_interview, role_play, cv_evaluation, skill_test, technical_test, showcase or recording, or unknown for a kind this API has no word for. It is never questionnaire: these are the rows that GET /hiring/interview-sets/v1 lists by default for the project (section 5.6), and that list leaves out question banks, every questionnaire (also a standalone one linked to the project) and the hidden helper question sets that other assessments keep for their own after-questions. To list your questionnaires, ask for them by name: GET /hiring/interview-sets/v1?assessment_type=questionnaire. |
job_post field |
What it holds |
|---|---|
id |
The job post, the id the routes of section 3.7 take. |
status |
The post's own switch: active (on), inactive (off) or other. It does not say whether the page opens: read public_state. |
public_state |
What the public page does now: live, awaiting_review, rejected, flagged, off or project_not_open, with the meanings of section 3.7.1. It is the page of the job post this view shows (the newest). In the answer to an edit that is normally the job post the call edited; it is another job post when a newer one exists, and another job post or none when the edited one was deleted (section 3.5.14). A job post that a check refused reads rejected, or project_not_open when its project is not open, or off when it has been switched off meanwhile (the order is in section 3.7.1). |
external_id |
Your own id for the job post (section 3.5.11), or null. |
title, description |
The job post's display text: the checked copy when the AI content check has written one, otherwise the words as stored on the job post (the AI's draft, or the words you sent in an edit). A returned description does not mean that candidates can see it: read public_state to see whether the public page is open. |
sent_title, sent_description |
The words as stored on the job post, before the content check could reword them: the AI's draft from when the project was created, or the words from the last edit (with leading and trailing blanks removed), or null when there are none. They are never the checked copy. Compare them with what you sent, after removing leading and trailing blanks from it, to confirm that your words landed, for example after a timeout or a 503 on an edit (section 3.5.14). |
review |
A best-effort reading of the result of the last AI content check. pending: not checked yet (a job post that has never been checked; in the answer to an edit it also means, for the job post that edit changed, that the check of the words it changed has not been seen to finish: section 3.5.14). approved: checked, and the checked copy is what the job post displays. rejected: refused (a job post that was never live does not open; a switched-on job post that has never been posted, for example one made in the dashboard and switched on later, is also taken offline by a refusal, and its public_state then reads rejected, or project_not_open when its project is not open, or off when it has been switched off meanwhile: section 3.7.1). flagged: a switched-on job post that had been posted before was live when a later check objected; it keeps serving on its existing link, is out of job boards and search, and a person at Flowmingo decides. On a read, review is not always about the current words: an edit does not reset the earlier result, so while the check of your new words is pending it can describe earlier words, and a job post that was already flagged or rejected before your edit can keep showing that earlier result for a while. And a check of older words that was still running when you edited can finish afterwards, after which the page can keep older checked wording while review reads approved; waiting and reading the project again does not change that. To know what the page shows, compare title and description (what it displays) with sent_title and sent_description (what you sent, trimmed). If they are not what you expect, change the words (even by one character; you can change them back afterwards) or change them in the dashboard: sending or saving the same words again starts no check (each change made through the edit route to the words of a switched-on job post uses one unit of the daily allowance: section 3.5.14). (In the answer to an edit, review is pending for the job post that edit changed, when it is the job post shown, until a check of the words that edit changed has been seen to finish. If a newer job post exists, or the edited one was deleted, the answer shows another job post, or none, with its own review: section 3.5.14.) These are this route's own words, not the review_status of the publication read (section 3.7.1), whose words are not_reviewed, approved and rejected. |
differs_from_sent |
true when either displayed field (title or description) differs from its stored value, which is what happens when the check reworded that field and the job post shows its checked copy. false when both displayed fields equal their stored values (the check kept the words, or has not written a copy yet). It does not say whether the current check has finished. An edit clears the checked copy of the field you changed only, so the other field can still carry an earlier checked copy, and differs_from_sent can stay true while the check of your new words is pending: read review too. It describes the whole of the job post the view shows, both fields, so it is where you see a rewrite of a field that your edit did not change (the warning job_post_rewritten_by_review of an edit speaks only of the fields that call changed: section 3.5.14). In the answer to an edit the view shows the job post the call edited in the normal case, but another job post when a newer one exists, and another job post or none when the edited one was deleted (section 3.5.14); a differs_from_sent read from another job post says nothing about the one you edited. After an edit, the stored words are the ones you sent, with leading and trailing blanks removed. |
| Status | code |
What to do |
|---|---|---|
400 |
(no code) | The id is not a UUID. |
403 |
(no code), or a code of section 11 | The key lacks the read_hiring scope, or has expired (no code), or is refused for a reason that has its own code (section 11). |
404 |
project_not_found |
One answer for an id that is not a project of yours, a project that was deleted, and a project in your organization that the key's creator cannot open (section 2.9). |
3.5.14 Edit a hiring project and its job post
POST /hiring/projects/{id}/update/v1 sets a project's title, position and description, and the words of its job post (job_post.title and job_post.description), to the values you send. Use it to keep a project in step with the job on your own platform, and to put your own wording on a job post: creating a project always has the AI draft the job post (section 3.5.2), so a job whose exact text matters is created first and then edited. Required API key scope: create_project. The answer shows the project only to a key that also holds read_hiring: with create_project alone, project is null and the answer is { id, changed, updated_at, project: null, warnings }, so a key that may not read a project does not learn its words through an edit (read it with a key that holds read_hiring, section 3.5.13). The answer is 200, also when nothing changed. The project id in the path may be written in capital or small letters; the id in the answer is in small letters.
It acts as the person who created the key, and that person must be an Owner or Admin of your organization. The role is read again on every call; during the grace period after a creator has left, the role they left with is used (section 2.10). The hiring project must be one that person can open (section 2.9). A key whose creator has left can still change the project's own fields while its grace period runs (section 2.10), but not the job post: a body that carries a job_post object, even an empty {} and even when it is the only thing in the body, is refused with 403 key_out_of_reach before anything is written, whatever role the creator left with, because the job post's words are saved with the person's access as it is today. The refusal depends on the key alone (its creator has left), not on what the job_post object holds, so an empty {} that would otherwise be 400 nothing_to_update is this 403 instead. An unknown project id still answers 404 first. A body without a job_post object still works until the grace period ends; after that the key stops working.
The job post's words are checked, and the check can reword them. The words you send in job_post.title and job_post.description are stored after leading and trailing blanks are removed from them, so compare the stored words with what you sent after trimming it. They then go through the same automatic content check that runs when a recruiter edits the same words in the dashboard. The check can reword the text or flag it, and the job post shows the checked wording, which is not always yours. After every edit of the job post's words, read job_post.differs_from_sent, job_post.review and the warnings of the answer (the first two are in the answer's project), or read the project again (section 3.5.13): differs_from_sent true means that a displayed field differs from the words stored, so the job post shows a checked copy that is not yours. It does not say whether the check of your new words has finished: read review too. When the call changed the job post's words, job_post.review in the answer is pending for the job post the call edited, when it is the job post the answer shows, until a check of those words has been seen to finish, also when the check cannot run at all (for example because the checking service is busy), and the warning job_post_awaiting_review is then made as well. The answer shows the project's newest job post, and differs_from_sent, public_state and review in its project.job_post describe that job post: the one the call edited in the normal case, another job post when a newer one exists (with its own values), and another job post or none when the edited one was deleted. The warnings are about the job post the call edited (when it was deleted, no warning about the check or the page is made). The warning job_post_rewritten_by_review is about the fields this call changed (the title and/or the description): a rewrite of a field it did not change, an older one for example, is not reported by it, and shows in the differs_from_sent of the job post the answer shows, which describes the whole job post. A later read reports the last check instead of the progress of this one: a job post that was already flagged or rejected before your edit can keep showing that earlier result for a while. review is a best-effort reading of the last check. A check of older words that was still running when you edited can finish afterwards, and the job post can then keep older checked wording while review reads approved. To know what the job post shows, compare title and description (what it displays) with sent_title and sent_description (what you sent, trimmed). If they are not what you expect, change the words (even by one character; you can change them back afterwards) or change them in the dashboard: sending or saving the same words again starts no check, here or in the dashboard, and a call whose words equal the stored ones writes nothing. Each such change made through this route to the words of a switched-on job post uses one unit of the daily allowance below, and changing them back is a second change that uses a second unit. This route cannot promise that a job post shows your exact wording.
Request payload.
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Optional | The name of the project, as your team sees it in the dashboard. 1 to 255 characters, without <, > or a NUL character (U+0000). |
position |
string | Optional | The role being hired for. 1 to 255 characters, without <, > or a NUL character. Changing it does not rewrite the job post. |
description |
string | Optional | The project's own copy of the job description, as text, up to 15000 characters, without a NUL character. It is not the text the job post shows: that is job_post.description. |
job_post |
object | Optional | The words of the job post: title and/or description, nothing else. |
job_post.title |
string | Optional | The title of the job post, as you want candidates to read it. 1 to 255 characters, without <, > or a NUL character. |
job_post.description |
string | Optional | The description of the job post, as text, up to 15000 characters, without a NUL character. The AI drafts nothing on this route: the words are yours, subject to the check above. |
Every field is optional, but send at least one: a body with none of the five answers 400 with the code nothing_to_update. Text is trimmed before it is checked. null and blank text are refused, because clearing a field is not supported, and so are a number or a list where text belongs, and an unpaired UTF-16 surrogate (half of an emoji, for example). A null for job_post itself is accepted and treated as if it were left out.
Fields this route does not take. external_id (fixed when the project and its job post are created: sending one is a 400, section 8.8), status (its own route: section 3.7.5), stages, assessments, idempotency_key, and the dashboard-only settings file_link, attributes, filters, priority, cv_score, source, location and locations are refused with 400 and the code project_field_not_supported when they carry a value, so that you never believe one was applied; a null in one of them is ignored. expected_updated_at is refused in the same way: this route has no version check, see below. A key inside job_post other than title and description is refused with 400 and the code job_post_field_not_supported. Any other top-level property is ignored without an error, so a misspelt field name is not reported.
What the call changes, and what it leaves alone.
- The request accepts only the text fields above, and the call does not intentionally change anything else: not the job post's switch or boost (use the publish, unpublish and boost routes: section 3.7), the project's status (section 3.7.5), your own ids, the project's stages or assessments, the job post's details (job functions, seniority levels, employment and workplace types, places, pay) or its screening questions. Those details are changed in the dashboard.
- Do not edit other settings of the same job post at the same moment. The job post's words are saved through the dashboard's own save, which writes back the other values it read a moment earlier. A change made to one of those other settings between that read and the save can therefore be overwritten. The condition behind
job_post_changed(below) protects the job post's title, description and switch only. - Editing the job post's title does not change the address of its public page.
- Nothing is sent to candidates and no webhook fires.
Sending the same body again. The call sets the fields you send to the values you send, as the dashboard's own save does. A value that already equals what is stored, once leading and trailing blanks are removed from what you sent, is not a change in itself. When no text differs, the answer says changed false, nothing is written, no new check is started and nothing is counted against the daily allowance below. When at least one project field you sent does differ, the call writes every project field you sent, also one that already equals what is stored, so that a value a colleague changed while the call was running becomes yours too. The last write wins: the call does not look at who changed the project last, and there is no version token (expected_updated_at is refused by name). updated_at in the answers is the project's ordinary timestamp; it is informational, so do not send it back and do not use it to tell whether the job post changed. Your own platform stays the source of truth: send one edit per job at a time. The one protection against overwriting a colleague is on the job post: if its words or its switch were changed between the moment the call read the job post and the moment it wrote, its words are not changed, and the call answers 409 with the code job_post_changed.
The title-only-draft marker. A job post made from a project's title alone can carry an internal note that its first description was drafted from the title. The call removes that note only when it changes the job post's description, as the dashboard does when a recruiter replaces the draft. If the removal fails, the words are saved and the warning job_post_draft_stamp_not_cleared says so. Sending the same description again does not remove the note and does not retry the removal, because nothing differs and so nothing is written: change the description again, or edit it in the dashboard. A read does not show the marker, so that warning and the messages of the two 503 answers named in the table below are the only places that tell you it may still be set.
Two writes, not one. The project fields (title, position, description) are written first, in one write, and the words of the job post second, through the dashboard's own save. They are not one transaction. Everything that can be checked without writing is checked before the first write, the job post's switch and the daily allowance below included, so a 400, a 403, a 404, a 409 project_has_no_job_post or job_post_is_off, a 429 job_post_edit_budget_reached or a 503 job_post_edit_budget_unavailable means that nothing was changed. If the job post step then fails, the project fields stay saved. The errors of that step (job_post_changed, job_post_not_saved and job_post_outcome_unknown) say which project fields were saved (every project field you sent, once any of them differed) and whether the job post's words were. If the project write itself ends in an error that does not say whether it went through, the call answers 503 with the code project_outcome_unknown: the project fields may or may not have been saved, and the job post was not attempted. If the edit was saved but its answer could not be built, the call answers 503 with the code edit_saved_answer_unavailable, and its message names the writes that were recorded. The answer is built after the writes and is about the job post that was edited: its review state and its warnings come from that job post, read again by its own id. If that job post is deleted between the save and the answer (an older project can hold several), no review state is compared and no warning about the check or the page is made, and for a key that can read the project, project.job_post is whatever a read of the project shows at that moment (the project's other job post, with its own differs_from_sent, public_state and review, or null). After either 503, after any other error and when the call times out, read the project (section 3.5.13) before you decide what to send again, and compare job_post.sent_title and job_post.sent_description (of the project's newest job post) with what you sent, after removing leading and trailing blanks from it: they are the stored words, never the checked copy, so they show whether your job post words landed. Reading needs the read_hiring scope, and a message that tells you to read the project says so when the key does not have it. When you were changing the job post's description, the message of job_post_outcome_unknown also says that, if the description was saved and the job post was drafted from its title only, the title-only-draft marker may still be set, and that sending the same description again will not clear it: change the description once more, or clear the marker in the dashboard. The message of edit_saved_answer_unavailable says the same when the marker could not be cleared. If no one else changed the same fields in between, sending the same body again leaves the values that were already saved as they are and tries the rest again. If someone else may be editing the same project, read it first: a repeat writes your values over theirs.
What the job post displays while the check runs.
- A job post that is switched on is checked at once, and the call waits for the check for up to about a minute. Give your client a timeout above 60 seconds when the body carries job post words (the call waits only when the words differ from the stored ones). A call that changes only project fields does not wait. Until the check is done, a job post that was approved before shows your new words unchecked, exactly as it does in the dashboard (one that has never been approved does not open until a check approves it: section 3.7.1). If the check cannot run (for example because the checking service is busy), the new words stay shown unchecked and
reviewin the answer sayspendingfor the job post the call edited, when it is the one the answer shows, until a check is seen to finish. A job post that has been live for a long time is not checked again by itself, exactly as in the dashboard: ifreviewis still not what you expect after a few minutes, change the words (even by one character; you can change them back afterwards) or change them in the dashboard, because saving the same words again starts no check. If the check approves them, its checked copy is what the page shows from then on (it may differ from your words:differs_from_sent). If the check refuses them, what happens depends on whether the job post has been posted before. A live job post that has been posted before is not taken down: it keeps serving, leaves job boards and search, andreviewreadsflagged. A live job post that has never been posted (it has no posting date, as with some job posts made in the dashboard and switched on later) is taken offline instead, andreviewreadsrejected; when you change the words of such a job post that was approved, the answer carries the warningjob_post_may_go_offline_if_rejected, on the conditions given below. A job post that had not been approved before readsrejectedinstead, and its page does not open (public_state, section 3.7.1). The two warnings that speak of a page being served,job_post_flagged_still_liveandjob_post_may_go_offline_if_rejected, are made only while the page of the job post this call edited is open when the answer is built (public_stateliveorflagged): never for a project that is a draft or archived, where nothing is being served, and not for a job post that was switched off after the save.job_post_may_go_offline_if_rejectedalso needs three things to have been true before the edit: the job post was switched on, had never been posted, and was approved. It is not made when a refusal came back within the wait: the job post'sreviewis thenrejectedand its page is not open (itspublic_stateisrejected, orproject_not_openwhen the project is not open, oroffwhen the job post was switched off meanwhile: section 3.7.1 gives the order), and the warningjob_post_review_rejectedcarries the verdict.public_statein the project view is the page of the job post the view shows: the job post the call edited in the normal case, another job post when a newer one exists, and another job post or none when the edited one was deleted. Such an edit uses a daily allowance, described next. - The words of a job post that is switched off or has expired are not edited by this call. A call that would change them answers
409with the codejob_post_is_offbefore anything is written, and the project fields of the same body are not written either. It applies to a job post whosestatusis notactive(section 3.5.13):inactive, which is switched off, andother, which includes an expired one. A call that changes only project fields, and a call whose job post words equal the stored ones, still work on such a job post. The reason: an edit clears the job post's checked copy of its words, nothing reliably checks the new words of a job post that is not switched on, and the publish route refuses to switch on an approved job post whose words were changed while it was off (409job_post_edited_while_off, section 3.7.2), so an edit of a switched-off job post could leave it stuck. To change the words, switch the job post on first (POST /hiring/job-posts/{id}/publish/v1, section 3.7.2) and then edit it, or edit it in the dashboard.
A daily allowance for edits that are checked by AI. An edit that changes the words of a job post is checked by AI at once (only a job post that is switched on can have its words edited here), and an organization may make 50 such edits per UTC day, counted across all its keys. The allowance has a counter of its own: the publish checks of section 3.7.2 have the same number but are counted separately. A call refused with job_post_is_off, an edit of project fields only and a call that changes no word do not use it. The allowance is taken after every check that needs no write and before anything is written, so a refused call changed nothing. A unit that was taken is not given back: an edit that then fails (for example with 409 job_post_changed, or with an unknown outcome) has used it. When the 50 are used, the call answers 429 with the code job_post_edit_budget_reached and a Retry-After header that holds the seconds until 00:00 UTC, when the count restarts (the message says the same in words). When the allowance cannot be read just now, the call answers 503 with the code job_post_edit_budget_unavailable, fails closed and changes nothing: try again in a moment. For a higher limit, write to contact@flowmingo.ai (section 8.5).
Sample request. This call renames the project and sets the words of its job post:
curl -s -X POST "$BASE/hiring/projects/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/update/v1" \
-H "X-API-Key: $FLOWMINGO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"title": "Support Engineer - Q1 hiring",
"job_post": {
"description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets, and you will work with our engineers to fix what you find."
}
}'
With a key that holds read_hiring as well as create_project, the answer is 200:
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"changed": true,
"updated_at": "2026-11-06T04:12:44.208Z",
"project": {
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Support Engineer - Q1 hiring",
"position": "Support Engineer",
"status": "active",
"description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets.",
"external_id": "REQ-48213",
"created_at": "2026-11-02T04:18:44.051Z",
"updated_at": "2026-11-06T04:12:44.208Z",
"job_post": {
"id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"status": "active",
"public_state": "live",
"external_id": "POST-48213",
"title": "Support Engineer",
"description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets, and work with our engineers to fix the problems you find.",
"sent_title": "Support Engineer",
"sent_description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets, and you will work with our engineers to fix what you find.",
"review": "approved",
"differs_from_sent": true
},
"assessments": [
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"assessment_type": "ai_interview",
"status": 1,
"status_label": "active",
"external_id": null
}
]
},
"warnings": ["job_post_text_may_be_rewritten", "job_post_rewritten_by_review"]
}
The project's title was saved as sent. The check reworded the job post's description, so differs_from_sent is true, job_post.description is the checked wording, and job_post.sent_description is the sentence that was sent. project.description is the project's own copy and was not part of this call. The job post's page stayed open (public_state live) while its new words were checked, as that of a live job post that has been posted before does. The answer has the fields of section 3.5.13 inside project, plus id, changed (false when nothing differed), updated_at (the same as project.updated_at) and warnings.
With a key that holds create_project but not read_hiring, the edit is made in the same way and the warnings follow the same rules, but project is null. This example is of an edit after which the check had approved the words as sent, so only the first warning is made:
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"changed": true,
"updated_at": "2026-11-06T04:12:44.208Z",
"project": null,
"warnings": ["job_post_text_may_be_rewritten"]
}
Such a key cannot see whether the check reworded the words, except through the warnings that name it; read the project with a key that holds read_hiring (section 3.5.13). job_post_rewritten_by_review speaks only of the title and the description that this call changed, so it tells such a key nothing about a field it did not send. The warnings are made from the job post that was edited, not from the project view, so this key is given them too, the two that speak of a page being served included, while the page of that job post is open when the answer is built; it cannot read public_state from the project or from the publication read (section 3.7.1) to check, because those need read_hiring.
Warnings. warnings lists things worth knowing that did not stop the call. More codes may be added: ignore one you do not know.
| Code | What it means |
|---|---|
job_post_text_may_be_rewritten |
The job post's words changed, so the AI content check may reword them and the job post displays its checked copy. It is in every answer in which they changed. Read job_post.differs_from_sent. |
job_post_awaiting_review |
A check of the new words had not been seen to finish when the call answered, and review reads pending (also when the check cannot run). It states the progress of the check, not the state of a page, so it is made whatever the page does: a job post that was switched off right after the save can still carry it while its check is pending. At the time of the call the job post was switched on, because the words of one that is not are not edited here (job_post_is_off). A later read reports the last check, not this progress. See the caution under this table. |
job_post_rewritten_by_review |
The check approved the job post and reworded the title and/or the description that this call changed: the job post displays the checked wording of that field, and differs_from_sent is true. A rewrite of a field this call did not change is not reported by it, whether it is an older one or one that this same check has just produced for the other field. Such a rewrite shows in the differs_from_sent of the job post the answer shows, which describes the whole job post (when that is another job post than the one you edited, it says nothing about yours): compare title and description with sent_title and sent_description (section 3.5.13). |
job_post_flagged_still_live |
The job post was live and had been posted before, and the last check flagged it (that check can concern earlier words: see the caution under this table). It keeps serving on its existing link, is out of job boards and search, and a person at Flowmingo decides (review flagged). It is made only while the page of the job post this call edited is open when the answer is built (public_state live or flagged): never for a project that is a draft or archived, and not for a job post that has been switched off since the save, because nothing is being served then. |
job_post_review_rejected |
The job post was switched on when you made the call, and the last check refused it (review rejected; that check can concern earlier words: see the caution under this table). It is the verdict of the check and says nothing about a page, so it is made whatever the page does. Read public_state in the project (section 3.7.1; reading needs read_hiring) to see whether the page is open, then change the words (even by one character; you can change them back afterwards; each change made through this route uses a unit of the daily allowance) or change them in the dashboard: a new check starts only when the words change (see the caution under this table). |
job_post_may_go_offline_if_rejected |
The job post was switched on and approved, had never been posted (it has no posting date, as with some job posts made in the dashboard and switched on later), and this call changed its words. If the check refuses the new words, this job post is taken offline (review rejected) instead of being flagged and kept serving, as one that has been posted before would be (job_post_flagged_still_live). The warning is made only while the page of the job post this call edited is open when the answer is built (public_state live or flagged, section 3.7.1): never for a project that is a draft or archived, where nothing is being served, and not for a job post switched off after the save. It is not made when the check refused the words within the wait: the page is then not open (public_state rejected, or project_not_open when the project is not open, or off when the job post was switched off meanwhile) and job_post_review_rejected carries the verdict. review and public_state say how the check ended. |
job_post_read_back_delayed |
The words were saved, but the dashboard's save could not read them back at once. The answer shows the saved state. |
job_post_draft_stamp_not_cleared |
The words were saved. The call changed the job post's description, but an internal marker, which says that the description was first drafted from the job's title alone, could not be cleared. Sending the same description again does not retry it, because nothing differs: change the description again, or edit it in the dashboard. |
The page warnings follow the page. job_post_flagged_still_live and job_post_may_go_offline_if_rejected are made only while the page of the job post this call edited is open when the answer is built. For example, when you change the words of a switched-on job post in an archived project and the check flags them, review reads flagged and public_state reads project_not_open, and job_post_flagged_still_live is not among the warnings, because nothing is being served. job_post_review_rejected and job_post_awaiting_review are different: they state the verdict or the progress of the check, so they are made whatever the page does. A job post switched off right after the save can therefore still carry job_post_awaiting_review while its check is pending.
A caution that has no warning of its own. review is a best-effort reading of the last check. A check of older words that was still running when you edited can finish afterwards, and the job post can then keep older checked wording while review reads approved and no warning appears. The same older check can be the one that flagged or refused the job post. To know what the job post shows, compare title and description (what it displays) with sent_title and sent_description (what you sent, trimmed). If they are not what you expect, change the words (even by one character; you can change them back afterwards) or change them in the dashboard: sending or saving the same words again starts no check, and a call whose words equal the stored ones writes nothing. A blank at the beginning or the end is not a change, because blanks there are removed first. Each change made through this route to the words of a switched-on job post uses one unit of the daily allowance below, and changing them back is a second change that uses a second unit.
Errors on this route. Every error has the shape of section 8.4. Branch on code. A message that tells you to read the project says that reading needs the read_hiring scope when the key does not have it.
| Status | code |
What to do |
|---|---|---|
400 |
(no code) | The id is not a UUID, or a value is refused: title, position or job_post.title is blank, over 255 characters, or contains <, >, a NUL character or an unpaired surrogate; description or job_post.description is blank, over 15000 characters, or contains a NUL character or an unpaired surrogate; a value is null or is not text. message names the field. Nothing was changed. |
400 |
nothing_to_update |
None of title, position, description, job_post.title and job_post.description was sent. An empty job_post object {} counts as none (for a key whose creator has left the company, during the grace period, it is the 403 key_out_of_reach below instead). Nothing was changed. |
400 |
project_field_not_supported |
The body carries a field this route does not take, such as external_id, status or expected_updated_at. Remove it. Nothing was changed. |
400 |
job_post_field_not_supported |
job_post carries a key other than title and description. Remove it. Nothing was changed. |
400 |
job_post_pay_range_invalid |
The job post's saved pay range has a minimum above its maximum, so its words cannot be edited until that is corrected in the dashboard. Nothing was changed. |
400 |
api_key_has_no_acting_user |
The key has no user and no creator on record. Create the key from a signed-in account. |
403 |
(no code), or a code of section 11 | The key lacks the create_project scope, or has expired (no code), or is refused for a reason that has its own code (section 11). |
403 |
project_create_requires_admin |
The person who created the key is not an Owner or Admin of the organization (any more). During the grace period after a creator has left, the role they left with is used. |
403 |
key_out_of_reach |
The project is one the key's creator cannot open (section 2.9). Or the body carries job post words (job_post.title or job_post.description) and the creator's role, as it is now, no longer lets them edit the job post. Or the body carries a job_post object, even an empty {}, and the creator has left the company: during the grace period after a creator has left (section 2.10) the key can still edit the project's own fields, but any body with a job_post object is refused with this code before anything is written, decided from the key itself, whatever role the creator left with (it comes after the 404 for an unknown project and before nothing_to_update). Nothing was written. |
404 |
project_not_found |
No such hiring project in your organization. |
409 |
project_has_no_job_post |
The body carries job post words (job_post.title or job_post.description) and the project has no job post to edit. Nothing was changed. |
409 |
job_post_is_off |
The call would change the words of a job post that is switched off or has expired (its status is not active). Nothing was written, and the project fields of the same body were not written either. Switch the job post on first (section 3.7.2) and then edit it, or edit it in the dashboard. A call with project fields only, or with job post words equal to the stored ones, is not refused. |
409 |
job_post_changed |
The job post was edited, or switched on or off, between the moment the call read it and the moment it wrote. Its words were not changed. The message says which project fields of the same call were saved. Read the project (section 3.5.13) and send the job_post part again. |
429 |
job_post_edit_budget_reached |
The organization has used its 50 AI-checked edits of job post words for today (UTC). Nothing was changed. Wait the seconds in the Retry-After header, or until 00:00 UTC, then send the same body again. |
500 |
job_post_not_saved |
The dashboard's save refused the job post's words before writing anything. The message says which project fields of the call were saved, if any. Read the project (section 3.5.13), then send the same body again if no one has changed the same fields in the meantime. |
503 |
job_post_edit_budget_unavailable |
The daily allowance could not be read just now. Nothing was changed. Try again in a moment. |
503 |
project_outcome_unknown |
The project write ended in an error that does not say whether it went through: the project fields may or may not have been saved, and the job post was not attempted. Read the project (section 3.5.13) to see its state, then send the same body again if it is still needed and no one has changed the same fields in the meantime. |
503 |
job_post_outcome_unknown |
The job post's words may or may not have been saved. The message says which project fields were saved. Read the project (section 3.5.13) and compare job_post.sent_title and job_post.sent_description (of the project's newest job post) with what you sent, after removing leading and trailing blanks from it, then send the same body again if it is still needed and no one has changed the same fields in the meantime. When you were changing the description, the message also says that the title-only-draft marker may still be set if the description was saved, and that sending the same description again will not clear it. |
503 |
edit_saved_answer_unavailable |
The edit was saved, but its answer could not be built. The message names what was written, and says so when the title-only-draft marker could not be cleared (sending the same description again will not retry it). Read the project to confirm (reading needs read_hiring); job_post.sent_title and job_post.sent_description are the words as stored, after leading and trailing blanks were removed from what you sent. |
The limits on requests per key of section 8.5 apply to this route like any other, and so does the daily allowance above.
3.6 Create an assessment with the AI
Six routes each make one assessment from a job title, the way the dashboard's own wizard does: the AI writes the content, the assessment is published, and it is ready to send to candidates. Use them when your system knows the job and not the questions. To write the questions yourself, use POST /interview/set/v1 (sections 3.1 to 3.4). To open a role with its assessments in one call, use the project route (section 3.5.5). Required API key scope for all of them: create_assessment. Each takes an optional external_id, your own id for the assessment (section 3.6.4).
| Operation | Makes | Units (section 3.6.11) |
|---|---|---|
POST /assessments/cv-evaluation/v1 |
A CV Evaluation (section 3.6.5) | 1 |
POST /assessments/ai-interview/v1 |
An AI Interview (section 3.6.6) | 2 |
POST /assessments/role-play/v1 |
A Role-play (section 3.6.7) | 2 |
POST /assessments/skill-test/v1 |
A Skill Test (section 3.6.8) | 3 |
POST /assessments/showcase/v1 |
A Showcase (section 3.6.9) | 1 |
POST /assessments/technical-test/v1 |
A Technical Test (section 3.6.10) | 6 |
GET /assessments/requests/{idempotency_key}/v1 |
Reads where a request has got to (section 3.6.3) | none |
Every path is relative to https://apis.flowmingo.ai/company/integration. The AI Interview, the Role-play and the Showcase usually take about half a minute, a Skill Test about a minute and a Technical Test 3 to 9 minutes (the figures of the OpenAPI document); a CV Evaluation is one AI call.
3.6.1 What you need
- The
create_assessmentscope (section 2.3). A key does not gain it automatically: tick Create assessments on the key. - Two people are checked, and each must be an Owner or Admin of your organization at the moment of each call. One is the person who created the key. The other is the person the assessment is made as: the user the key is bound to, when it has one, otherwise the person who created the key (the two can be different people). The project route (section 3.5.5) makes its assessments as the person it makes the project as, the creator of the key. The roles are read again on every call, so a key whose creator was demoted stops working here at once, and so does a key whose creator has left, during the grace period that follows (section 2.10). A failed check of either person answers
403with the codeassessment_create_requires_admin, so the code alone does not say which of the two failed; after the grace period the key is refused before it reaches these routes (section 2.10). If the role cannot be confirmed just now the answer is503assessment_create_role_unconfirmed; a key with no creator on record is refused with a403before anything runs (section 11). Nothing is created in any of these. - Each assessment costs AI generation, so your organization has a per-minute and a per-day budget, counted across all its keys (section 3.6.11).
- The assessment is ready to use. It is published, attached to the project if you named one, and made with the dashboard's own defaults. The dashboard-only settings named in section 3.6.4 are refused; other properties that are not listed are ignored.
3.6.2 How a call works: keys, replays and waiting
All six routes work the same way.
| Situation | Answer |
|---|---|
| The assessment is ready within about 45 seconds | 201 with the assessment and idempotent_replay false. |
| It is not ready by then | 202 with a Retry-After header (10 seconds) and a short body. The work carries on in the background. |
The same idempotency_key and the same body, after the assessment was made |
200 with the same assessment and idempotent_replay true. Nothing new is made, and it costs nothing. |
The same idempotency_key with a different body (a different external_id included), or for another kind |
409 idempotency_key_reused. Nothing is created. |
| The request fails | An error status with a code (section 3.6.12). |
idempotency_keyis required. It is a token you choose, 8 to 128 characters fromA-Z a-z 0-9 . _ : -, for example the id of the job in your own system plus the kind (ats-job-48213-cv). It belongs to your organization, whichever of your API keys sends it, and it is remembered for good. Use one key for each assessment you want.- The same body means the same request once the defaults are filled in: a field you send with the value its default has (such as
languageen, orduration_minutes20) is the same request as leaving it out (nameandinterview_languagesare the exceptions: sending the name the dashboard would have given, or["English"], is a different request from leaving them out), text is trimmed, and ajob_descriptionshorter than 50 characters counts as none. Anexternal_idis part of the body: another one, one added or one dropped is a different request, and sendingnullis the same request as leaving it out. A request you stored before this field existed and send again unchanged is still a replay, never a409. The answer to a repeat is read fresh, so it shows the assessment as it is now (one deleted in the dashboard readsstatusdeleted,readyfalseandexternal_idnull). - Two calls with the same key at the same moment make one assessment. The second waits for the first one's result and answers it.
- Resuming can cost again. Repeating a request that is still pending or has failed resumes it and can take units again (section 3.6.11); it can then answer
201,202or an error instead of200. - After a
202, ask again with the same key, never with a new one (a new key makes a second assessment). Wait the seconds theRetry-Afterheader says, then either send the same request again (each repeat waits up to about 45 seconds itself) or read the status route (section 3.6.3), until the status isready. - To wait less, send a
Preferheader.Prefer: respond-asyncmakes the call answer at once with where the assessment has got to (a202like the one above, with aRetry-After; an assessment that is already finished is answered as usual,200or201), andPrefer: wait=20waits at most 20 seconds (a longer wait is cut to the usual wait of about 45 seconds). Use it when your caller gives up early, for example a no-code step that times out at 30 seconds. The answer carriesPreference-Applied(respond-asyncorwait=<seconds>) when it was applied; without the header, or with one that cannot be read, nothing changes. It is not part of the request, so the sameidempotency_keycan be sent again with a different preference (or none). After a202that came from aPrefer, follow the assessment with the status read (section 3.6.3), not by sending the request again: a repeated request with a short wait restarts a failed attempt each time, which can cost again, and it answers202before it can say why the attempt failed, while the status read reports the failure. A wait of 0 still includes sign-in, validation and the budget check, so give your caller a few seconds more than your own network needs. It applies to the six assessment routes of this section.POST /hiring/projects/v1takes it too, for the assessments it makes (section 3.5.5); it drafts the job post before it answers, and the header does not shorten that.
The 202 body:
{
"status": "pending",
"kind": "technical_test",
"idempotency_key": "ats-job-48213-tt",
"message": "The assessment is still being made. Nothing is lost: ask again with the same idempotency_key (send the same request, or read GET /integration/assessments/requests/{idempotency_key}/v1) until its status is ready.",
"retry_after_seconds": 10
}
A loop that sends the request until it stops answering 202:
const BASE = 'https://apis.flowmingo.ai/company/integration'
const HEADERS = { 'Content-Type': 'application/json', 'X-API-Key': process.env.FLOWMINGO_API_KEY }
// Every repeat has the same body and the same idempotency_key, so it never makes a second assessment.
async function createAssessment(route, body, giveUpAfterMs = 15 * 60 * 1000) {
const startedAt = Date.now()
for (;;) {
const res = await fetch(`${BASE}/assessments/${route}/v1`, { method: 'POST', headers: HEADERS, body: JSON.stringify(body) })
if (res.status !== 202) return { status: res.status, body: await res.json() } // 201, 200, or an error with a code
if (Date.now() - startedAt > giveUpAfterMs) throw new Error('still being made: read it later with the status route')
const seconds = Number(res.headers.get('Retry-After')) || 10
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
}
3.6.3 Read where a request has got to
GET /assessments/requests/{idempotency_key}/v1 answers where a create call has got to. Use it to follow a call that answered 202. It only reads: it never starts or restarts any work. Any key of your organization can read a request that another key made, and, like the create routes, it needs a key whose creator is an Owner or Admin (section 3.6.1).
| Field | What it holds |
|---|---|
idempotency_key |
The key the request was made with. |
kind |
ai_interview, cv_evaluation, skill_test, showcase, technical_test or role_play. |
status |
pending while it is being made, ready when the creation completed, failed when it could not be made. Whether the assessment is usable now is in assessment.status and assessment.ready, which change if someone deletes or archives it later. |
project_id |
The project it was to be attached to, or null. |
assessment |
Once status is ready: the assessment, in the shape its create route answers with, external_id included. null before. |
failure |
When status is failed: code, message and retryable. null otherwise. retryable true means that sending the original request again with the same idempotency_key can finish it (section 3.6.12). |
retry_after_seconds |
10, while it is pending or failed with retryable true. Absent otherwise. |
A key that no request of your organization used answers 404 with the code assessment_request_not_found (a key that could never have been accepted, such as one with the wrong characters, answers the same). A request that was made for a project that the key's creator can no longer open (the project was archived or deleted, or taken out of their access) answers 404 project_not_found. Here that does not mean nothing was made: the request exists, and an assessment may have been made for it. The message says so: "This request was made for a project that this API key can no longer open (it may have been archived or deleted, or taken out of the key's access), so its status is not shown. The request exists and an assessment may have been made for it. This API key may not be able to read the assessments list, so ask a colleague who can open it (an Owner, for example) to check it there." Reading the assessments list needs the read_hiring scope, and a key whose creator is limited to some projects sees only the assessments of those projects, so a colleague who can open every project is the one who can look; if you sent an external_id, GET /hiring/interview-sets/v1?external_id= finds the assessment there. The six create routes answer the same code and status for a project_id they cannot use, and there it is true that nothing was created.
{
"idempotency_key": "ats-job-48213-tt",
"kind": "technical_test",
"status": "pending",
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"assessment": null,
"failure": null,
"retry_after_seconds": 10
}
3.6.4 Fields every route takes
Names are snake_case. Text is trimmed before it is checked.
| Field | Type | Required | Description |
|---|---|---|---|
idempotency_key |
string | Yes | See section 3.6.2. |
job_title |
string | Yes | The job the assessment is for. It names the assessment, and its content is written from it. 2 to 255 characters (2 to 200 for a Skill Test, a Showcase and a Technical Test), without <, > or a NUL character (U+0000). |
job_description |
string or null |
Optional | The job description as text, so the content fits the job better. Up to 15,000 characters (10,000 for a Skill Test). Under 50 characters it is ignored, and the content comes from the job title alone. Only the first part of a long one reaches the AI: up to the first 10,000 characters for a CV Evaluation, up to the first 9,600 for an AI Interview, a Role-play and a Showcase, much less for a Skill Test and a Technical Test. |
name |
string or null |
Optional | The name of the assessment, as your team sees it. Default: the job title, a dash and the kind, as the dashboard names one, with today's date in UTC for every kind except a Skill Test and a Technical Test, for example Senior Backend Engineer – CV Evaluation, 22 Oct '26 or Data Analyst – Test. Up to 1,024 characters (255 for an AI Interview and a Role-play, 200 for a Skill Test and a Technical Test), without < or >. |
project_id |
string (UUID) or null |
Optional | Attach the assessment to this hiring project (see below; section 5.7 shows how to find its id). |
external_id |
string or null |
Optional | Your own id for the assessment: plain text, 1 to 128 characters, stored exactly as you send it (see below, and section 8.8). |
external_id. It follows the rules of section 8.8: not trimmed, scoped to your company, not unique, and fixed when the assessment is made. The camelCase externalId is not read. A value that breaks the rules is a 400 with no code and the message "external_id must be plain text of 1 to 128 characters with no control characters. It is stored exactly as sent."; nothing is created. Every answer of the route carries external_id (your id exactly as stored, or null when you sent none, and null once the assessment has been deleted), and so do the status read (section 3.6.3) and the assessments list, which ?external_id= filters. The id is written a moment after the assessment exists, inside the call, so no answer of the call shows the assessment without it. It is part of the request (section 3.6.2) and it is not the idempotency_key. An assessment that a create-project call makes (section 3.5.5) gets no id.
project_id. It must be an active hiring project of your organization that the key's creator can use (a project-limited Admin can use only the projects they were given), and that the person the assessment is made as (section 3.6.1) can use too; a project that person cannot use answers the same 404 as a project that is not active. The answer then says linked true. If the creator is limited to some projects, any project outside theirs, another organization's and a deleted project included, answers 403 with the code key_out_of_reach ("The person who created this API key does not have access to that hiring project ...", section 11) before anything is made or any AI is called. A project the creator may name but that is not an active project of your organization (archived, or, for a creator with access to all hiring projects, deleted or another organization's) answers 404 with the code project_not_found. Either way nothing is created; a value that is not a UUID answers 400 with no code. A creator limited to some projects can still make an assessment that names no project. If the assessment was made but could not be attached, the call fails with 502 assessment_project_link_failed, which sending the same request again repairs.
Send project_id when you make a Skill Test or a Technical Test. The invite route sends these two only through a hiring project the test is attached to (section 4.6.1). A test made without project_id is not attached to one, so inviting a candidate to it answers 400 with the code project_required (or 404 if you name a project it is not attached to). No route of the API attaches an existing test to a project afterwards: attach it in the Flowmingo dashboard, or make a new one with project_id (or with the project route, section 3.5.5, which attaches the tests it makes).
Three kinds are scored on criteria that you can supply: a CV Evaluation, an AI Interview and a Role-play. A criterion has this shape:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | What the candidate is scored on, in a few words (for example 5+ years of Go). 1 to 255 characters. |
details |
string or null |
Optional | A sentence of detail for the scorer, up to 1,000 characters. |
importance |
string | Optional | How much it weighs: good_to_have (the default), important, very_important or must_have. These are the dashboard's Good to Have, Important, Very Important and Must Have. |
Report language. Where a route takes language, it is a language tag the dashboard lists (en, vi-VN, th-TH, ja-JP and so on) or the bare language (vi finds vi-VN), with the default en. A language that is not supported answers 400 with the code language_not_supported, and its message lists the accepted tags.
Options these routes refuse. The routes read only the fields documented here. A value in any of features, addons, attributes, com_accesses, pdf_url, file_link, status, set_type, id, priority or interview_duration is refused with 400 and the code assessment_field_not_supported, so you are never left believing it was applied (a null is ignored). The Technical Test refuses more, listed in section 3.6.10. Any other property is ignored without an error.
3.6.5 CV Evaluation
POST /assessments/cv-evaluation/v1 makes a CV Evaluation: a set of criteria that candidates' CVs are scored against, published and ready to use. It takes the fields of section 3.6.4 and:
| Field | Type | Required | Description |
|---|---|---|---|
criteria |
list of criteria | Optional | 1 to 20 criteria (section 3.6.4). Leave it out and the AI writes them from the job, which costs one AI call; send them and no AI call is made. |
language |
string or null |
Optional | The language the evaluation reports are written in. Default en. |
- A project can have only one CV Evaluation. With a
project_idthat already has one, the call answers409with the codeproject_already_has_cv_evaluationand nothing is made. - The free CV report that candidates may get after applying follows your organization's own setting. It is on for a new CV Evaluation, but the email goes out only when someone in your organization has accepted the notice in the dashboard (the answer says whether they have:
notice_acknowledged). This route never accepts it for you. - The answer adds
report_language,criteria(each withid,title,detailsandimportance),criteria_countandcomplimentary_cv_evaluation(enabledandnotice_acknowledged). A full sample is in section 3.6.13.
3.6.6 AI Interview
POST /assessments/ai-interview/v1 makes an AI Interview the way the dashboard's wizard does: the AI writes the criteria and the interview questions from the job title (and the description), and the interview is published, ready to send to candidates. Everything else takes the dashboard's defaults: 20 minutes, English, the candidate uploads a CV first, a public link, the free tier with no paid options. It takes the fields of section 3.6.4 and:
| Field | Type | Required | Description |
|---|---|---|---|
criteria |
list of criteria | Optional | 1 to 20 criteria (section 3.6.4). The questions are then written against yours. Leave it out and the AI writes the criteria. |
duration_minutes |
integer | Optional | How long the interview lasts, 1 to 60. Default 20. It steers how many questions are written. Your plan has its own limit: a longer interview answers 400 with the code assessment.length_exceeds_plan and nothing is created. |
interview_languages |
list of text | Optional | The languages a candidate can choose to be interviewed in, 1 to 5, by the names the dashboard shows (English, Vietnamese). Letter case does not matter. Default ["English"]. A name the dashboard does not offer answers 400 with the code interview_language_not_supported, and its message lists the accepted names. This does not change the language the questions are written in, which follows the job title and description. |
language |
string or null |
Optional | The language the evaluation reports are written in. Default en. It is fixed once the interview is made. |
- If the project's job post already has auto-invite switched on, the interview becomes the one it invites with, as in the dashboard.
- The answer adds
duration_minutes,interview_languages,report_language,criteria,criteria_countandquestions(interview, the questions the AI interviewer asks, andpre_screening, the ones the candidate answers first, such as the CV upload). A sample is in section 3.5.9.
3.6.7 Role-play
POST /assessments/role-play/v1 makes a Role-play: the candidate plays themselves in a situation, an AI plays a character, and the conversation is scored. The AI writes the criteria and one 15-minute scenario with its character from the job title (and the description). It is published and ready to send to candidates, with the dashboard's defaults: English, the free tier, a public link. It takes the fields of section 3.6.4 and:
| Field | Type | Required | Description |
|---|---|---|---|
criteria |
list of criteria | Optional | 1 to 20 criteria (section 3.6.4). The scenario is then written against yours. |
scenario |
string or null |
Optional | The situation the candidate reads, in the second person ("You are..."), up to 4,000 characters. Send it together with character, or leave both out. |
character |
string or null |
Optional | The character the AI plays, written for the AI ("The AI plays..."), up to 4,000 characters. Send it together with scenario. No AI call writes the scene when you send both. |
language |
string or null |
Optional | The language the evaluation reports and the scenario are written in. Default en. It is fixed once the Role-play is made. |
- Sending only one of
scenarioandcharacteranswers400with the coderole_play_scene_invalid; a text over 4,000 characters is a400with no code. - The character is the answer key. Candidates never see it and the API never returns it.
- The answer adds
duration_minutes(15),report_language,interview_languages,scenario,scenes_count,criteriaandcriteria_count:
{
"id": "4c9d1e2f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"type": "role_play",
"name": "Data Analyst — Role-play, 22 Oct '26",
"job_title": "Data Analyst",
"external_id": null,
"status": "active",
"ready": true,
"duration_minutes": 15,
"report_language": "en",
"interview_languages": ["English"],
"scenario": "You are a Data Analyst at Northstar Metrics, a company that provides analytics services to subscription businesses. You are meeting Daniel Reed, Growth Director…",
"scenes_count": 1,
"criteria": [ ... ],
"criteria_count": 5,
"project_id": null,
"linked": false,
"idempotent_replay": false
}
Here scenario and criteria are cut; the criteria have the shape of section 3.6.5.
3.6.8 Skill Test
POST /assessments/skill-test/v1 makes a Skill Test the way the dashboard's automatic path does: the AI writes one fixed multiple-choice paper from the job title (and the description), it is approved and published, and it is ready to invite candidates to. Every candidate gets the same questions, in a shuffled order. The defaults are 30 questions, 20 minutes, medium difficulty and strict exam security. It takes the fields of section 3.6.4 and:
| Field | Type | Required | Description |
|---|---|---|---|
question_count |
integer | Optional | How many multiple-choice questions, 1 to 40. Default 30. |
duration_minutes |
integer | Optional | The time a candidate has, 1 to 180. Default 20. Your plan has its own limit: a longer test answers 400 with the code assessment.length_exceeds_plan and nothing is created. |
difficulty |
string | Optional | easy, medium, hard or very_hard. Default medium. |
exam_security |
string | Optional | strict (the default) records camera and screen and flags leaving the page; standard watches less. |
- There is no
languagefield: the questions follow the job title and description. Whatevernameyou give, the questions are still written forjob_title. - Send
project_idif you mean to invite candidates to this test through the API: a Skill Test is sent only through a hiring project it is attached to (section 3.6.4). - The answer adds
question_count,duration_minutes,difficulty,exam_securityandconfig_revision(the test's stored configuration revision; it is not a counter of every edit). Itsstatusispublishedwhen the paper is published and usable,draftwhile it is being made,archivedordeletedafter the dashboard changed it:
{
"id": "8e7a1c52-3d4b-4f90-a6e1-2b9c5d7f0a13",
"type": "skill_test",
"name": "Data Analyst – Test",
"job_title": "Data Analyst",
"external_id": null,
"status": "published",
"ready": true,
"question_count": 30,
"duration_minutes": 20,
"difficulty": "medium",
"exam_security": "strict",
"config_revision": 4,
"project_id": null,
"linked": false,
"idempotent_replay": false
}
3.6.9 Showcase
POST /assessments/showcase/v1 makes a Showcase: candidates record themselves (screen, camera and microphone) doing a few tasks, each with a time limit, and nobody scores it with criteria. By default the AI is asked for two tasks of ten minutes each from the job title (or from the description when you send one); the result can have fewer, so read tasks_count. The Showcase is published, ready to invite candidates to. It takes the fields of section 3.6.4 and:
| Field | Type | Required | Description |
|---|---|---|---|
tasks |
list of tasks | Optional | Your own tasks, 1 to 10, in the order candidates do them. Leave it out and the AI is asked for two. When you send tasks no AI call is made, and job_description and language are ignored. |
language |
string or null |
Optional | The language the AI writes the tasks in. Default en. |
A task has:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | The task the candidate reads before recording, 1 to 1,000 characters. No two tasks may have the same title. |
content |
string or null |
Optional | A longer brief, up to 5,000 characters. |
max_seconds |
integer | Optional | The most the candidate may record for this task, 60 to 1,200 seconds. Default 600. |
- The tasks together may record at most 3,600 seconds, and no more than your plan allows. A repeated title or too much recording time answers
400with the codeshowcase_tasks_invalid; a total over your plan's limit answers400assessment.length_exceeds_plan. Nothing is created. - A Showcase can be sent through the invite route, by email address (section 4.8).
- It starts with the dashboard wizard's defaults. A Showcase made through the API has an intro message for the candidate and the CV upload question, like one made in the dashboard wizard.
- The answer adds
tasks(each withid,title,content,max_secondsandpriority, the order candidates do them in),tasks_countandtotal_seconds. Itsstatusisactivewhen it is published and usable,draftwhile it is being made,inactiveordeleted:
{
"id": "6b3a9f10-2c4d-4e5f-9a7b-8c1d2e3f4a5b",
"type": "showcase",
"name": "Data Analyst – Showcase, 22 Oct '26",
"job_title": "Data Analyst",
"external_id": null,
"status": "active",
"ready": true,
"tasks": [
{
"id": "3b8e2a64-7c1d-4f0e-9a52-1d6f0c9b7e21",
"title": "Open a growth dashboard you built and walk through its weekly reporting.",
"content": "Open a dashboard you created for growth reporting, …",
"max_seconds": 600,
"priority": 1
}
],
"tasks_count": 2,
"total_seconds": 1200,
"project_id": null,
"linked": false,
"idempotent_replay": false
}
Here tasks is cut to its first entry of two (tasks_count), and the text of the task is cut.
3.6.10 Technical Test
POST /assessments/technical-test/v1 makes a Technical Test: a few coding problems that the AI writes and then proves, by running its own reference solution against its own test cases in a code sandbox (a problem that fails the check is thrown away). The test is then published, and attached to the hiring project you name in project_id, which is what lets you invite candidates to it (see "Create it inside a hiring project" below). The defaults are two medium problems, one of them a debugging problem, in Python 3, JavaScript and Java, with AI assistance not allowed, and a time derived from the problems. It takes the fields of section 3.6.4 and:
| Field | Type | Required | Description |
|---|---|---|---|
languages |
list of text | Optional | The programming languages a candidate can solve the problems in, up to 4 of python3, javascript, typescript, go, java, cpp, ruby, php, rust, csharp, swift, kotlin and scala. Leaving it out, null or [] selects the default, python3, javascript, java. Generation attempts every language you name, and every extra language costs time, but the usable result can offer fewer: read languages in the answer. A language you name that the test does not offer, or that is known to be unavailable right now, is refused on a new request with 400 and the code technical_test_language_not_supported. |
skills |
list of text | Optional | Up to 6 skills the problems should probe (for example concurrency, SQL), each up to 100 characters without < or >. Leave it out and the problems test general problem solving for the job. |
difficulty |
string | Optional | easy, medium, hard or very_hard. Default medium. Harder problems are given more time. |
problem_count |
integer | Optional | How many problems, 1 to 6. Default 2. A test can end with fewer than you asked for, because a problem that fails the sandbox check is thrown away: the answer says how many were kept. |
include_debugging_problem |
boolean | Optional | Whether one of the problems is a debugging problem (broken starter code the candidate must fix). Default true. It is the last of the problems, not an extra one: with problem_count 1 the only problem is the debugging one. |
ai_assistance_allowed |
boolean | Optional | Whether the candidate may use AI assistance. Default false, which tells the candidate plainly that it is not allowed and marks the screen. It is a declaration, not something that is enforced. |
- Create it inside a hiring project. Send
project_id(section 3.6.4) when you make the test. Candidates can be invited to a Technical Test only through a hiring project it is attached to, and the API has no route that attaches an existing one afterwards: withoutproject_idthe invitation fails with400project_requireduntil a person attaches the test to a project in the Flowmingo dashboard (section 4.6.1). The test is attached once it is published, and the answer then sayslinkedtrue. - It takes minutes. The call waits about 45 seconds and then answers
202, so plan to ask again with the same key, or read the status route, until it isready. The first answer is normally202andpending. - Capacity. A Technical Test creation can be refused with
429assessment_capacity_busywhen generation capacity is not available. Nothing is started: send the same request again later. - Your plan's length limit (
assessment.length_exceeds_plan) is known only once the AI has written the problems, so for this kind it can arrive after the wait, as afailedstatus with that code. Ask for fewer or easier problems with a newidempotency_key. By then an unpublished draft of the test may already exist and generation units may have been spent. - Refused options. On top of section 3.6.4, a value in any of these answers
400assessment_field_not_supported: sponsorship and white-label, who may take the test (access,access_type,recipients), the intro, outro and background media,result_visibility,max_attempts,exam_security,strictness, durations (duration,duration_minutes,duration_seconds),time_limit_ms,memory_mb, your owntest_cases,starter_code,harness,reference_solutionorquestions,settings, and pre-screening questions. The problems, their cases and the time are all produced by the generation. - The answer adds
languages(those a candidate can actually use),skills,difficulty,duration_minutes(the problems' times added up),problem_count,problemsandgeneration. Each problem hasid,title,difficulty,duration_minutes,languages,is_debugging,verified(the AI's own solution passed the problem's own cases in the sandbox),sample_case_count(cases the candidate can see) andhidden_case_count(cases that grade the answer). The answer shows only these counts: it never returns a hidden case, the harness or any solution.generationsays how many problems wererequested,keptanddiscarded. Thestatusispublished,draft,archivedordeleted. A full sample is in section 3.6.14.
3.6.11 Budget and capacity
Every assessment costs AI generation. Your organization therefore has two budgets, counted in units (a Skill Test costs more than a CV Evaluation), shared by all its API keys and by the assessments that project calls make (section 3.5.5):
| Budget | Units |
|---|---|
| Per minute | 60 |
| Per UTC day | 1,500 |
| Kind | Units |
|---|---|
| CV Evaluation | 1 |
| Showcase | 1 |
| AI Interview | 2 |
| Role-play | 2 |
| Skill Test | 3 |
| Technical Test | 6 |
- When units are taken. A request takes its units when its work starts. A repeat of a request that already finished costs nothing. A request that is refused before any AI work gives its units back; once the AI has been called the units stay taken even if the attempt then fails, and a new attempt takes its own.
- When the budget is spent the call answers
429with the codeassessment_create_limit_reachedand starts nothing. Themessagesays whether to try again in a minute or after 00:00 UTC (when both are spent it names the day), and what the kind costs. - If the budget cannot be checked just now the call answers
503assessment_create_limit_unavailableand starts nothing: it never starts work it could not count. - Capacity is separate from the budget. When capacity is not available the call answers
429assessment_capacity_busyat once and starts nothing. Send the same request again a little later.
3.6.12 When a request fails
A request can fail in two ways.
- Refused at the door. A rule, a missing scope or role, a
project_idthat cannot be used, a spent budget: the answer is an error status with acode, and nothing is recorded under theidempotency_key. Correct the request and send it again with the same key. - Failed after it was accepted. The work started and could not finish. The call that is waiting answers with the error status and the
codebelow. A call that already answered202finds the same thing later asstatusfailedwith afailureobject on the status route (section 3.6.3). The failure stays under the key: whenretryableistrue, send the same request again with the same key and the work takes up again; when it isfalse, an unchanged repeat is not expected to resolve it without a corrective step, which the last column of the table names. A key stays bound to the body it was first used with, so if the fix changes the body, use a new key.
Two statuses here are not general server errors: 502 and 500 come with a code from the table, and every row says whether sending the same request again can finish the assessment.
code |
Status | What it means | Sending the same request again |
|---|---|---|---|
assessment_generation_failed |
502 | The AI could not write this assessment just now, and nothing usable was kept. | Can finish it, in a minute. If it keeps failing, shorten or simplify the job description. |
assessment_not_ready |
500 | The assessment was made but did not pass the final readiness check (published, attached, with its content). | Can finish it: it is checked and finished. |
assessment_project_link_failed |
502 | The assessment was made but could not be attached to the project. | Can finish it: the attachment is tried again, and nothing is made twice. |
assessment_create_role_unconfirmed |
503 | The role of the person who owns the key could not be confirmed just now, so the work was paused. | Can finish it, in a moment. |
assessment_languages_unavailable |
503 | The list of interview languages could not be read, so the languages you named could not be confirmed and nothing was started (AI Interview). | Can finish it, in a moment. |
assessment_create_limit_unavailable |
503 | The generation budget could not be checked, so nothing was started. | Can finish it, in a moment. |
assessment_capacity_busy |
429 | Generation is busy. Nothing was started. | Can finish it, a little later. |
assessment_create_limit_reached |
429 | Your organization has spent its generation budget (section 3.6.11). Nothing was started. | Can finish it, after the minute or the 00:00 UTC the message names. |
technical_test_not_publishable |
502 | The problems the AI wrote did not pass the checks a Technical Test needs, so it was not published. Nothing is wrong with your request. | Not by itself. Send it with a new idempotency_key and the AI writes the problems again. |
assessment_creator_not_authorised |
403 | The person the request runs as is no longer an Owner or Admin, so the work was stopped. That person is the user the key is bound to, when it has one, and otherwise the person who created the key. | Not by itself. Create the key again from an Owner or Admin account. |
assessment_create_requires_admin |
403 | Refused at the door: the person who created the key, or the person the request runs as (the user the key is bound to, when it has one, otherwise the person who created the key), is not an Owner or Admin of the organization (any more). Both are checked and the code does not say which failed. | Not by itself. Use a key of an Owner or Admin. |
project_not_found |
404 | On a create call, project_id does not name an active hiring project of your organization that the key can use (a key whose creator is limited to some projects gets a 403 for a project outside theirs instead, section 3.6.4), and nothing was created. On the status read (section 3.6.3), and in the failure of a request that was stopped because its project went away, the request exists and an assessment may already have been made: the message says so. |
Not by itself. On a create call, use another project. On the status read or a stopped request, check the assessments list (a colleague who can open every project, such as an Owner, may have to) before you send the request again. |
assessment_request_not_found |
404 | The status route found no request with this key in your organization. | Not applicable: nothing was sent. |
assessment_was_deleted |
409 | The assessment this key made was deleted or archived since, so it was not touched again. | Not by itself. Use a new key to make another one. |
project_already_has_cv_evaluation |
409 | The project already has a CV Evaluation (a project can have one), so nothing was made. | Not by itself. Use the existing one, or delete it in the dashboard first. |
idempotency_key_reused |
409 | The key was already used for a different assessment request (another external_id counts as a different request). Nothing was created. |
Not by itself. Use a new key for a different assessment, or send the original request to get the original assessment back. |
assessment.length_exceeds_plan |
400 | The assessment is longer than your plan allows. Nothing was created, except for a Technical Test: its length is known only once the AI has written the problems, so an unpublished draft may already exist and generation units may have been spent. | Not by itself. Ask for less (a shorter duration, or fewer or easier problems) with a new key. |
interview_language_not_supported |
400 | interview_languages names a language the dashboard does not offer. |
Not by itself. Fix the name. |
language_not_supported |
400 | language is not a supported language tag. |
Not by itself. Fix the tag. |
technical_test_language_not_supported |
400 | languages names a language the Technical Test does not offer, or one that cannot be run right now. |
Not by itself. Use another language, or try later. |
showcase_tasks_invalid |
400 | The tasks are not usable: a repeated title, or more recording time than allowed. | Not by itself. Fix the tasks. |
role_play_scene_invalid |
400 | Only one of scenario and character was sent: they go together. |
Not by itself. Send both, or neither. |
cv_evaluation_criteria_missing |
400 | A CV Evaluation needs at least one criterion with a name. | Not by itself. Send criteria, or leave them out. |
assessment_field_not_supported |
400 | The body carries a dashboard-only setting these routes refuse (section 3.6.4). | Not by itself. Remove it. |
api_key_has_no_acting_user |
400 | The key has no user and no creator on record. A key with no creator on record is normally refused first, with a 403 (section 11). |
Not by itself. Create the key from a signed-in account. |
assessment_compose_invalid_input |
none: it is inside failure |
The project call only: the project's position or description does not fit this kind (section 3.5.5). The project was created, and the call answers 201 (200 for a repeat, 202 while another assessment is still pending); the code is in that assessment's failure, never an HTTP status. |
Not by itself. Make the assessment with its own route and a job title that fits. |
| (no code) | 400 | The body is invalid: a missing or blank field, text too long or containing < or >, a field of the wrong type, an idempotency_key of the wrong form, an external_id that breaks the rules of section 8.8. message names the field. |
Not by itself. Fix the field. |
A problem with the key itself (401, or a 403 for a missing scope, an expired key, a creator who has left, can open no hiring project or is not on record) is described in section 11.
3.6.13 Sample: create a CV Evaluation
The ids in these samples are placeholders.
curl -X POST https://apis.flowmingo.ai/company/integration/assessments/cv-evaluation/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"idempotency_key": "ats-job-48213-cv",
"external_id": "REQ-48213",
"job_title": "Support Engineer",
"job_description": "We are hiring a support engineer for our payments platform. You will review, prioritise and answer customer tickets."
}'
The answer is 201 (criteria is cut to two of its seven entries here):
{
"id": "7d2c1c3e-5b1f-4f55-9a3b-0c9a2f7e8d11",
"type": "cv_evaluation",
"name": "Support Engineer – CV Evaluation, 22 Oct '26",
"job_title": "Support Engineer",
"external_id": "REQ-48213",
"status": "active",
"ready": true,
"report_language": "en",
"criteria": [
{
"id": "1f0e0f0a-6b6c-4c1c-9a3a-0a0a0a0a0a0a",
"title": "Customer support and ticket triage",
"details": "Help payments-platform customers by reviewing, prioritizing, and responding to support tickets.",
"importance": "must_have"
},
{
"id": "2a1f1a1b-7c7d-4d2d-8b4b-1b1b1b1b1b1b",
"title": "Clear written communication",
"details": "Write understandable, helpful answers to customers about their issues.",
"importance": "must_have"
}
],
"criteria_count": 7,
"project_id": null,
"linked": false,
"complimentary_cv_evaluation": { "enabled": true, "notice_acknowledged": true },
"idempotent_replay": false
}
Sending the same request again returns this assessment with 200 and idempotent_replay true; sending it with another external_id is a 409. Find the assessment later with GET /hiring/interview-sets/v1?external_id=REQ-48213 (section 8.8). The id is the assessment's id: use it as com_interview_set_ids on the invite route (section 4) for the kinds that route can send (section 4.2).
3.6.14 Sample: a Technical Test
A Technical Test takes minutes, so the first answer is normally 202. The request names the hiring project (here a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d, a placeholder: use the id of one of your own projects from GET /hiring/projects/v1) so that the test is attached to it and candidates can be invited to it through the API (section 3.6.10):
curl -i -X POST https://apis.flowmingo.ai/company/integration/assessments/technical-test/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"idempotency_key": "ats-job-48213-tt",
"job_title": "Data Analyst",
"job_description": "We are hiring a data analyst to report on growth and campaign spend.",
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
}'
HTTP/1.1 202 Accepted
Retry-After: 10
The body is the 202 body of section 3.6.2. Read where it has got to, until status is ready:
curl -H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
https://apis.flowmingo.ai/company/integration/assessments/requests/ats-job-48213-tt/v1
{
"idempotency_key": "ats-job-48213-tt",
"kind": "technical_test",
"status": "ready",
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"assessment": {
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"type": "technical_test",
"name": "Data Analyst – Technical Test",
"job_title": "Data Analyst",
"external_id": null,
"status": "published",
"ready": true,
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"linked": true,
"languages": ["python3", "javascript", "java"],
"skills": [],
"difficulty": "medium",
"duration_minutes": 60,
"problem_count": 2,
"problems": [
{
"id": "5d0f6a9e-2c1b-4e7a-8f3d-9b1c2a3d4e5f",
"title": "Rank Campaign Conversion Rates",
"difficulty": "medium",
"duration_minutes": 30,
"languages": ["python3", "javascript", "java"],
"is_debugging": false,
"verified": true,
"sample_case_count": 2,
"hidden_case_count": 4
},
{
"id": "2e6f8a1c-4b3d-4c5e-9f7a-0b1c2d3e4f5a",
"title": "Debug the Campaign Budget Ledger",
"difficulty": "medium",
"duration_minutes": 30,
"languages": ["python3", "javascript", "java"],
"is_debugging": true,
"verified": true,
"sample_case_count": 2,
"hidden_case_count": 5
}
],
"generation": { "requested": 2, "kept": 2, "discarded": 0 }
},
"failure": null
}
linked true says the test is attached to the project, so candidates can now be invited to it (section 4.6). Without project_id in the request, project_id here is null and linked is false, and an invitation would fail with project_required. While it is being made the same read shows status pending (section 3.6.3). The same request sent again once it is ready answers 200 with this assessment and idempotent_replay true.
3.7 Publish, unpublish and boost a job post
A job post is the public page where candidates read about a role and apply. Four routes switch it on or off, promote it, and open or close the hiring project it belongs to, the way the Live switch and the status control do in the dashboard. A fifth route says whether the page is open. None of them changes the words of the job post: for its title and description, use the edit route of section 3.5.14 (while the job post is switched on) or the dashboard. Every path is relative to https://apis.flowmingo.ai/company/integration.
| Operation | Scope | What it does | Section |
|---|---|---|---|
GET /hiring/job-posts/{id}/publication/v1 |
read_hiring |
Says whether the job post is open to the public, and how it is boosted. | 3.7.1 |
POST /hiring/job-posts/{id}/publish/v1 |
create_project |
Switches the job post on, after the AI content check. | 3.7.2 |
POST /hiring/job-posts/{id}/unpublish/v1 |
create_project |
Switches the job post off. | 3.7.3 |
POST /hiring/job-posts/{id}/boost/v1 |
create_project |
Sets how widely the job post is promoted. | 3.7.4 |
POST /hiring/projects/{id}/status/v1 |
create_project |
Opens, drafts or archives the hiring project. | 3.7.5 |
The id of a job post is the job_post.id of the create-project answer (section 3.5.9) or the id of a row of GET /hiring/job-posts/v1 (section 5.6). The id of a project is the one section 5.7 explains how to find. The read answers 200. The four POST routes answer 201 when they work, also when nothing changed (changed false), and so does the cancel route of section 4.9.1.
The four POST routes act as the person who created the key, and that person must be an Owner or Admin of your organization. The role is read again on every call. During the grace period after a creator has left (section 2.10), the role they left with is used. If the role is not enough, the answer is 403 with the code project_create_requires_admin. The hiring project of the job post must also be one that person can open, or the answer is 403 with the code key_out_of_reach (section 2.9). A job post or a project that is not in your organization answers 404. The read route needs only read_hiring.
3.7.1 Is the job post open to the public?
A job post whose status is active is not necessarily open: status is only the post's own switch. A job post is open to the public only when all three of these hold:
- it is switched on,
- the AI content check approved it, and
- its hiring project is active.
So the publication read, and the publish, unpublish and boost answers, all carry the same block, and the field to read is public_state. (The project status answer has its own shape: section 3.7.5.) This request changes nothing:
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" \
"$BASE/hiring/job-posts/c0ffee00-dead-4bee-8f00-0123456789ab/publication/v1"
{
"job_post_id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "active",
"review_status": "approved",
"review_rejection_reason": null,
"public_state": "live",
"public_url": "https://talent.flowmingo.ai/jobs/ZW5naW5lZXI",
"tier": "off",
"reasons": [],
"warnings": []
}
| Field | What it holds |
|---|---|
job_post_id, com_project_id |
The job post, and the hiring project it belongs to. |
status |
The post's own switch: active (on), inactive (off) or other. It does not say whether the page opens. Read public_state. |
review_status |
The AI content check: not_reviewed (it has not run, or has not finished), approved or rejected. |
review_rejection_reason |
Why the check refused the post, when review_status is rejected. Otherwise null. |
public_state |
What the public page does now. The values are in the next table. |
public_url |
The address of the public page when the post could open there. It is null for a post that is off or rejected. Send people to it only when public_state is live. |
tier |
The boost: off, flowmingo or extended (section 3.7.4). |
reasons |
Codes that say why the page is not open: post_off, awaiting_review, review_rejected, project_not_open and flagged_while_live. The list is empty when public_state is live. |
warnings |
Things worth knowing that did not stop the call. Ignore a code you do not know. linkedin_posted is in every answer for a post that is marked as posted on LinkedIn, the read included. company_profile_incomplete is added only by a publish call, never by the read: it says the company has no logo or description. boost_not_public_yet is added only by a boost call. |
public_state |
What it means |
|---|---|
live |
The page opens. |
awaiting_review |
The post is switched on, but the AI content check has not approved it yet. Its link shows "not found". The check runs about once a minute on posts that are switched on. |
rejected |
The check refused the post. It never opens. review_rejection_reason says why. |
flagged |
The post was live and a later check flagged it. The link still works, but the post is out of the job boards. |
off |
The post is switched off. |
project_not_open |
The post is still switched on, but its hiring project is a draft, archived or deleted. |
off comes before project_not_open. A post that is switched off reads off, even when its hiring project is a draft or archived: reasons then holds both post_off and project_not_open. The state project_not_open itself appears only when a post is still switched on while its project is not active. After you move a project to draft or archived, the job post can already read off, so read reasons, not the state alone, to see every cause. project_not_open in turn comes before rejected, awaiting_review and flagged: a post that the check refused reads rejected only while it is switched on and its project is open. If its project is not open it reads project_not_open, and if it has been switched off it reads off.
Errors: 404 with the code job_post_not_found for a job post that is not in your organization, and 403 with the code key_out_of_reach for one inside a project the key's creator cannot open.
3.7.2 Publish a job post
POST /hiring/job-posts/{id}/publish/v1 switches the job post on, like the Live switch on the project's job post page. It takes no body. Nothing is emailed and no webhook fires.
curl -s -X POST -H "X-API-Key: $FLOWMINGO_API_KEY" \
"$BASE/hiring/job-posts/c0ffee00-dead-4bee-8f00-0123456789ab/publish/v1"
{
"job_post_id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "active",
"review_status": "approved",
"review_rejection_reason": null,
"public_state": "live",
"public_url": "https://talent.flowmingo.ai/jobs/ZW5naW5lZXI",
"tier": "off",
"reasons": [],
"warnings": ["company_profile_incomplete"],
"changed": true
}
The answer is the block of section 3.7.1 plus changed, which is false when the post was already on. Read public_state, not status: live means the page opens now. The warning company_profile_incomplete says the company logo or description is missing. Only a publish call adds it: the read of section 3.7.1 for the same post answers []. The post is published anyway, as in the dashboard, but boosting needs both.
- The hiring project must be active. If it is a draft, archived or deleted, the call answers
409with the codeproject_not_openand changes nothing. Open the project first (section 3.7.5). - The text is checked before it goes public. A post whose wording has no checked copy yet (it was never checked, it was refused, it was flagged while it was live, or it was edited since its last check) goes through the same AI check as the dashboard first, and is switched on only if it passes (the one exception is in the next point). If the check refuses it, the call answers
422with the codepublish_review_rejectedand arejection_reason, and nothing is changed. A post whose approved wording already has a checked copy needs no check. - A post that was approved, then edited while it was off, cannot be switched on from here. (An edit in the dashboard counts. The edit route of section 3.5.14 does not change the words of a job post that is switched off, so an edit made while it is off cannot cause this: it answers
409job_post_is_off. An edit of a post that is switched on can leave it in the same state, for example if its check does not finish. Once you have this409, switch the post on in the dashboard.) Its new wording has no check yet (the same is true if an earlier check flagged it). The call answers409with the codejob_post_edited_while_offand changes nothing. Switch it on once in the dashboard, which checks it. - A check can be slow, and a failed check is rolled back. After the switch, the call waits up to 60 seconds for the checked copy. If the check cannot finish, or refuses the post after all, Flowmingo tries to switch the post back off, and the call answers
503with the codejob_post_review_unavailableor422with the codepublish_review_rejected. If switching it back off fails too, themessagesays the post may still be live: open it in the dashboard and switch it off. After any of these answers, readpublic_state(section 3.7.1) before you tell anyone the post is live or off. Try again in a moment after a503. - The post must stay as it was. If it is edited, switched on or off, or deleted while the check runs, the call answers
409with the codejob_post_changed. If that happens before the switch, nothing is switched on. If the post was edited and checked again after this call switched it on, it is left exactly as it now is. Read the post again and retry. - There are 50 checks per organization per UTC day, counted across all your keys. At the limit the call answers
429with the codepublish_review_limit_reached. If the counter cannot be read, it answers503with the codepublish_review_limit_unavailableand changes nothing. - Publishing again is safe. A post that is already on stays on, and the answer says
changedfalse. If that post was never checked, the call can still run its content check.
An example of a refused post:
{
"statusCode": 422,
"error": "Unprocessable Entity",
"code": "publish_review_rejected",
"message": "The AI check of this job post did not pass, so it was switched back off. Reason: The post asks applicants to pay a fee. Change the text in the dashboard and publish again.",
"rejection_reason": "The post asks applicants to pay a fee.",
"timestamp": "2026-11-05T08:15:00.000Z",
"path": "/integration/hiring/job-posts/c0ffee00-dead-4bee-8f00-0123456789ab/publish/v1"
}
Branch on code, never on the wording of message.
3.7.3 Unpublish a job post
POST /hiring/job-posts/{id}/unpublish/v1 switches the job post off. Its public link stops opening, and it leaves the job boards and search at the next sync, which takes a few minutes. The hiring project stays as it is.
| Field | Type | Required | Description |
|---|---|---|---|
withdraw_boost |
boolean | Optional (default true) |
true switches the boost off too, as the project page does. false leaves the boost on, and then publishing the post again boosts it again without asking. It must be a real JSON boolean: the text "false" is refused with 400. |
curl -s -X POST "$BASE/hiring/job-posts/c0ffee00-dead-4bee-8f00-0123456789ab/unpublish/v1" \
-H "X-API-Key: $FLOWMINGO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{ "withdraw_boost": true }'
{
"job_post_id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "inactive",
"review_status": "approved",
"review_rejection_reason": null,
"public_state": "off",
"public_url": null,
"tier": "off",
"reasons": ["post_off"],
"warnings": [],
"changed": true,
"boost_withdrawn": true
}
The answer is the block of section 3.7.1 plus changed (false when the post was already off) and boost_withdrawn (true when this call also switched the boost off). An unpublish of a post that is already off can still withdraw the boost, so changed can be false while boost_withdrawn is true.
- Switching off a job post or moving its project to draft or archived does not reliably stop invitations or reminders. An automatic invitation triggered by an application may still be sent; reminders may still go out, although some invitations and reminders may be held.
- The warning
linkedin_postedsays the job was marked as posted on LinkedIn. Close it there too. - To undo it, publish the post again (section 3.7.2).
3.7.4 Boost a job post
POST /hiring/job-posts/{id}/boost/v1 sets how widely the job post is promoted. Boosting is free: no wallet, credit or plan is involved.
| Field | Type | Required | Description |
|---|---|---|---|
tier |
text | Yes | off, flowmingo or extended. |
tier |
What it does |
|---|---|
off |
The link works, but the post is not promoted. |
flowmingo |
The post is shown to suitable candidates across the Flowmingo network. |
extended |
The same, plus a best-effort listing with partner job boards. Each board decides whether and when to show it, so the reach varies. |
curl -s -X POST "$BASE/hiring/job-posts/c0ffee00-dead-4bee-8f00-0123456789ab/boost/v1" \
-H "X-API-Key: $FLOWMINGO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{ "tier": "flowmingo" }'
{
"job_post_id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "active",
"review_status": "approved",
"review_rejection_reason": null,
"public_state": "live",
"public_url": "https://talent.flowmingo.ai/jobs/ZW5naW5lZXI",
"tier": "flowmingo",
"reasons": [],
"warnings": [],
"changed": true
}
The answer is the block of section 3.7.1 plus changed, which is false when the post already had that tier.
- Raising the tier needs three things: the post is switched on (otherwise
409with the codejob_post_not_active), its hiring project is active (otherwise409with the codeproject_not_open), and the company profile has a logo and a description (otherwise409with the codecompany_profile_required, and503with the codecompany_profile_unavailablewhen the profile cannot be read). The post also needs a description to be checked (otherwise400with the codejob_post_description_empty). - A raise runs the dashboard's AI check. If the check refuses the post, the call answers
422with the codeboost_review_rejectedand arejection_reason. The boost is not applied, and the post stays exactly as it was, live or not. - There are 50 raises per organization per UTC day, counted across all your keys. At the limit the call answers
429with the codeboost_limit_reached. If the counter cannot be read, it answers503with the codeboost_limit_unavailableand changes nothing. - If the post changes while it is being checked, the call answers
409with the codejob_post_changedand no boost is applied. - Lowering the tier, or switching it off, needs no check and uses none of the 50.
- The warning
boost_not_public_yetsays the boost was set before the post is approved, so it does nothing until the post is. The call never rewrites the post's text. - To undo it, set the previous tier.
3.7.5 Open, draft or archive a hiring project
POST /hiring/projects/{id}/status/v1 moves a hiring project between three states, like the status control in the dashboard (which is for Owners and Admins, and so is this call). The answer is 201, also when the project was already in that state. It changes only the status. To change the project's name, role or description, use section 3.5.14, and to read the project's status back, section 3.5.13.
| Field | Type | Required | Description |
|---|---|---|---|
status |
text | Yes | active, draft or archived. |
status |
What it does |
|---|---|
active |
Brings the project back to the active board. Its job post opens again only once the post is switched on and has been checked. |
draft |
Takes the project's job post off the job board and closes its link. |
archived |
Hides the project from the active board and closes the job link. |
Switching off a job post or moving its project to draft or archived does not reliably stop invitations or reminders. An automatic invitation triggered by an application may still be sent; reminders may still go out, although some invitations and reminders may be held. Candidate data, interviews and reports are kept in every case, and invitations already sent and referral links keep working. Google is not asked to drop an archived job. You can move the project back at any time.
curl -s -X POST "$BASE/hiring/projects/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/status/v1" \
-H "X-API-Key: $FLOWMINGO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{ "status": "archived" }'
{
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "archived",
"changed": true,
"job_post_ids": ["c0ffee00-dead-4bee-8f00-0123456789ab"],
"notes": [
"The project is hidden from the active board and its job link is closed. Switching off a job post or moving its project to draft or archived does not reliably stop invitations or reminders. An automatic invitation triggered by an application may still be sent; reminders may still go out, although some invitations and reminders may be held. Candidate data stays. Google is not asked to drop an archived job."
]
}
| Field | What it holds |
|---|---|
com_project_id, status |
The project, and its state now. |
changed |
false when the project was already in that state. |
job_post_ids |
The project's job posts, whose public pages follow the project. |
notes |
Sentences for a person that say what this state means for candidates. Do not branch on their wording. |
A project that is not in your organization answers 404 with the code project_not_found. A project the key's creator cannot open answers 403 with the code key_out_of_reach.
3.7.6 Errors on these routes
Every error has the shape of section 8.4. Branch on code.
| Status | code |
What to do |
|---|---|---|
400 |
(no code) | The id is not a UUID, withdraw_boost is not a real JSON boolean, tier is not off, flowmingo or extended, or status is not active, draft or archived. Fix the request. |
400 |
job_post_description_empty |
A boost was asked for a job post with no description to check. Add one in the dashboard. |
400 |
api_key_has_no_acting_user |
The key has no user and no creator on record. Create the key from a signed-in account. |
403 |
(no code) | The key lacks the scope the route needs (read_hiring to read, create_project to change), or has expired (section 11). |
403 |
project_create_requires_admin |
The person who created the key is not an Owner or Admin of the organization (any more). During the grace period after a creator has left, the role they left with is used. |
403 |
key_out_of_reach |
The job post or project is inside a hiring project the key's creator cannot open (section 2.9). |
404 |
job_post_not_found |
No such job post in your organization. |
404 |
project_not_found |
No such hiring project in your organization (the status route). |
409 |
project_not_open |
The hiring project is a draft, archived or deleted. Open it first (section 3.7.5). |
409 |
job_post_edited_while_off |
The post was approved, then edited while it was off, so its new wording has no check. Switch it on once in the dashboard. Nothing was changed. |
409 |
job_post_changed |
The post was edited, switched or deleted while it was being checked. No boost was applied, and a publish switched nothing on, unless the post was edited and checked again after this call switched it on (it is then left as it is). Read it again and retry. |
409 |
job_post_not_active |
A boost was raised on a post that is off. Publish it first. |
409 |
company_profile_required |
A boost needs a company logo and description. The message names what is missing. Add them in the company profile, then boost again. |
422 |
publish_review_rejected |
The AI check refused the post. rejection_reason says why. The post was not switched on, or Flowmingo tried to switch it back off. If that failed too, the message says the post may still be live: switch it off in the dashboard. Change the text in the dashboard and publish again: the edit route of section 3.5.14 does not change the words of a job post that is switched off (job_post_is_off). |
422 |
boost_review_rejected |
The AI check refused the post for a boost. rejection_reason says why. The boost was not applied and the post stays as it was. |
429 |
publish_review_limit_reached, boost_limit_reached |
The 50 publish checks or the 50 boost raises of the UTC day are used. Try again after 00:00 UTC. |
503 |
job_post_review_unavailable |
The AI check could not run or finish. If it could not run, nothing was changed. If it could not finish after the switch, Flowmingo tried to switch the post back off, and if that failed too the message says the post may still be live. Try again in a moment. |
503 |
publish_review_limit_unavailable, boost_limit_unavailable, company_profile_unavailable |
A counter or the company profile could not be read just now. Nothing was changed. Try again in a moment. |
Repeating a call is safe. changed false means the switch of the post, the boost tier or the status of the project was already as you asked. The call can still do one small extra: an unpublish can withdraw the boost, and a publish can run the content check of a post that was never checked. After a timeout, read the job post's publication (section 3.7.1) before you repeat a publish.
3.8 Tracking links
A tracking link is one of the custom share links of Settings: the address of a job post, of your careers page or of a set of your job posts, with ?tl=<token> on the end. A candidate who applies through a link is credited the link's source, its name and its person. These five routes make, change, switch off, delete and list tracking links. They are not the candidate profile share link, which is a different feature and is not available through the API.
| Operation | Scope | What it does |
|---|---|---|
POST /tracking-links/v1 |
manage_tracking_links |
Makes a link for one job post, for a set of job posts or for your careers page. Needs the Growth plan. |
PATCH /tracking-links/{id}/v1 |
manage_tracking_links |
Changes the label, source, details or person of a link, or switches it on or off. |
DELETE /tracking-links/{id}/v1 |
manage_tracking_links |
Deletes a link. |
GET /tracking-links/v1 |
read_hiring and read_candidates |
Lists your links, newest first. |
GET /tracking-links/sources/v1 |
manage_tracking_links and read_candidates |
Lists the sources a link can credit. |
manage_tracking_links is opt-in: a key does not have it unless it is chosen when the key is created or edited (section 2.3). A link holds no candidate record, but the text people type into one (its label, its details, the name of the person it credits and the name of its source) can name an applicant or a referrer. That is why both lists need read_candidates, and why the answer to a make or a change leaves that text out for a key without it (below). The MCP server has no tool for these routes.
Who can use them. The key acts as the person who created it. To make, change or delete a link, or to list the sources, that person must be an Owner or an Admin: an Owner reaches every hiring project, an Admin the projects they hold. A Member is refused with 403, whatever custom role they have, normally with the code tracking_link_requires_admin. A link for your careers page needs access to all hiring projects (an Owner has it; an Admin who was given projects one by one does not, even if the list names every project today), and a link for a set of job posts needs every project of its posts. A call from a key whose creator has left the company is refused, normally with 403 and the code key_creator_left, including during the grace period that other routes normally allow (section 2.10). Making a link, listing links and listing sources also need Custom share links to be switched on for your company (403 with the code custom_share_links_off). Changing and deleting a link are not stopped by that switch or by your plan, so you can still tidy up what you made.
3.8.1 Make a link
POST /tracking-links/v1 takes this JSON body.
| Field | Type | Meaning |
|---|---|---|
target_kind |
text, required | job_post, job_post_set or careers_page. A link to an AI interview or to a bundle of assessments cannot be made through the API (400, code tracking_link_kind_not_supported). |
project_id, target_id |
UUIDs | For job_post only, and both are required: the hiring project and one of its job posts (the id from GET /hiring/job-posts/v1). Leave both out for the other two kinds. |
target_job_post_ids |
list of UUIDs | For job_post_set only: 2 to 50 distinct job posts of your company. |
source_type |
text | The code of one of your own sources, as GET /tracking-links/sources/v1 lists them: 1 to 40 lower-case letters, digits and underscores, for example linkedin or career_fair. |
source_detail |
text | Up to 60 characters. Needs a source. |
label |
text | A name for the link, up to 60 characters. |
attributed_person |
object | { "person_id": "<uuid>" }: a person your company already has (the attributed_person.id of a link that credits them). |
A link needs a person, a source or both (400, code tracking_link_needs_person_or_source). A person cannot be made through the API from a name and an email: attributed_person.outside is a 400 with the code tracking_link_field_not_supported, and a person given by a numeric user id is a 400 with the code tracking_link_colleague_not_supported. token, id and enabled cannot be sent on a make (400, tracking_link_field_not_supported). Text that starts with =, +, -, @, a tab or a carriage return is refused (400, tracking_link_text_not_allowed). A text field that is not a JSON string is a 400 too. A source_type of any other shape, or one that names none of your sources or an archived one, is a 400.
curl -s -X POST -H "X-API-Key: $FLOWMINGO_API_KEY" -H "Content-Type: application/json" \
"$BASE/tracking-links/v1" \
-d '{"target_kind": "job_post", "project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "target_id": "c0ffee00-dead-4bee-8f00-0123456789ab", "source_type": "linkedin", "label": "Careers fair, Lagos"}'
The answer is 201. This one is for a key that holds read_candidates:
{
"id": "2b6f1d8c-4e3a-4c9b-8a7d-5e0f9c1b3a64",
"target_kind": "job_post",
"target_id": "c0ffee00-dead-4bee-8f00-0123456789ab",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"source_type": "linkedin",
"source_label": "LinkedIn",
"source_detail": null,
"label": "Careers fair, Lagos",
"attributed_person": null,
"token": "k3JwX9aQ2mTn7vYbL0sDhA",
"enabled": true,
"created_at": "2026-11-11T08:30:00.000Z",
"address": "https://talent.flowmingo.ai/jobs/c2VuaW9yLW51cnNlLTJmOWE?tl=k3JwX9aQ2mTn7vYbL0sDhA",
"address_host_note": "Built on Flowmingo’s default candidate address. This route does not apply a company’s own candidate domain."
}
tokenis 22 case-sensitive characters made by the server. It is a public label, not a credential: it sits in the address a candidate opens and grants nothing alone. It never changes and is never reused, even after the link is deleted.addresssays where the link points. It does not say that the link credits anyone: a link credits an application only while it is switched on (enabled) and its target is open. A switched-off link and a link to a closed job post keep their address.addressis built on Flowmingo's default candidate address and does not apply a company's own candidate domain (address_host_notesays so). It isnullwhen the API cannot build one: for a link to an AI interview or a bundle (made in the dashboard), for a job post whose address cannot be read, and for a job post link whose job post has moved to another hiring project since the link was made. That job post's page still opens, but the link no longer credits applications: make a new link for the post.applications_countcan appear on the items of the list (section 3.8.3). It is not in the answer to a make or a change, and it is left out unless your company's source and attribution screens are switched on.- Without
read_candidates, the answer to a make and to a change leaves outlabel,source_detail,source_labeland the person'sname: the fields are absent, andnullmeans the link has none. The person is then{ "id": "...", "kind": "..." }(kindiscolleagueoroutside), and thatidis what you send asperson_id.source_typeis left out as well when it is the code of a source your company made itself (a code is the source's name in lower-case letters: a source namedJane Doehas the codejane_doe). The codes of the ten default sources (linkedin,facebook,instagram,whatsapp,telegram,email,career_fair,agency,company_website,other) andnullare always present. The write is made as you sent it, whatever the answer leaves out. - No
target. The answer to a make or a change does not say whether the link's job post is open: that is in the list (section 3.8.3). A key that holds onlymanage_tracking_linkscan therefore be handed the address of a link to a closed job post and cannot see through this API that the post is closed. Make that check with a key that also holdsread_hiringandread_candidates.
The plan. Making a link needs the Growth plan or above. Below Growth the answer is 403 with the code growth_required and required_tier set to growth. When the plan cannot be confirmed just now the answer is 503 with the code plan_unconfirmed: try again. It is not a statement about your plan. Changing a link, deleting a link and the two lists never need the plan.
Projects and job posts you cannot reach. A project_id that the key's creator cannot open is a 404 with the message Project not found., the same answer as a project that does not exist, belongs to another company or was deleted. A job post that cannot be linked is a job post that is not there: a job_post link whose job post is not in project_id, and a job_post_set with a job post in a project the creator does not hold, answer 404 with Job post not found., the same answer as an id that does not exist. A link for your careers page, when the creator holds only some projects, is a 403.
The key is checked again. The key and the person who created it are checked again shortly before a link is read, written, changed or deleted. A call that no longer qualifies by then (a key that was revoked, expired or lost a scope, a creator who was demoted or lost the project and, for a make, a company that was downgraded) is refused and nothing is saved. A change made after that check applies from your next request. When the check cannot be made the answer is 503 with the code key_reach_unavailable and a Retry-After header, and nothing was read or changed.
3.8.2 List the sources a link can credit
GET /tracking-links/sources/v1 needs manage_tracking_links and read_candidates. It lists your company's own sources that are not archived or deleted, in your order, as { "items": [...], "total": n }. Each item has source_type (the code to send when you make or change a link), label (the name), short_tag and colour (a colour or null).
{
"items": [
{ "source_type": "linkedin", "label": "LinkedIn", "short_tag": "LinkedIn", "colour": null },
{ "source_type": "career_fair", "label": "Career fair", "short_tag": "Career fair", "colour": null }
],
"total": 2
}
This read can write: it can create default sources. That is why it needs the managing scope and not a reading one. A source's name is text people typed, so a key that holds only manage_tracking_links is refused with 403 and is not handed the names. The key is checked again before the default sources are written and again before they are read, and a call that is refused at either check has created nothing.
3.8.3 List your links
GET /tracking-links/v1 needs read_hiring and read_candidates. It answers { "items": [...], "total": n } with the links of the hiring projects the key's creator can open, newest first, at most 2,000. A link for a set of job posts is listed only when that person can open every project of its posts, and the links made in the dashboard for an AI interview or a bundle can be listed too, with address null. Switched-off links are left out unless you send include_disabled=true.
| Query parameter | Type | Meaning |
|---|---|---|
project_id |
UUID | Only the links that touch this hiring project: the links filed under it, and a set anchored on another project when one of its job posts was in this project when the set was made (a set is returned for each project it touches, and com_project_id says which one it is anchored on). A careers page link belongs to no one project and is not returned for one. A project that is not yours, or that the creator cannot open, is a 404. |
include_disabled |
true or false |
Default false. |
Send each filter once, in its plain spelling (?project_id=<uuid>&include_disabled=true). A bracketed spelling (project_id[]=..., project_id[0]=..., include_disabled[]=..., also percent-encoded) is a 400, never "no filter", and so is a filter sent twice and a query with more than 1,000 parameters.
Each item has the fields of a link (section 3.8.1) and a target: { "title": "...", "available": true }, with job_post_count and job_post_live_count for a set. available is false when what the link opens is gone or its public page would not open (a closed or deleted job post, a switched-off interview), for a job post link whose job post has moved to another hiring project (such a link credits nothing), and when the API could not confirm the job post's project just now (the list is still served). The list does not say who applied; applications_count can say how many.
3.8.4 Change and delete a link
PATCH /tracking-links/{id}/v1 takes label, source_type, source_detail, attributed_person ({ "person_id": "<uuid>" } or null) and enabled (a JSON boolean). Only the fields you send change. null clears label, source_type, source_detail and attributed_person, but a link keeps a person or a source, and source_detail needs a source. "enabled": false stops the link crediting applications at once. Its address is not changed, and the applications it already credited keep their credit. The target, the project and the token never change (a body that names them is a 400). The answer is 200 with the link, and it leaves text out for a key without read_candidates in the same way as a make.
DELETE /tracking-links/{id}/v1 deletes the link and answers 200 with { "id": "...", "deleted": true }. It stops crediting at once, the applications it already credited keep their source, and its token is never reused. To stop a link without losing it, switch it off instead. If two deletes of the same link arrive together, the link is deleted once: one answers 200 and the other 404.
A link that the key's creator cannot reach (a link of a hiring project they do not hold, a set of job posts or the careers page that needs projects they do not hold, an AI interview link they cannot open) answers 404 to a change and to a delete, the same answer as an id nobody has.
3.8.5 Limits, repeats and failures
- Links. A hiring project can hold at most 200 links and your company 2,000 (
409). A switched-off link counts; a deleted link frees its place. - Changes. Making, changing and deleting links together are limited to 30 a UTC minute and 300 a UTC day for your company, across all its keys. Past that the answer is
429with the codetracking_link_write_limit_reachedand aRetry-Afterheader. When this limit cannot be checked just now the answer is503with the codetracking_link_write_limit_unavailable, and nothing was changed. - Reads. The list and the source list together are limited to 30 a UTC minute for your company, across all its keys. Past that the answer is
429with the codetracking_link_read_limit_reachedand aRetry-Afterheader. When the server is already serving as many of these reads as it allows at once, the answer is429with the codetracking_link_reads_busy, and nothing is counted against your limit. When the limit cannot be checked just now the answer is503with the codetracking_link_read_limit_unavailableand aRetry-Afterheader, and nothing was read. - The key limit. The limit of 600 requests a minute for each key (section 8.5) applies to these routes too.
- A ban. A key whose holder has been banned is refused with
403and the codeuser_banned. When that cannot be checked just now the answer is503(no code), and nothing was read or changed. - No idempotency key. Making a link twice makes two links, and a duplicate can be deleted by its id.
- The
500codes.internal_errormeans nothing was made or changed, so send the request again.tracking_link_saved_not_read_backmeans the link is saved and only the read after it failed: for a change, send the same change again (it answers the link); for a make, do not send it again, because the id is in the message.tracking_link_may_be_savedmeans the database did not confirm the save, so nobody can say whether it happened. For a change or a delete, send it again: that is safe. For a make, the message carries the id the link would have, and you must look before you make it again. A key that holdsread_hiringandread_candidateslists the links withinclude_disabled=trueand looks for the id. Any other key sendsPATCH /tracking-links/{id}/v1with{"enabled": true}, which answers the link, or404when the key cannot reach a link with that id (that call counts as one of the 30 changes a UTC minute and switches the link on if somebody had switched it off). If you find the link, it exists. Not finding it proves nothing: the list leaves out, and a change answers404for, every link the creator can no longer reach, so making the link again can leave two links.
3.8.6 Errors on these routes
Branch on statusCode and code, never on the wording of message.
| Status | code |
Meaning |
|---|---|---|
400 |
tracking_link_kind_not_supported |
target_kind is not job_post, job_post_set or careers_page. |
400 |
tracking_link_field_not_supported |
A field the route does not take (token, id, enabled on a make, the target on a change), or attributed_person.outside. |
400 |
tracking_link_colleague_not_supported |
attributed_person names a person by numeric user id. Use person_id. |
400 |
tracking_link_text_not_allowed |
A label or source_detail starts with =, +, -, @, a tab or a carriage return. |
400 |
tracking_link_needs_person_or_source |
The link would have neither a person nor a source. |
400 |
none | An id that is not a UUID, a source_type of the wrong shape or one that names no source of yours, a text field that is not a string, a bracketed filter, a query of more than 1,000 parameters. |
401 |
none | The key is missing, wrong or revoked. |
403 |
tracking_link_requires_admin |
The key's creator is not an Owner or an Admin. |
403 |
key_reaches_nothing, key_no_creator |
The key's creator can open no hiring project, or the key has no creator on record (section 11). |
403 |
growth_required |
Making a link needs the Growth plan (required_tier is growth). |
403 |
custom_share_links_off |
Custom share links are not switched on for your company. |
403 |
key_creator_left |
The key's creator has left the company. |
403 |
user_banned |
The key's holder has been banned. |
403 |
none | The key lacks the scope (API key missing required scope), or has expired, or the careers page link needs projects the creator does not hold. |
404 |
none | A link, job post, project or person that is not there for this key (section 3.8.1 and 3.8.4). |
409 |
none | A hiring project holds 200 links, or your company 2,000 (a switched-off link counts). |
409 |
tracking_link_try_again |
A job post of a set changed while the link was being made. Nothing was made: try again. |
400, 413 |
not guaranteed | A body that cannot be read as JSON (400) or that is over the size limit (413). Do not branch on the code of these two. |
429 |
api_key_rate_limited, tracking_link_write_limit_reached, tracking_link_read_limit_reached, tracking_link_reads_busy |
A limit of section 3.8.5 or 8.5. |
500 |
internal_error, tracking_link_saved_not_read_back, tracking_link_may_be_saved, or none |
See section 3.8.5. |
503 |
plan_unconfirmed, tracking_link_write_limit_unavailable, tracking_link_read_limit_unavailable, key_reach_unavailable, or none |
A check could not be made just now. Try again. |
When more than one of these could apply to a call, this guide does not say which one you get: handle each.
What you can rely on.
- A tracking link's token is a public label. It does not change and is not reused.
- A link credits an application only while it is switched on and its target is open.
addressis where it points and promises nothing more. - The routes act as the person who created the key. The text people typed into a link and the names of sources reach only a key that holds
read_candidates. - For a project, a job post or a link, "not there" and "not reachable by the key's creator" are answered the same way (
404). - The list does not say who applied.
4. Invite candidates through the API
This route adds candidates to your account and, unless you switch it off, asks for an invitation email for each one to take an AI interview, a role-play, a Skill Test, a Technical Test or a Showcase (the sender picks up an AI interview or a role-play invitation only while the assessment is published, and even then it queues the email separately, before or after the answer comes back, and can skip it, section 4.5). It can also order a CV evaluation: send the candidate's cv_link and the evaluation runs on that CV. No invitation email is sent for a CV evaluation. A Skill Test, a Technical Test or a Showcase is sent exactly as the Flowmingo dashboard sends it, by email address, and has its own rules, answer and limits (sections 4.6 and 4.8). An AI interview or a role-play invitation can be scheduled for a later time (section 4.9). Required API key scope: invite_candidates. If you ran the interview yourself and have a recording of it, there is nobody to invite: hand the recording over instead, with two other routes and their own scope, submit_recordings (section 4.7).
4.1 Invite endpoint & environments
| Environment | Settings URL | API |
|---|---|---|
| Production | https://team.flowmingo.ai |
https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1 |
Method: POST. The answer is 201 when the request was accepted. A 201 does not mean every candidate was invited, or that every invitation will be emailed: read the result for each candidate, including its warnings (see 4.5).
This route has no approval step and processes the request at once; the emails it starts are a separate matter (section 4.5). It does not wait for a person's approval, because it is built for your own systems (your ATS or CRM). The approval step applies to AI assistants connected through MCP, which use the routes in section 9.7, not this one.
The key acts with its creator's permissions (section 2.9). For a key whose creator is limited to some projects, a project, job post or interview set of your organization that the creator cannot send to answers 403 with the code key_out_of_reach before anything is created or sent, in an otherwise valid request: the other checks of this route (sections 4.2 and 4.3) answer first.
4.2 Which assessments can be invited through the API
Find the id and the assessment_type of each of your assessments with GET /hiring/interview-sets/v1 (see section 5.1).
assessment_type |
Through this route |
|---|---|
ai_interview |
Yes. An older interview with no recorded type is read as ai_interview. The sender picks the invitation up for emailing only while the assessment is published (status 1); an email that is already queued can still leave if the status changes afterwards. For a draft, an inactive or an archived assessment the call still succeeds, the invitation is created, and the result carries a warning (section 4.5). |
role_play |
Yes, with the same rule: the sender picks the invitation up only while the assessment is published (section 4.5). |
cv_evaluation |
An order, not an invitation. Send the candidate's cv_link and the evaluation runs on that CV. No invitation email is sent, and with send_invite true the candidate's entry can carry a note in invitation_error_messages saying there is nobody to invite. Without a cv_link nothing is ordered. |
skill_test, technical_test |
Yes, by email address, one test per request, with the extra rules, the different answer and the own daily limits in section 4.6. Sending costs nothing; a sponsored test can use your organization's wallet when a candidate starts it (section 4.6.7). |
recording |
No, it is not an invitation. The request is refused with a 400 and the code assessment_type_not_invitable_via_api, and nothing is created. A recording evaluation takes recordings of interviews you ran yourself: hand them over with the two routes of section 4.7. |
showcase |
Yes, by email address, one Showcase per request, with the rules, the answer and the limits in section 4.8. Sending costs nothing; a Showcase that is "Sponsored by you" uses your organization's wallet when a candidate completes it (section 4.8). |
questionnaire |
No. The request is refused with a 400 and the code assessment_type_not_invitable_via_api, and nothing is created. Send it from the Flowmingo dashboard. (A question bank is refused the same way.) |
The refusal covers the whole request. If one id in com_interview_set_ids is a kind that cannot be invited, nobody is invited to any of the assessments in that request. The message names every refused id and its type:
{
"code": "assessment_type_not_invitable_via_api",
"statusCode": 400,
"message": "Cannot invite through the API: 9c5d2a14-7e3b-4f68-b1a0-5d8e3c7f2a96 (Questionnaire). Only AI interviews, role-plays, CV evaluations, Skill Tests, Technical Tests and Showcases can be invited or ordered here; send other assessments from the Flowmingo dashboard. Nothing was created.",
"timestamp": "2026-10-20T08:15:00.000Z",
"path": "/integration/interview/candidate/invite/v1"
}
Branch on code, never on the wording of message.
4.3 Request payload
| Field | Type | Required | Notes |
|---|---|---|---|
com_interview_set_ids |
UUID[] | Conditionally | The assessments to invite the candidates to. Send this, com_project_ids, or both. Name a project or an assessment: a job post alone is not enough. A request that names no project, assessment or job post is refused (400, or 403 with the code key_must_name_target for a key whose creator is limited to some projects). A Skill Test, a Technical Test or a Showcase must be the only id in the request (sections 4.6 and 4.8). |
com_project_ids |
UUID[] | Conditionally | Hiring projects to put the candidates into (their pipeline). A Skill Test or a Technical Test takes at most one (section 4.6). Find the ids as in section 5.7. |
com_job_post_ids |
UUID[] | Optional | Job posts the invitation is for. Not used by a Skill Test or a Technical Test. |
candidates |
array | Yes | Must contain at least 1 candidate and at most 20,000 (section 8.5). |
candidates[].ats_candidate_id |
string | Optional | Your own id for the candidate: plain text of 1 to 250 characters, used exactly as you send it (an empty string counts as not sent). It does not have to be a UUID, and an id that worked before keeps working. It is returned in the result for this candidate, in this response only (section 4.5). Must be unique within a single request. When you send it, com_interview_set_ids (or the deprecated com_interview_set_id) is required too. More than 250 characters is refused (section 4.3.2). A Skill Test or a Technical Test refuses a value here; leave it out or send null (section 4.6). To find a person again later, use external_id (section 4.3.1). |
candidates[].email |
string | Optional | Where the invitation goes. If it is missing and cv_link exists, the CV is read in the background and the candidate is invited once an address is found in it. A Skill Test or a Technical Test needs an email for every candidate. |
candidates[].name |
string | Optional | Candidate display name. Use this or firstname and lastname. |
candidates[].firstname |
string | Optional | Candidate first name. |
candidates[].lastname |
string | Optional | Candidate last name. |
candidates[].cv_link |
string URL | Optional | A public link to the CV. Required to order a CV evaluation. Optional for a Skill Test or a Technical Test. |
candidates[].thumb_file_link |
string URL | Optional | A link to a thumbnail picture of the candidate. Not used for a Skill Test or a Technical Test. |
candidates[].source |
string | Optional | Where you found the candidate, free text. Flowmingo does not return it and does not use it for the invitation. It is kept only inside the record saved for your ats_candidate_id (section 4.5), so only when you send that together with an assessment id; otherwise it is thrown away. Not used for a Skill Test or a Technical Test. |
candidates[].external_id |
string | Optional | Your own id for the person, kept on your company's record of them and never on Flowmingo's shared candidate profile (section 4.3.1). |
candidates[].application_external_id |
string | Optional | Your own id for the person in the job. It is kept on the application in the hiring project this request names, so the request must name exactly one project (section 4.3.1). |
candidates[].invitation_external_id |
string | Optional | Your own id for the invitation to the assessment this request names, so the request must name exactly one assessment and send the invitation (section 4.3.1). |
invitation_message |
string | Optional | A note added to the invitation email: up to 2,500 characters of text (formatting and the address inside a link are not counted; a web address written out in the text is; an emoji counts as more than one character); an empty string means no note, and within these limits so does a blank note or what an empty editor leaves. A refused note gets a 400, for example invitation_note_empty for a note with no text left once unsupported content is removed, invitation_note_too_long for one with too much text, and invitation_note_too_large for one over 5,000 characters once unsupported content is removed, formatting and link addresses included. A note over 50,000 characters as sent gets a plain 400 with no code. For an AI interview or a role-play, write the final text: placeholders such as {{name}} are not supported. A Skill Test and a Technical Test use it too, and fill in a few placeholders for each candidate (section 4.6.1). |
send_invite |
boolean | Optional (default true) |
true asks for the invitation email for an AI interview or a role-play to be queued, which the sender does separately, before or after the call answers, and only if it does not skip the invitation (section 4.5). Large requests are paced: after the first 50 candidates, each further 50 places in the list adds about five minutes before the invitation is due, so the 1,000th candidate of one request is due about 100 minutes after the call. Once the per-key invitation limit is enforced (section 8.5), one call can make at most 300 invitations and one key at most 300 an hour and 1,000 a day, so spread a larger batch over several calls and several hours. false adds the candidate without queuing that email. A CV evaluation does not depend on it: with a cv_link it is ordered either way. A Skill Test or a Technical Test has its email attempted during your request, not in later batches (section 4.6). Must be a JSON boolean (see below). |
send_at |
text or null |
Optional | When to send the invitation email, as a date and time with a time zone, for example 2026-11-10T09:00:00+08:00 or 2026-11-10T01:00:00Z: from 2 minutes to 90 days from now. It applies to every candidate in the request. Only for an AI interview or a role-play, and only with send_invite true (section 4.9). It is when the invitation is sent, not when the candidate takes the interview. Leave it out to send at once. |
attachment_sys_file_ids |
UUID[] or null |
Optional | The ids of files you uploaded to Flowmingo, as a list even for one file (section 11). The files are checked but not sent: this route always sends from Flowmingo's own address, and an email from that address carries no company files, so an invitation email that is sent goes out without them and the request still succeeds. Each file must be one that the person the key acts for uploaded (section 4.6.2), or one attached to a message template that your organization shares or to a private template of that same person. Any other id is refused with 400 and the code attachment_not_yours, and nothing is created. Every id must be a version-4 UUID. A Skill Test and a Technical Test take it too, with the same result (section 4.6.1). |
practice_mode |
number or null |
Optional | Leave it out for a real invitation. 0 or 1 sends a practice invitation. Must be a JSON number. The invitation still adds an unstarted result (status 5) to the results lists, so practice_mode does not keep a test invitation out of your reports (section 5.1). Refused for a Skill Test or a Technical Test unless it is null (section 4.6). |
metadata |
object or null |
Optional | Accepted for compatibility, and not used: send a JSON object or null, and Flowmingo neither stores it nor returns it. Do not rely on it to carry anything: for an AI interview or a role-play put your own id on each candidate as ats_candidate_id instead (a Skill Test or a Technical Test does not take an ats_candidate_id either, section 4.6.1). A Skill Test or a Technical Test refuses any value other than null (section 4.6.1). |
The older single-value fields com_interview_set_id, com_project_id and com_job_post_id still work but are deprecated. com_interview_set_id accepts an AI interview, a role-play, a Skill Test or a Technical Test of yours; use com_interview_set_ids for a CV evaluation. The deprecated com_project_id also requires that project to have at least one job post: without one the request answers 404 (No job post found for project ...). A Skill Test or a Technical Test needs no job post.
A key inside a candidate that this table does not list is ignored: it is not used and it is not stored.
Five rules apply to the whole request:
- Ids are version-4 UUIDs. Every assessment, project and job post id in this request must be a version-4 UUID, which is what Flowmingo returns for them. Text that is not a UUID, or a UUID of another version, answers
400witheach value in com_interview_set_ids must be a UUID(the field name changes with the field). Copy ids from the API instead of writing them by hand. Your own ids are not Flowmingo ids:ats_candidate_id,external_id,application_external_idandinvitation_external_idare text you choose, and none of them is checked as a UUID. - Every id must be yours. Each assessment, project and job post id must belong to your organization. If any one does not, the request answers
404(for exampleInterview set 3f2504e0-4f89-41d3-9a0c-0305e82c3301 not found), nothing is created, and the other ids in the request are not used either. - A key whose creator is limited to some projects must name where the invitation goes: a project or an interview set, because a job post alone is not enough. A request that names no project, interview set or job post answers
403with the codekey_must_name_target. The403with the codekey_out_of_reachis described in section 4.1. Nothing is created. - Booleans and numbers must be real JSON booleans and numbers.
"send_invite": falseworks."send_invite": "false"is refused with400(send_invite must be true or false (a JSON boolean, not text)), and so is a quoted number such as"practice_mode": "1". Tools that send every field as text (spreadsheet and form connectors) need a step that writes real JSON, or you can leavesend_inviteout: it defaults totrue. - Daily limits. While
send_inviteistrue, an organization can send 5000 invitations a day, and 1000 a day to any one assessment. Each check adds the candidates in your request to what was already sent that day. When a request names several assessments, the per-assessment check adds up the invitations already sent for all of them, so a request can be refused although each assessment is below 1000 on its own: send one request per assessment when you are close to a limit. A request over a limit is refused with400and a message that beginsRate limit exceeded. The day is the calendar day on our server clock, not in your time zone. The limits are not checked whensend_inviteisfalse. A Skill Test, a Technical Test or a Showcase has limits of the same size (1000 a day for one assessment, 5000 for your organization) that answer differently, with429and a code (sections 4.6.6 and 4.8). A scheduled invitation also counts on the day it is due (section 4.9).
4.3.1 Your own ids for a candidate
A candidate can carry four ids of yours. They name different things and are kept in different places, so choose by what you want to find again:
| Field | It names | Kept on | Can you read it back? |
|---|---|---|---|
ats_candidate_id |
The candidate row in this request | A record Flowmingo saves for each assessment you name (section 4.5) | No. It is returned in this response only. |
external_id |
The person, as your system knows them (for example your ATS candidate id) | Your company's record of the person, never Flowmingo's shared candidate profile | Yes: GET /hiring/candidates/v1, also as a filter, and GET /hiring/candidates/{id}/v1 (section 5.6). |
application_external_id |
The person in the job (what an ATS calls the application id) | The application in the hiring project this request names | Yes: GET /hiring/applications/v1, also as a filter (section 5.6). |
invitation_external_id |
The invitation to the assessment | The invitation to the assessment this request names | Yes, while the invitation is listed by GET /hiring/interviews/upcoming/v1. There is no filter (section 5.6). |
The three external_id fields follow the rule of section 8.8: optional plain text of 1 to 128 characters with no control characters, stored exactly as you send it, scoped to your company and not unique, set when the record is created and not changed afterwards. null counts as not sent. None of them is an idempotency key: sending a request again does not make it safe to repeat (section 8.6). The route that stages an invitation for a person to approve (POST /approvals/invite/v1, section 9.7) keeps none of the three and refuses a row that carries one (section 8.8).
- A project and an assessment for each id.
application_external_idis the id of the application in one hiring project, andinvitation_external_idthe id of the invitation to one assessment. So the request must name exactly one project, incom_project_ids(or the deprecatedcom_project_id), and exactly one assessment, incom_interview_set_ids(or the deprecatedcom_interview_set_id). The same id listed twice counts once, and the list and the deprecated single field count together:com_project_ids: [A]besidecom_project_id: Bnames two projects, and with anapplication_external_idthat is refused as ambiguous even though only the list is used (the same forcom_interview_set_idsbesidecom_interview_set_id). Copying one id to several applications or invitations would put one id on several records, so a request that names none or several is refused before anything is created, with the codes below. To use these fields for several projects or assessments, send one request for each.invitation_external_idalso needssend_invitetrue: withfalsethe request is refused. The reason differs by kind: an AI interview or a role-play that is not to be emailed makes no invitation, so there is nothing to keep the id on, while a Skill Test or a Technical Test makes the invitation without emailing it, and the rule still refuses the id. - Fill a blank, never overwrite. A request can land on a person, an application or an invitation that already exists: a person invited before, or a project the person is already in. The id is stored only if that record has none. If it already holds the same text, nothing changes and the result confirms it. If it holds a different text, that text stays, your id is not stored, and the result says so with
external_id_kept_existing,application_external_id_kept_existingorinvitation_external_id_kept_existing(section 4.5). The stored text is not shown. If the same address appears twice in one request, both rows land on the same records. The rule also holds when two requests meet: a request that reactivates an inactive invitation changes only the fields it owns, so it cannot erase an id that another request stored at the same moment. - An email is required. A candidate sent with only a
cv_linkis read in the background and the person's record is made later, so there is nothing to keep an id on. Any of the three ids on such a candidate is refused. - A failure to save an id never fails the invitation. The invitation is made, and the id that could not be saved is reported in
external_id_error_messages(section 4.5). Send the same request again to try once more: an id that is already stored as you sent it is confirmed, so nothing is stored twice.
Each refusal below is a 400, creates nothing, and ends its message with Nothing was created.. external_id_invalid and external_id_requires_email apply to a Skill Test and a Technical Test too, and so does invitation_external_id_requires_invite when send_invite is false (section 4.6.1); the other codes are for an AI interview, a role-play and a CV evaluation.
code |
When |
|---|---|
external_id_invalid |
One of the three ids is not plain text of 1 to 128 characters with no control characters, is only spaces, holds half of a surrogate pair (for example the lone code unit U+D800, sent as a JSON unicode escape), or is not a JSON string (a number, a list or an object). The message names the row and the field, for example candidates[1].external_id. |
external_id_requires_email |
A candidate with no email carries one of the three ids. |
application_external_id_requires_project |
application_external_id is sent and the request names no hiring project. |
application_external_id_ambiguous_project |
application_external_id is sent and the request names more than one hiring project. |
invitation_external_id_requires_invite |
invitation_external_id is sent and the request names no assessment, or has send_invite false. |
invitation_external_id_ambiguous_assessment |
invitation_external_id is sent and the request names more than one assessment. |
4.3.2 Limits on a candidate's values
Flowmingo checks every value it is going to store before it writes anything. A value that does not fit is refused with 400, the message names it (for example candidates[2].ats_candidate_id) and ends with Nothing was created., and no candidate, record or invitation exists afterwards. The message of a value that is too long gives its length and the limit, and no message repeats the value. Lengths are counted in characters, so an emoji counts once.
| Value | Longest |
|---|---|
ats_candidate_id |
250 characters. |
email |
250 characters, counted as Flowmingo keeps it, in lower case. A few letters become two characters when lower-cased, so an address that fits as you sent it can be too long once converted. |
name, firstname, lastname |
250 characters each. |
cv_link |
1,536 characters for an AI interview, a role-play or a CV evaluation, and the part after the last / (the file name) at most 1,024 characters. 2,048 characters for a Skill Test or a Technical Test. |
thumb_file_link |
2,048 characters. Not checked for a Skill Test or a Technical Test, which do not use a thumbnail. |
code |
When |
|---|---|
candidate_field_too_long |
A value is longer than the table allows. |
candidate_field_invalid |
A value is not text (an object or a list), contains a NUL (zero) character, or an entry of candidates is not a JSON object. An invitation_message with a NUL character is refused with the same code. When you send an ats_candidate_id with an assessment id, the record saved for it keeps a copy of name, source and thumb_file_link (section 4.5), and those three must be valid text: a NUL character, or half of a surrogate pair (for example the lone code unit U+D800, sent as a JSON unicode escape), is refused. An ats_candidate_id or an email that holds half of a surrogate pair (for example the lone code unit U+D800, sent as a JSON unicode escape) is refused with this code on a request for an AI interview, a role-play or a CV evaluation, and so is an email on a request for a Skill Test or a Technical Test, where it used to answer 500. |
A Skill Test or a Technical Test is held to the same limits, apart from the cv_link limit in the table and three differences: ats_candidate_id is not checked there because a test refuses it anyway, thumb_file_link is not checked because a test does not use a thumbnail, and the email is measured after the spaces around it are removed (a test ignores them), then in lower case (section 4.6.1).
4.4 Sample request
The ids in these samples are placeholders. Run unchanged with a real key, the request answers 404 and names the first id that is not yours, until you replace them with ids from your own organization.
curl -X POST https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"com_interview_set_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"],
"com_project_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"com_job_post_ids": ["c0ffee00-dead-4bee-8f00-0123456789ab"],
"candidates": [
{
"name": "Alex Doe",
"email": "alex@example.com",
"cv_link": "https://storage.googleapis.com/flowmingo-demo/cv/alex.pdf"
}
],
"invitation_message": "Hi Alex, please complete the short interview in the next 48 hours.",
"send_invite": true
}'
import fetch from 'node-fetch'
const response = await fetch('https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'fl_live_abcd12345.n8G3zPNw9gWkK1Kj'
},
body: JSON.stringify({
com_interview_set_ids: ['3f2504e0-4f89-41d3-9a0c-0305e82c3301'],
com_project_ids: ['a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'],
com_job_post_ids: ['c0ffee00-dead-4bee-8f00-0123456789ab'],
candidates: [
{
name: 'Alex Doe',
email: 'alex@example.com',
cv_link: 'https://storage.googleapis.com/flowmingo-demo/cv/alex.pdf'
}
],
invitation_message: 'Hi Alex, please complete the short interview in the next 48 hours.',
send_invite: true
})
})
const data = await response.json()
console.log(data)
import requests
response = requests.post(
"https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1",
headers={
"Content-Type": "application/json",
"X-API-Key": "fl_live_abcd12345.n8G3zPNw9gWkK1Kj",
},
json={
"com_interview_set_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"],
"com_project_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"com_job_post_ids": ["c0ffee00-dead-4bee-8f00-0123456789ab"],
"candidates": [
{
"name": "Alex Doe",
"email": "alex@example.com",
"cv_link": "https://storage.googleapis.com/flowmingo-demo/cv/alex.pdf",
}
],
"invitation_message": "Hi Alex, please complete the short interview in the next 48 hours.",
"send_invite": True,
},
)
print(response.json())
<?php
$payload = [
'com_interview_set_ids' => ['3f2504e0-4f89-41d3-9a0c-0305e82c3301'],
'com_project_ids' => ['a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'],
'com_job_post_ids' => ['c0ffee00-dead-4bee-8f00-0123456789ab'],
'candidates' => [
[
'name' => 'Alex Doe',
'email' => 'alex@example.com',
'cv_link' => 'https://storage.googleapis.com/flowmingo-demo/cv/alex.pdf',
],
],
'invitation_message' => 'Hi Alex, please complete the short interview in the next 48 hours.',
'send_invite' => true,
];
$ch = curl_init('https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
payload := map[string]any{
"com_interview_set_ids": []string{"3f2504e0-4f89-41d3-9a0c-0305e82c3301"},
"com_project_ids": []string{"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},
"com_job_post_ids": []string{"c0ffee00-dead-4bee-8f00-0123456789ab"},
"candidates": []map[string]any{
{
"name": "Alex Doe",
"email": "alex@example.com",
"cv_link": "https://storage.googleapis.com/flowmingo-demo/cv/alex.pdf",
},
},
"invitation_message": "Hi Alex, please complete the short interview in the next 48 hours.",
"send_invite": true,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest(
"POST",
"https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1",
bytes.NewReader(body),
)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", "fl_live_abcd12345.n8G3zPNw9gWkK1Kj")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
}
require 'net/http'
require 'json'
require 'uri'
uri = URI('https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request['X-API-Key'] = 'fl_live_abcd12345.n8G3zPNw9gWkK1Kj'
request.body = {
com_interview_set_ids: ['3f2504e0-4f89-41d3-9a0c-0305e82c3301'],
com_project_ids: ['a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'],
com_job_post_ids: ['c0ffee00-dead-4bee-8f00-0123456789ab'],
candidates: [
{
name: 'Alex Doe',
email: 'alex@example.com',
cv_link: 'https://storage.googleapis.com/flowmingo-demo/cv/alex.pdf'
}
],
invitation_message: 'Hi Alex, please complete the short interview in the next 48 hours.',
send_invite: true
}.to_json
response = http.request(request)
puts response.body
4.5 Sample response
The route answers with a results array: one entry for each candidate you sent, in the order you sent them.
{
"results": [
{
"email_address": "alex@example.com",
"cv_link": "https://storage.googleapis.com/flowmingo-demo/cv/alex.pdf",
"external_id": null,
"application_external_id": null,
"invitation_external_id": null,
"invitations": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"email": "alex@example.com",
"status_text": "pending"
}
],
"applications": [
{
"id": "2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
],
"submissions": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status_text": "need_interview"
}
]
}
]
}
This sample shows the main fields. Each entry has one invitation, application and result for each assessment and project you named, in the arrays invitations, applications and submissions. The older single-value fields invitation and submission (the first element of each array) are still returned beside them so integrations written against the earlier response keep working. New integrations should read the arrays. Each entry also carries the three external_id keys. They are null in this sample because the request in section 4.4 sent no id of yours (section 4.3.1).
These fields of an entry name your own ids and the records Flowmingo keeps for them:
| Field | What it holds |
|---|---|
ats_candidate_id |
The ats_candidate_id you sent for this candidate, exactly as you sent it. Present only when you sent one. |
record_id |
The id (a UUID) of the record Flowmingo saved for your ats_candidate_id under the first assessment you named. Present only when you sent an ats_candidate_id with an assessment id. Every request saves new records, even for an ats_candidate_id you used before, so record_id is different each time and sending a request again does not find the old one. It is returned in this response only: none of the read routes returns it. |
ats_records |
One entry for each assessment you named, in the order you named them, each with com_interview_set_id and the record_id saved under that assessment. The first entry is the one record_id repeats. An assessment id listed twice counts once. Present only with an ats_candidate_id, and not used for a Skill Test or a Technical Test. |
com_contact_id |
The candidate as a contact of your organization: your company's own record of the person, the one an external_id is kept on (section 4.3.1). It is returned on the entries of an AI interview, a role-play and a CV evaluation request, once the person's record was found or created for this request, so it is missing from the entry of a candidate who failed before that (read error_message). The entry of a Skill Test or a Technical Test never carries it, even though a contact may be created for the person (section 4.6.4). |
tal_candidate_entry_id |
Not used by this route: it is never set, so it does not appear in the answer. A candidate sent with only a CV link is described in candidate_entry. |
external_id, application_external_id, invitation_external_id |
The ids you sent for this candidate that are now stored, exactly as you sent them. Always present: null when you sent none, when the record is not there (the step that failed says why in its own error field), or when the record already held a different id, which was kept. |
external_id_kept_existing, application_external_id_kept_existing, invitation_external_id_kept_existing |
Present, and true, only when the record already held a different id: that one stays, yours was not stored, and the stored one is not shown. |
external_id_error_messages |
Present only when saving one of the three ids failed. It is keyed by field name (external_id, application_external_id, invitation_external_id), and the invitation itself is fine. Send the same request again to try once more. |
The record behind record_id holds the ats_candidate_id, the address, the names and the CV link of the candidate row, and a copy of its name, source and thumb_file_link. Nothing else you send inside a candidate is kept in it.
When something could not be created for a candidate, the entry carries a message instead: error_message for the candidate as a whole, and invitation_error_messages, application_error_messages and submission_error_messages, which are keyed by the assessment or project id. The older single-value forms are invitation_error_message, application_error_message and submission_error_message, and candidate_entry_error_message appears when a CV-only candidate could not be queued. An invitation that was created but will not be emailed because its assessment is not published is not an error: it is reported in warnings (below). That is the only case warnings reports: an email that does not go out for another reason (an address on the unsubscribe list, a failed address check, a refusal by the mail provider) is never listed there, and is reported as described at the end of this section. Check these fields as well as the status code.
warnings: an invitation that was created but will not be emailed yet. An AI interview or a role-play is emailed only while its assessment is published (status 1 in the assessments list, section 5.6). This route does not refuse an invitation to one that is a draft, inactive or archived: it creates the invitation and answers 201, and nobody is emailed. The entry of each candidate who holds such an invitation then carries warnings, a list with one item for each assessment concerned:
"warnings": [
{
"code": "assessment_not_active",
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"message": "Assessment \"Sales Assessment\" is a draft, so this invitation is not emailed: it is sent once it is published in Flowmingo, and never if that is more than 7 days after it was due (normally when this call was made)."
}
]
| Field | What it holds |
|---|---|
code |
assessment_not_active is the only code today. Branch on it, never on the wording of message, and ignore a code you do not know. |
com_interview_set_id |
The assessment that is not published. |
message |
A sentence for a person to read. It names the assessment and says whether it is a draft, inactive or archived. |
Once the assessment is published (a draft) or active again (an inactive or an archived one), an invitation that is still pending becomes eligible for the sender, unless 7 days have passed since it was due. Eligible is not the same as queued: the sender still skips an invitation whose candidate has already started or finished that assessment, and one whose address failed our address check. An email that is queued is sent asynchronously, with no promise of when or whether it is delivered. The message of a warning says what happens once the assessment is active; neither it nor the publication proves that an email was queued or delivered. The invitation is due when you made the call, or later for a candidate far down a long list: Flowmingo spaces out the sends of a request with more than 50 candidates, adding about 5 minutes for each further 50 places, so the 1,000th candidate of one request is due about 100 minutes after the call, and a longer list waits longer still. To choose the time yourself for an AI interview or a role-play, send send_at (section 4.9). An invitation that is still unsent 7 days after it was due is not sent automatically after that. Once it is marked failed, an explicit resend (a later invite request for the same candidate) can make it eligible again, with a new due time. What the dashboard then shows depends on the case. For an invitation that is still pending on an active assessment, whose candidate has not started or finished it, a check normally marks it as failed. An invitation whose candidate has already started or finished that assessment is not marked failed: it is marked Started and it is not sent. The check runs only for an active assessment, so for an assessment that is still a draft the invitation simply stays unsent, and once the assessment is active the check can mark it failed in the same way (the paragraph below says more). Do not count 7 days from your call as a precise deadline. warnings is absent when there is nothing to warn about. It is also absent when send_invite is false (nothing was to be emailed), and for a CV evaluation, a Skill Test or a Technical Test: an unpublished test is refused with 400 (section 4.6.1). Publish the assessment first, or read warnings before you tell anyone an invitation was emailed.
An internal failure never shows its own text. When a step fails inside Flowmingo, the message for that step (error_message, invitation_error_messages, application_error_messages, submission_error_messages or external_id_error_messages) reads This step failed because of an internal error (reference 1a2b3c4d). Try again later; if it keeps failing, contact Flowmingo support and quote this reference. The reference is eight hexadecimal characters and finds the failure in our log. Nothing else about the failure is shown: no database text and no system text. A message we wrote for you stays as it is, for example Email already invited or Candidate was already in this project. This covers the entries of an AI interview, a role-play and a CV evaluation, and external_id_error_messages on every kind of request; it does not change the error field of a Skill Test or a Technical Test (section 4.6.4). Branch on which message field is present, not on its wording.
A 201 means the request was processed, not that every email was delivered. Read the result of each candidate, not only the status: what was refused is listed there, in the message fields above, and what will not be emailed because the assessment is not published is listed in warnings. No warnings does not prove that an email was sent or delivered. For a Skill Test or a Technical Test, the email of each eligible invitation you asked to send (send_invite true) is attempted before the answer is sent: read email_delivery on each candidate, which says what happened, including not_requested and the skipped_* values for a duplicate or an invitation that cannot be emailed (section 4.6.5). For an AI interview or a role-play, the sender queues the email asynchronously for an invitation it picks up (send_invite true and the assessment published; it can still skip one, as described above). A queued email can leave before or after the answer reaches you, can be delayed, and may never be delivered, so do not rely on the order or the timing. An email that fails is reported, on a best effort basis, as an invitation.status.update event with the sub event invitation_email_failed. Its reason is unsubscribed (the address was already on our unsubscribe list when the email was queued) or what the mail provider reported, for example invalid_email, bounced, dropped or blocked. You receive the event by webhook, or from the events feed (section 7), which needs a key with read_hiring, keeps events for about 30 days, and cannot be read by a key limited to some projects. The feed removes candidate_email from each event unless the key also has read_candidates, and you need that address to match an event to your candidate by candidate_email and interview_set_id: it carries no ats_candidate_id. Not every failure is reported by an event, so do not treat the absence of one as delivery: an invitation still unsent 7 days after it was due is not sent automatically after that (an explicit resend, once it is marked failed, can make it eligible again), and is normally marked failed in the dashboard (the check that does so runs for an active assessment, and an invitation whose candidate has already started or finished that assessment is marked Started instead); a failure inside Flowmingo while the email is prepared, and an invitation whose content our checks reject, show on the invitation in the dashboard; and later reports from the mail provider that a message was deferred or that the person unsubscribed are not sent as events.
4.5.1 Sample: a request with your own ids, and its result
This request names one assessment and one hiring project, so all three ids can apply. It also sends an ats_candidate_id. The ids are placeholders, as in section 4.4.
curl -X POST https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"com_interview_set_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"],
"com_project_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"candidates": [
{
"name": "Alex Doe",
"email": "alex@example.com",
"ats_candidate_id": "ATS-10442",
"external_id": "CAND-77120",
"application_external_id": "APP-48213-0031",
"invitation_external_id": "INV-90017"
}
],
"send_invite": true
}'
The result of the one candidate:
{
"results": [
{
"ats_candidate_id": "ATS-10442",
"record_id": "f1a3b5c7-9d0e-4f2a-8b4c-6d8e0f1a2b3c",
"ats_records": [
{
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"record_id": "f1a3b5c7-9d0e-4f2a-8b4c-6d8e0f1a2b3c"
}
],
"email_address": "alex@example.com",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"external_id": "CAND-77120",
"application_external_id": "APP-48213-0031",
"invitation_external_id": "INV-90017",
"invitations": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"email": "alex@example.com",
"status_text": "pending"
}
],
"applications": [
{
"id": "2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3",
"com_project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
],
"submissions": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status_text": "need_interview"
}
]
}
]
}
Send the same request again and each id that is already stored as you sent it is confirmed in the result, while record_id is the id of a new record (section 4.5).
4.6 Skill Tests and Technical Tests
A Skill Test (assessment_type skill_test) or a Technical Test (technical_test) is sent through the same route, POST /interview/candidate/invite/v1, and behaves like Send in the Flowmingo dashboard: the test's invitation email is addressed to the candidate's email address (when send_invite is true; section 4.6.5 says what can happen to it), and the candidate is added to the pipeline of the hiring project the test is attached to. For an application that this call creates, the recruiter's Source reads "API added:
4.6.1 The request
- One test per request.
com_interview_set_ids(or the deprecatedcom_interview_set_id) holds that one id and nothing else. A test beside any other assessment, or two tests, is refused with400and the codeassessment_test_requires_single_set, and nothing is created. Send each test, and each interview, in a call of its own. - The test must be published and attached to a hiring project of yours. An unpublished test is refused with
400and the codeskill_test.variants.published_requiredortechnical_test.published_required. Publish it in the Flowmingo dashboard first. - Name the project, or leave it out. Put the project in
com_project_ids(one id) or in the deprecatedcom_project_id. If you name none, the test's single active project is used. A test that is attached to no active project answers400with the codeproject_required, and one that is attached to several answers400withproject_ambiguous: send the id of the project you mean. Naming two projects answers400withassessment_test_requires_single_project. A test needs no job post, socom_job_post_idsis not used (an id that is not yours is still a404). - Every candidate needs an email, at most 100 candidates per call. A candidate without an email is refused with
400and the codeassessment_test_email_required(the message names the row, for examplecandidates[2]); a candidate without an email who carries one of the three ids of section 4.3.1 is refused withexternal_id_requires_emailinstead. More than 100 candidates are refused withassessment_test_too_many_candidates. A CV link alone does not invite anyone to a test.name,firstname,lastnameandcv_linkare used.sourceandthumb_file_linkare not. An address that ends in@placeholder.local, or a preview address, is refused with400and the codeskill_test_invite.placeholder_recipient_not_allowedortechnical_test_invite.placeholder_recipient_not_allowed. - A candidate row must be well formed. Each entry of
candidatesmust be a JSON object. Anemail, when given, must be a valid address (spaces around it are ignored). Sendname,firstname,lastname,cv_link,thumb_file_linkandsourceas JSON strings, and keep<and>out of a name. A row that breaks a rule is refused with400, the message names the row and the first problem (for examplecandidates[2]: email must be an emailorcandidates[2] must be an object), and nothing is created. The message does not repeat the value you sent. An empty or whitespace-only value passes this check, and a blankemailis still refused withassessment_test_email_required(withexternal_id_requires_emailwhen the row carries one of the three ids); leave out optional fields you do not use. A list of more than 100 candidates is refused withassessment_test_too_many_candidatesbefore the rows are checked. - Fields a test does not take. A value in
ats_candidate_id(on any candidate), a value inmetadataand apractice_modeother thannullare refused with400and the codeassessment_test_field_not_supported, and nothing is created. Anullin any of the three is accepted and ignored. Candidates are identified by their email. Candidates can still practise before either kind of test: the invitation email has a practice link.practice_modeis the field for a practice invitation to an AI interview, and it does not apply here. - Your own ids. A test takes
external_id,application_external_idandinvitation_external_id, as JSON strings, under the rules of section 4.3.1 (a blank is filled, and a different id already there is kept). The application is the one in the project the test is sent through, which is the project you named or the test's single project. The invitation is the test's invitation for that address, found again when you repeat a call. A test is always one assessment and one project, so the refusals for several projects or assessments cannot arise. A row that carries an id but no email is refused withexternal_id_requires_email.invitation_external_idneedssend_invitetrue: withfalsethe request is refused with400and the codeinvitation_external_id_requires_invite, and nothing is created. An id that is not valid is refused with400and the codeexternal_id_invalid, and nothing is created.ats_candidate_id,metadataandpractice_modestay refused, as the bullet above says. - Lengths. The limits of section 4.3.2 apply to a test too (
cv_linkmay be up to 2,048 characters here).thumb_file_linkis not checked, because a test does not use a thumbnail, and theemailis measured after the spaces around it are removed, then in lower case. A value that is too long, holds a NUL character or, for anemail, holds half of a surrogate pair (for example the lone code unit U+D800, sent as a JSON unicode escape), is refused with400and the codecandidate_field_too_longorcandidate_field_invalid, and nothing is created. - Fields a test does take.
invitation_messageis added to the email, and placeholders in it are filled in for each candidate, among them{{candidate_name}},{{candidate_first_name}},{{candidate_last_name}},{{company_name}},{{position_title}}and{{sender_name}}. A name in double braces that is not a supported placeholder is replaced by nothing, and a brace that is not closed can make the email for that candidate fail.attachment_sys_file_idsare files available to you (otherwise400with the codeattachment_not_yours); they are checked but not sent (section 4.3).send_invitedefaults totrue;falsecreates the invitations without emailing anyone, and a later call withsend_invitetruecan email an invitation that was not emailed before, as long as it is still open and the candidate has not started (section 4.6.5 lists what is skipped). - The email is Flowmingo's standard invitation for that kind of test. The API cannot change its subject or language, the address its link points to, or add per-candidate accommodations.
- The emails are attempted during your request, one candidate after another, not in later batches as they are for an interview. This applies to each eligible invitation when
send_inviteistrue: withfalseno email is attempted, and an invitation that already exists, is closed or has been started can be skipped (section 4.6.5). A call with many candidates therefore takes longer, and the answer tells you what happened to each one. If your client gives up first, read section 4.6.5 before you repeat the call.
4.6.2 Who the invitation goes out as
An invitation to a test is issued on behalf of a person: the user the key belongs to, or else the person who created the key. Their name is on the email when one is attempted, and their access to hiring projects applies. A project of your company that this person cannot open answers 403 (section 2.9). A project that is archived answers 404 (Project ... not found), and so does a test that is not attached to the project you named (Project Skill Test not found or Project Technical Test not found). During the grace period after the creator has left (section 2.10), sending a Skill Test or a Technical Test also stops at once and answers 404 Project ... not found, because those two go out as the person and read their access today, not the access they left with. A key with no creator on record is refused with a 403 (section 11), and nothing is created: create the key from a signed-in account, or ask an Owner or an Admin to take it over.
4.6.3 Sample request
The ids are placeholders. Run unchanged with a real key, the request answers 404 and names the first id that is not yours.
curl -X POST https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"com_interview_set_ids": ["8e7a1c52-3d4b-4f90-a6e1-2b9c5d7f0a13"],
"com_project_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"candidates": [
{ "name": "Alex Doe", "email": "alex@example.com" },
{ "firstname": "Sam", "lastname": "Lee", "email": "sam@example.com" }
],
"invitation_message": "Hi, please take this short test in the next three days."
}'
4.6.4 Sample response
The answer is 201 with a results array: one entry for each candidate row you sent, in the order you sent them (the sample answers the request in section 4.6.3: one entry for Alex and one for Sam). Each entry carries assessment_invites, a list that always holds one invitation, instead of the interview fields (invitations, applications, submissions and their older single-value forms are not present). It also carries the three external_id keys of section 4.3.1: null unless you sent that id. When a record already held a different id, or saving an id failed, the entry carries the matching *_kept_existing flag or external_id_error_messages, as in section 4.5.
{
"results": [
{
"email_address": "alex@example.com",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"external_id": null,
"application_external_id": null,
"invitation_external_id": null,
"assessment_invites": [
{
"com_interview_set_id": "8e7a1c52-3d4b-4f90-a6e1-2b9c5d7f0a13",
"assessment_type": "skill_test",
"email": "alex@example.com",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"tal_invite_id": "6b1d2f9a-4c7e-4a38-9f50-d3e8c1a7b264",
"status": "sent",
"email_delivery": "queued",
"reused_existing": false,
"reused_from_other_project": false,
"expires_at": "2026-11-03T09:00:00.000Z",
"max_attempts": 1,
"error": null
}
]
},
{
"email_address": "sam@example.com",
"tal_candidate_id": "2d8f5a1c-6b3e-4c97-8a4d-9e0f1b7c3a52",
"external_id": null,
"application_external_id": null,
"invitation_external_id": null,
"assessment_invites": [
{
"com_interview_set_id": "8e7a1c52-3d4b-4f90-a6e1-2b9c5d7f0a13",
"assessment_type": "skill_test",
"email": "sam@example.com",
"tal_candidate_id": "2d8f5a1c-6b3e-4c97-8a4d-9e0f1b7c3a52",
"tal_invite_id": "c4a9e7d3-5b21-4f68-9d0a-7e3b8c1f2a46",
"status": "sent",
"email_delivery": "queued",
"reused_existing": false,
"reused_from_other_project": false,
"expires_at": "2026-11-03T09:00:00.000Z",
"max_attempts": 1,
"error": null
}
]
}
]
}
Field of assessment_invites[] |
Meaning |
|---|---|
com_interview_set_id, assessment_type |
The test, and its kind: skill_test or technical_test. |
email, tal_candidate_id |
The recipient address of the invitation, and the candidate. It does not say that an email was sent to that address: read email_delivery. |
tal_invite_id |
The invitation. null when none was created; error says why. |
status |
Where the invitation stands after the call: sent or opened (not started yet), in_progress, completed, expired, exhausted (all attempts used), cancelled, or not_created when there is none. More values may be added later, so do not treat the list as closed. |
email_delivery |
What happened to the email (section 4.6.5). |
reused_existing |
true when the invitation already existed when this row was handled (a repeated call, or an address repeated in the same request). It does not promise that the invitation is unchanged: repeating a Skill Test call can bring an invitation the candidate has not started up to the test's current settings (its paper, duration, attempts and expiry), and a reused invitation can read email_delivery queued when this call handed over the email of an invitation that an earlier call had created but not emailed (for example with send_invite false). Read status, email_delivery, expires_at and max_attempts in the answer. |
reused_from_other_project |
Technical Test only. true when the invitation returned for this row hangs on a different attachment of the test to a hiring project than the one used for this request (the one you named, or the test's single project), and was reused as it is. That is normally a different hiring project; it is also true if the same project had the test detached and attached again. The invitation stays where it was, whatever its status; read status and email_delivery separately. Always false for a Skill Test, which refuses a conflicting invitation with 409 (section 8). |
expires_at, max_attempts |
When the invitation expires (or null), and how many attempts the candidate has. |
error |
A short reason when the row was refused or its email failed, otherwise null. When no invitation was created, the entry also carries it as error_message. |
The answer never contains the candidate's invitation link or token. Those are the candidate's own access, so they are not returned to your system or its logs. A 201 does not mean every candidate was emailed: read email_delivery for each one.
4.6.5 What happened to each email, and repeating a call
email_delivery is one of these. A value that starts with skipped_ means no email was sent for that row; it is not an error.
email_delivery |
Meaning |
|---|---|
queued |
This call passed the invitation email to our email service. |
already_queued |
Usually: the email for this invitation was handed over for sending earlier, by an earlier call or from the dashboard. Nothing was sent now. In a rare case (the invitation was deleted while your call was running) this value is also what you read although no email was handed over, so it is not proof of an earlier send. |
delivery_in_progress |
The delivery of this invitation is not settled: another send may be under way, or an earlier attempt failed or timed out and its mark on the invitation has not yet expired (about ten minutes from the start of that attempt). This call sent nothing. See "If a call fails, times out or reads delivery_in_progress" below. |
not_requested |
send_invite was false. The invitation exists and no email was sent. A repeat of an address in the same request reads skipped_duplicate_in_request instead; read the first row for it. |
skipped_already_started |
The candidate has started or finished this Skill Test, or has used attempts on it, so nothing was sent. A used-up Skill Test invitation reads this rather than skipped_not_unstarted. |
skipped_attempt_in_progress |
The candidate is taking this Technical Test right now. |
skipped_not_unstarted |
The invitation is no longer open (for example cancelled or expired, or, for a Technical Test, used up or finished) and was never emailed. The API leaves it as it is. |
skipped_duplicate_in_request |
The same address is in your request more than once. That row shares the first row's invitation and causes no send of its own. It does not say how the first row went: read the first row's email_delivery, which can be queued, not_requested or a failed: value. |
failed:<reason> |
The email may or may not have gone out for this candidate, and error carries a short reason. Usually the email service refused it and nothing was sent, but a failure can also happen after the email was handed over (for example when we could not record it), and failed:timeout_possibly_delivered means we stopped waiting for the email service. Never read a failed: value as proof that nothing was sent, with two exceptions: failed:skill_test_invite.recipient_unsubscribed and failed:technical_test_invite.recipient_unsubscribed mean the person has opted out of email: this call sent nothing and recorded no delivery. The invitation itself exists, and this attempt did not mark it emailed or change its status: a new invitation reads status sent from the moment it is created, and an existing one keeps the status it had. Once the person has opted back in, a later call can email it if it is still open and not started. |
not_attempted |
The answer holds no result for this address (status not_created, with an error). Nothing was sent for it. |
Repeating a call is built to be safe, but it is not a guarantee. A candidate who already holds the invitation is returned with reused_existing true, and the API tries not to email them twice:
- If the email was already handed over earlier, the row reads
already_queued. If the invitation was created earlier withsend_invitefalse, this call attempts the email, unless it is closed or the candidate has started (the next bullet and the table above). - If the candidate has started or finished the test, nothing is sent (
skipped_already_started,skipped_attempt_in_progressorskipped_not_unstarted). - The API never revives an invitation. A cancelled or used-up invitation gets no new link, no new attempts and no email, and neither does an expired Skill Test invitation;
statusshows how it stands. A Technical Test invitation that was never emailed and still readssentoropenedcan be emailed even after itsexpires_athas passed, so checkexpires_atbefore you rely on an old invitation. To give a candidate another go, do it from the Flowmingo dashboard. - Two calls for the same candidate at the same moment normally email once: the other reads
delivery_in_progressoralready_queued. - Repeating a Skill Test call can update an invitation the candidate has not started to the test's current settings (paper, duration, attempts and expiry).
reused_existingtruedoes not promise that the invitation is unchanged. - The protection against a second email is temporary. While an email is being sent the invitation carries a mark, and a repeat that finds the mark reads
delivery_in_progressand sends nothing. The mark lasts about ten minutes from the start of the attempt, not from when the email service answered. After that, a repeat can send another copy when the first email did go out but was never recorded as sent. That applies afterfailed:timeout_possibly_delivered, after any otherfailed:value and after adelivery_in_progressthat does not clear.
If a call fails, times out or reads delivery_in_progress. The answer does not say whether the email went out, and none of the read routes returns an invitation's email_delivery or status, so your system cannot look it up. Check the invitation in the Flowmingo dashboard, or ask the candidate, before you repeat the call. If you cannot tell, wait about ten minutes from the start of the first call and then repeat it, accepting that the candidate may then get a second email.
A person holds one invitation per test. A Technical Test candidate who already has one through another hiring project gets that invitation back. For a Skill Test, a candidate who already holds an invitation through another hiring project makes the call stop with 409 and the code skill_test_invite.already_invited_to_test. The call is not undone. It sends no email, but by then the candidates and their pipeline entries for every row of the request had been created, and the invitations created for the rows before the conflicting one remain, not emailed by this call (the rows after it have a candidate and a pipeline entry, and may or may not hold an invitation yet). Repeating the same request unchanged stops at the same row with the same 409, so none of those invitations is emailed that way. Take the conflicting candidate out of the request and repeat it: the other candidates' invitations are found again, and for the ones that are still open and not emailed yet the email is attempted (a closed invitation, or one emailed earlier, is skipped as usual).
4.6.6 Daily limits
A Skill Test or a Technical Test has two limits per UTC day, counted across all your API keys together: 1000 invitations to any one test and 5000 for your organization. The day is the UTC calendar day, and a limit resets at 00:00 UTC. Each check adds the candidates in your request (each distinct address once) to the invitations already sent that day by any route, the dashboard included, so a request can be refused when the day's invitations were also sent from the dashboard. The check is made before your existing invitations are looked at, so even a repeat of a call whose candidates were all emailed earlier can be refused with 429 once the day's allowance is used up. After the call, the place of a candidate who was not emailed (already_queued or any skipped_ value) is given back. The place of delivery_in_progress and of every failed: value is not, because the email may have gone out. The limits are not checked when send_invite is false.
- At a limit the request answers
429with the codeassessment_test_invite_limit_reached, and nothing is sent. This429is not a short pause: try again after 00:00 UTC, or send fewer candidates. - If the limit cannot be checked just now, the request answers
503with the codeassessment_test_invite_limit_unavailable, and nothing is sent. Try again in a moment. We would rather refuse than send past a limit.
4.6.7 What it costs
Sending the invitation costs nothing. Nothing is charged when the API sends a Skill Test or a Technical Test, just as nothing is charged when you press Send in the dashboard.
A test that your organization has set up as sponsored can use your organization's wallet when a candidate actually starts it, under the same rules as an invitation sent from the dashboard: the wallet balance, your spending cap if you set one, and one charge per candidate and test. The API key does not choose this, and the request has no field for it: it follows the test's own settings in the dashboard, and sending with the API changes none of those rules.
4.7 Hand over interview recordings
If you ran an interview yourself, for example a Teams or Zoom meeting or a phone call, and you have the recording, two routes turn it into a scored result. Flowmingo transcribes the recording, splits it into questions and answers, and scores it like an AI Interview. Both routes need the scope submit_recordings (section 2.3). Evaluating recordings is part of the Scale plan and above: below it, both routes answer 403 with the code recording_requires_scale and nothing is made. A recording can be at most 1 GiB and 120 minutes long.
The order is always the same:
POST /interview/recording/uploads/v1gives you an upload URL for each file (section 4.7.2).- You
PUTthe bytes of each file to its URL (section 4.7.2). POST /interview/recording/v1turns the uploaded files into submissions of a recording evaluation (section 4.7.3).- You follow each recording and read its score with the routes you already know (section 4.7.5).
4.7.1 What you need
- The
submit_recordingsscope (section 2.3). A key does not gain it automatically: tick Submit interview recordings on the key. - A key that belongs to a person. An upload belongs to the person the key belongs to (the key's user, else the person who created the key) and cannot be used by anyone else, and the submissions are created as that person. A key with no creator on record is refused with a
403(section 11). Nothing is made: create the key from a signed-in account, or ask an Owner or an Admin to take it over. - A key whose creator can open the assessment. The key acts with its creator's permissions (section 2.9). If the creator is limited to some projects,
POST /interview/recording/v1accepts only a recording evaluation that they made, or that is linked to a project they can open; any other answers403("The person who created this API key does not have access to that hiring project, candidate or assessment result ..."). A creator with access to all hiring projects is not narrowed. The upload route (step 1) names no assessment, so it only needs a creator who can open a hiring project. - A recording evaluation to hold the criteria. It is an assessment with
set_type8. The criteria you give it (section 3.2.2) guide how each recording is scored. Its saved questions do not: the questions in a report are the ones the interviewer asks in the recording. Make it once for each role withPOST /interview/set/v1and"set_type": 8(scopecreate_set, sections 3.1 to 3.4). It must belong to your organization, and itsidis thecom_interview_set_idof step 2.GET /hiring/interview-sets/v1lists your existing ones when you sendassessment_type=recording(section 5.6). Below the Scale plan, making one is refused with403recording_requires_scaletoo. - The right to record. Step 2 asks you to confirm that you have the right to record and analyze these interviews and that the people in them were told (
consent_confirmed, section 4.7.3).
4.7.2 Step 1: get upload URLs, then upload the files
POST /interview/recording/uploads/v1 takes the files you are about to upload, with their names and sizes. The body holds one list:
| Field | Type | Required | Description |
|---|---|---|---|
files |
list of objects | Yes | The recordings you are about to upload, 1 to 50 in one call. |
files[].file_name |
string | Yes | The name of the file, with its extension: mp4, mov, webm, mkv, avi, wmv, flv, m4a, mp3, wav or ogg, in any letter case. 1 to 1,024 characters, without a NUL character. It is kept as the name of the upload. It is not used to find the candidate. |
files[].file_size |
integer | Yes | The size of the file in bytes: a whole number above 0 and at most 1 GiB (1,073,741,824 bytes). The size you send is a promise: the real size is read from storage when you create the submissions, and a file over the limit is refused and deleted. |
curl -X POST https://apis.flowmingo.ai/company/integration/interview/recording/uploads/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"files": [
{ "file_name": "aisha-rahman-interview.mp4", "file_size": 183500000 },
{ "file_name": "notes.pdf", "file_size": 52000 }
]
}'
The answer is 201 when the call itself worked. Every file you sent is in uploads or in rejected, found by its index (the first file is 0), so read rejected before you upload:
{
"uploads": [
{
"index": 0,
"upload_id": "d2c4e6a8-1b3d-4f5a-9c7e-0a2b4c6d8e1f",
"signed_url": "https://storage.example.com/2026/10/21/1/aisha-rahman-interview.mp4?X-Amz-Signature=...",
"fallback_url": null,
"content_type": "video/mp4"
}
],
"rejected": [
{
"index": 1,
"code": "unsupported_extension",
"message": "\".pdf\" is not a supported recording type. Use one of: webm, mp4, mov, m4a, mp3, wav, ogg, mkv, avi, wmv, flv."
}
]
}
| Field | What it holds |
|---|---|
uploads[].upload_id |
Send this as upload_id in step 2. It is good for one submission. |
uploads[].signed_url |
Where to PUT the bytes of the file. It is valid for two hours. |
uploads[].fallback_url |
The same upload on a second host, for networks that cannot reach signed_url. Use it only if signed_url cannot be reached. null when none is configured. |
uploads[].content_type |
The Content-Type header to send with the PUT, exactly as given. |
rejected[].code |
Why a file was refused: file_name_invalid (missing, blank, over 1,024 characters or with a NUL character), unsupported_extension (not one of the recording types above), file_size_required (missing, 0, negative, fractional or not a number), file_too_large (over 1 GiB) or upload_unavailable (our side: send this file again in a moment). |
rejected[].message |
The same in words. |
A refused file costs the others nothing. Now send the bytes of each file to its signed_url, a plain HTTP PUT with the file as the raw body and the content_type from the answer as the Content-Type header. No other authentication is needed, so do not add your API key to this request:
curl -X PUT -H 'Content-Type: video/mp4' --upload-file aisha-rahman-interview.mp4 \
'https://storage.example.com/2026/10/21/1/aisha-rahman-interview.mp4?X-Amz-Signature=...'
An upload that you never use is simply left.
4.7.3 Step 2: create the submissions
POST /interview/recording/v1 turns the uploaded files into submissions of your recording evaluation. For each entry Flowmingo checks that the file really arrived and is within 1 GiB, finds the candidate by email address or creates them, creates the submission as the person your key belongs to, and queues the recording.
| Field | Type | Required | Description |
|---|---|---|---|
com_interview_set_id |
UUID | Yes | The recording evaluation the recordings are scored against (section 4.7.1). |
consent_confirmed |
boolean | Yes | Must be the JSON value true. It says that you have the right to record and analyze these interviews and that the people in them were told. Anything else, including the text "true", is refused with consent_required and nothing is created. The consent is stored with every submission. |
items |
list of objects | Yes | The recordings to turn into submissions, 1 to 50 in one call. |
items[].client_ref |
string | Yes | Your own id for this recording: 1 to 128 characters of letters, digits and . _ : -. Unique within the request. Do not reuse one for a different recording. |
items[].upload_id |
UUID | Yes | The upload_id that step 1 returned for this file, after you have PUT its bytes. Each upload backs one submission. |
items[].email |
string | Yes | The candidate's email address. It identifies the candidate: it is matched exactly as sent, letter case included, with the candidate Flowmingo already has, and a new candidate is made when there is none. Spaces around it are removed. |
items[].firstname |
string | Yes | The candidate's first name: at most 250 characters, without < or >. |
items[].lastname |
string | Optional | The candidate's last name. Leave it out for a person with one name. At most 250 characters, without < or >. |
items[].language |
string | Optional | The language spoken in the recording, as a short code such as en, bn or es: at most 10 characters. It is used to transcribe the recording. Leave it out for English. Bengali (bn) is read as Indian Bengali (bn-IN), with English as a second guess. |
items[].external_id |
string | Optional | Your own id for the submission this entry creates: plain text of 1 to 128 characters with no control characters, stored exactly as you send it, scoped to your company and not unique (section 8.8). It is set when the submission is created and cannot be changed afterwards. null counts as not sent. It is separate from client_ref: client_ref only tells a retry from a new recording and is not kept on the submission, and external_id is never used to recognise a retry. An entry whose external_id is not valid is rejected on its own with the code invalid_external_id (section 4.7.4). |
curl -X POST https://apis.flowmingo.ai/company/integration/interview/recording/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"consent_confirmed": true,
"items": [
{
"client_ref": "oracle-req-8841-cand-17",
"upload_id": "d2c4e6a8-1b3d-4f5a-9c7e-0a2b4c6d8e1f",
"email": "aisha.rahman@example.com",
"firstname": "Aisha",
"lastname": "Rahman",
"language": "en",
"external_id": "interview-8841-round-2"
},
{
"client_ref": "oracle-req-8841-cand-18",
"upload_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"email": "sam.lee@example.com",
"firstname": "Sam",
"lastname": "Lee"
}
]
}'
The answer is 201 when the call itself worked, with one result for each entry, in the order you sent them (index starts at 0). Each entry is checked on its own, so a bad entry costs only itself. Look at state of every result: here the first recording was queued, and the second file had not been uploaded yet.
{
"results": [
{
"index": 0,
"client_ref": "oracle-req-8841-cand-17",
"state": "accepted",
"submission_id": "6d8f0b2e-4c5a-4b7f-9e3d-8a0c2b4d6f9e",
"external_id": "interview-8841-round-2"
},
{
"index": 1,
"client_ref": "oracle-req-8841-cand-18",
"state": "rejected",
"code": "upload_not_completed",
"message": "The file has not arrived yet. PUT the bytes to signed_url, then send this request again."
}
]
}
state |
Meaning |
|---|---|
accepted |
The submission was created and the recording is queued to be transcribed and scored. submission_id is the submission, and external_id is the id you sent for this entry (null when you sent none). |
duplicate |
This client_ref had already created a submission from this upload, within the last seven days. submission_id is the original, and external_id is the id the original holds, which can differ from what this retry sent (null when it holds none). |
rejected |
Nothing was created for this entry. code says why and message says it in words (section 4.7.4). The entry has no submission_id and no external_id. |
external_id on a submission. The external_id of an entry is stored on the submission it creates and returned in the result of the entry. You can read it again on the submission with GET /hiring/submissions/{id}/v1, GET /hiring/submissions/{id}/detail/v1 and the results list (section 5.2). A submission made any other way holds null. The results list has no external_id filter yet.
Sending the same request again is safe in the normal case, with three limits. An entry whose client_ref already created a submission from the same upload comes back as duplicate with the original submission_id, so a repeat after a timeout does not create a second submission.
- Seven days, and one API key. Flowmingo remembers a
client_reffor seven days after the submission was created, and only for the API key that sent it. After that a repeat does not come back asduplicate. For about thirty days from the creation the upload is still recorded as used, and a repeat is thenrejectedwithupload_already_used(read its row in section 4.7.4 before you act on it). Do not repeat an entry that is older than seven days: look its submission up instead (section 4.7.5). - The check can fail. If Flowmingo cannot check whether the recordings were already handled before it starts, the call answers
503with the codeidempotency_store_unavailableand creates nothing. If that check fails part of the way through, the call answers201: the entries already handled keep their state, and the entries it could not check come backrejectedwith the codeidempotency_store_unavailableand nothing created for them. Send those again. - A rare case can create a second submission. If a submission was created but Flowmingo could not save the record of it, a repeat of that entry can create a second submission. When you do not know whether an entry was created, for example because your request timed out, look for a submission of that candidate in the results of the recording evaluation (section 4.7.5) before you send the entry again.
A retry gets the original's id. A duplicate shows the external_id the original submission holds, whatever the retry sent. Sending a different external_id with the same client_ref and upload_id changes nothing on the submission.
A duplicate is shown only while you can still open the original. The original is held to the same rule as the submission reads (section 2.9): the person your key belongs to must still be able to open it. If that person has lost access to the assessment since, the entry is rejected with the code duplicate_not_accessible, and neither the submission_id nor the external_id is shown. The same happens when the original cannot be found any more, because its submission or its assessment was deleted: nothing can be read of it, so nothing is said about it, and the answer never shows an external_id of null for it. If the details of the original cannot be read at that moment, the entry is rejected with the code duplicate_read_failed: nothing was created again, and the same entry sent again in a moment answers duplicate. A key whose creator can open every hiring project is not narrowed by the access rule, but it gets duplicate_not_accessible too for an original that no longer exists.
4.7.4 When a call or an entry is refused
A refusal of the whole call has the error body of section 8.4, and nothing is made:
Status and code |
What it means |
|---|---|
400 consent_required |
consent_confirmed is not the JSON value true. |
400 invalid_request |
files or items is missing or is not a list, or com_interview_set_id is not an id. |
400 batch_empty, batch_too_large |
The list is empty, or holds more than 50 entries. Split the batch. |
400 set_not_recording |
com_interview_set_id names an assessment that is not a recording evaluation. Make one with POST /interview/set/v1 and "set_type": 8. |
400 api_key_has_no_acting_user |
The key belongs to no person (section 4.7.1). A key with no creator on record is normally refused first, with a 403 (section 11). |
403 |
The key lacks the submit_recordings scope, or has expired, or belongs to a person who has been blocked (user_banned), or its creator has left (section 2.10) or can open no hiring project. |
403 key_out_of_reach |
On POST /interview/recording/v1 only: the key's creator is limited to some projects and com_interview_set_id is an assessment they did not make and that is not linked to a project they can open. It is checked after set_not_found and before set_not_recording. |
403 recording_requires_scale |
The organization is below the Scale plan, so nothing was made. Evaluations that already exist are not affected. |
404 set_not_found |
No assessment with this com_interview_set_id exists in your organization. |
503 recording_api_disabled, upload_service_unavailable, recording_plan_check_failed, idempotency_store_unavailable |
The recording routes are switched off for now, the upload service did not answer, your plan could not be read (this is not a reason to upgrade), or Flowmingo could not check whether these recordings were already handled. Send the same request again in a moment. |
A refusal of one entry of step 2 is a rejected result with a code. The codes marked "again" are worth sending again with the same client_ref and upload_id: after request_in_progress or idempotency_store_unavailable wait a few seconds, or up to five minutes if it persists. For the others, change the entry first, except upload_already_used, whose row says what to check. Branch on code, never on the wording of message.
code |
What it means | Send it again as it is? |
|---|---|---|
invalid_client_ref |
client_ref is missing, or is not 1 to 128 characters of letters, digits and . _ : -. |
No |
duplicate_client_ref |
The same client_ref appears twice in the request. The first entry with it was used. |
No |
client_ref_reused |
This client_ref already belongs to a submission made from a different upload. Use a new client_ref for each recording. |
No |
invalid_upload_id |
upload_id is missing, or is not an id as step 1 returns it (a UUID). |
No |
invalid_email |
email is not a valid email address. |
No |
invalid_name |
firstname is missing, or firstname or lastname is not text of at most 250 characters without < or >. |
No |
invalid_language |
language is not text of at most 10 characters. |
No |
invalid_external_id |
external_id is not plain text of 1 to 128 characters with no control characters, is only spaces, holds half of a surrogate pair, or is not a JSON string. Only this entry is refused. |
No |
upload_already_used |
Another entry has used or is holding this upload. Either a submission was already made from it, or another entry that is still being handled (in this request or in one that is running) holds it, and then no submission may exist. When two entries of one request name the same upload_id, the second is refused this way even if the first then fails, for example with upload_not_completed. Read the result of the other entry first, or look for a submission of that candidate in the results of the recording evaluation (section 4.7.5). Make a new upload only when you know that no submission was made from this one. |
Not until you know which |
upload_not_found |
No upload with this id exists. Create the upload again. | No |
upload_not_yours |
The upload was created for a different organization or API key holder. | No |
file_too_large |
The uploaded file is over 1 GiB, so it was removed. Create a new upload for a smaller file. | No |
upload_not_completed |
The file has not arrived yet, or it arrived empty. PUT the bytes to signed_url first. |
Again, after the upload |
request_in_progress |
Another request with this client_ref is still being handled. |
Again, after a few seconds |
idempotency_store_unavailable |
Flowmingo could not check whether this recording is already being handled, so it was not created. | Again, after a few seconds |
upload_unavailable |
Flowmingo could not check this upload just now. | Again, in a moment |
submission_failed |
The submission could not be created. | Again, in a moment |
duplicate_not_accessible |
This client_ref already made a submission that the person your key belongs to can no longer open, for example because the assessment was taken away from them, or that does not exist any more because it or its assessment was deleted. Nothing was created, and neither its submission_id nor its external_id is shown. Use a new client_ref and a new upload for a new recording, or give the access back. |
No |
duplicate_read_failed |
This client_ref already made a submission, so nothing was created again, but its details could not be read just now. |
Again, in a moment |
4.7.5 Follow a recording and read its score
A queued recording is transcribed, split into questions and answers and scored like an AI Interview. The recordings are processed one after another, so a long batch can take hours, not minutes. The recording file is deleted from Flowmingo storage once it has been transcribed; the transcript and the report are kept.
A recording can fail after it was accepted. Step 2 checks that the file arrived and is within 1 GiB. The recording itself is checked later, when it is processed. A recording longer than 120 minutes fails, and so do a file that cannot be read as audio or video, one with no audio track, one in which no speech is found, and one in which no questions and answers can be found. A temporary fault is tried again automatically, up to three attempts in all. A recording that fails for good never gets a score, and its upload is deleted. Its result then reads score_status not_scored, the same as a recording that is still waiting, so read these two fields, which the three result routes below return for recordings only:
| Field | What it holds |
|---|---|
recording_status |
queued (waiting, or waiting to be tried again after a temporary fault), processing, transcribed (the questions and answers are written and the score follows) or failed. null when no state is recorded. |
recording_error |
The reason in words, at most 500 characters, when the recording failed or a temporary fault is waiting to be tried again. null otherwise. |
To try a failed recording again, upload the file again (step 1) and send it as a new entry with a new client_ref: the same client_ref with a different upload is refused with client_ref_reused.
- One recording:
GET /hiring/submissions/{id}/v1, or.../detail/v1for the AI evaluation, with thesubmission_idfrom step 2 (sections 5.1 and 5.3). Itsassessment_typeisrecording. - All the recordings of a role, best first:
GET /hiring/submissions/v1?assessment_type=recording&com_interview_set_id=<id>&sort_by=overall_score&order=desc(section 5.2). - To be told when a result is stored: the webhook
assessment.completed(see the webhook guide), or poll the events feed (section 7).
These reads need read_hiring, and the candidate's name and email need read_candidates (section 2.3).
4.8 Showcases
A Showcase (assessment_type showcase) is sent through the same route, POST /interview/candidate/invite/v1, and behaves like Send in the Flowmingo dashboard: the candidate gets an email with a link, records themselves doing the tasks, and appears on the board of the hiring project the Showcase is attached to. One Showcase goes out per call, under the same rules as a Skill Test or a Technical Test (section 4.6), with the differences below. Required API key scope: invite_candidates.
4.8.1 The request
- One Showcase per request.
com_interview_set_ids(or the deprecatedcom_interview_set_id) holds that one id and nothing else. Anything beside it is refused with400and the codeassessment_test_requires_single_set, and nothing is created. - The Showcase must be published and attached to an active hiring project of yours. A Showcase that is not switched on is refused with
400and the codeshowcase.invite.set_not_published, also whensend_inviteisfalse, and an archived one withshowcase.invite.set_archived. Name the project incom_project_ids(one id) or in the deprecatedcom_project_id. If you name none, the Showcase's single active project is used. A Showcase attached to no active project answers400with the codeproject_required, and one attached to several answers400withproject_ambiguous: send the id of the project you mean. A project that is archived or that the person the invitation goes out as cannot open answers404with the messageProject not found, and so does a Showcase that is not attached to the project you named, with the messageProject Showcase not found. These two404answers carry a message and nocode, so branch on the status. - Every candidate needs an email, at most 100 candidates per call. The codes are those of section 4.6.1:
assessment_test_email_required,external_id_requires_emailandassessment_test_too_many_candidates. An address that ends in@placeholder.local, or a preview address, is refused with400and the codeshowcase.invite.placeholder_recipient_not_allowed. - Daily limits: 1,000 invitations to one Showcase and 5,000 for your organization per UTC day. A call over a limit answers
429with the codeassessment_test_invite_limit_reached, and one whose limit cannot be checked answers503withassessment_test_invite_limit_unavailable. Nothing is sent in either case (section 4.6.6). - Fields a Showcase does not take. A value in
ats_candidate_id, a value inmetadataand apractice_modeother thannullare refused with400and the codeassessment_test_field_not_supported.send_atis refused with400and the codesend_at_not_supported_for_assessment(section 4.9). A Showcase also cannot be sent to a Matched Talents candidate, by WhatsApp, or with a subject of your own. - The email uses Flowmingo's English Showcase template.
invitation_messagecan add a custom note to it (the limits are those of section 4.3), andattachment_sys_file_idsis checked but the files are not sent (section 4.3). The API cannot change the subject or the language. - The API sets no expiry on a new Showcase invitation, so
expires_atisnull. An invitation that already existed (a reused one) can carry anexpires_atvalue, and one that is past it is reported as it stands.
Sample request. The ids are placeholders, as in section 4.4:
curl -X POST https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"com_interview_set_ids": ["6b3a9f10-2c4d-4e5f-9a7b-8c1d2e3f4a5b"],
"com_project_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"candidates": [
{ "name": "Alex Doe", "email": "alex@example.com" }
]
}'
The answer is 201 and has the shape of section 4.6.4: each entry of results carries assessment_invites, here with assessment_type showcase. It never contains the candidate's invitation link or token. The sample is cut to its main fields:
{
"results": [
{
"email_address": "alex@example.com",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"external_id": null,
"application_external_id": null,
"invitation_external_id": null,
"assessment_invites": [
{
"com_interview_set_id": "6b3a9f10-2c4d-4e5f-9a7b-8c1d2e3f4a5b",
"assessment_type": "showcase",
"email": "alex@example.com",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"tal_invite_id": "9d4e8a21-6c3b-4f57-8e10-2a7b5c9d1e34",
"status": "sent",
"email_delivery": "queued",
"reused_existing": false,
"expires_at": null,
"error": null
}
]
}
]
}
4.8.2 What happened to each email, and repeating a call
email_delivery takes the values of section 4.6.5. Repeating a call is built to be safe:
- A call never revives or rewrites an invitation that exists. A cancelled or expired one, or one the candidate has started, is reported as it stands:
skipped_not_unstartedorskipped_already_started. One that was already emailed readsalready_queued. A person who has already recorded the Showcase gets no new invitation (statusnot_created, with the reason inerror). - The first email of an invitation is claimed before it is sent, so a retry, or a second key, cannot email the same person twice.
- Two values mean something particular for a Showcase:
email_delivery |
Meaning for a Showcase |
|---|---|
delivery_in_progress |
Another call is sending this invitation right now. This call sends no second email, and it normally gives back the unit of your daily allowance it reserved. If giving the unit back fails, the unit stays counted. |
failed:delivery_unknown |
The email service may have sent it, or a send was never confirmed within 15 minutes. The API will not send it again. Check the invitation in the dashboard, and resend it there if the candidate did not get it. The unit of your daily allowance stays counted when this call's own send was lost. It is normally given back when this call only saw another call's send that was never confirmed. |
For a Skill Test or a Technical Test, delivery_in_progress is described in section 4.6.5.
4.8.3 What it costs
Sending the invitation costs nothing. A Showcase that your organization has set up as "Sponsored by you" debits your organization's wallet when the candidate completes it, not when the invitation is sent. The API key does not choose this: it follows the Showcase's own settings in the dashboard.
4.9 Schedule an invitation, and cancel it
send_at on POST /interview/candidate/invite/v1 sends the invitation email at a time you choose, instead of now. It is when the invitation is sent, not when the candidate takes the interview. Flowmingo has no calendar and no meeting slots: the candidate opens the link when they like. At that time the invitation becomes due. The sender then queues the email if the invitation is still eligible, usually within a minute (section 4.5). Sending and delivery are not guaranteed. The dashboard shows a scheduled invitation as "Invite scheduled". Section 4.9.2 says what the API can and cannot do to move or withdraw an invitation, and section 4.10 how to pause, and resume, the reminders that follow one.
send_at is a date and time with a time zone: an offset or Z, for example 2026-11-10T09:00:00+08:00 or 2026-11-10T01:00:00Z. It applies to every candidate in the request. These rules apply, and a request that breaks one is refused with 400 and the code in the table. Nothing is created.
| Rule | Code |
|---|---|
| It is for an AI interview or a role-play only. A Skill Test, a Technical Test, a Showcase and a CV evaluation cannot be scheduled. | send_at_not_supported_for_assessment |
send_invite is true. |
send_at_requires_send_invite |
The request names the assessment (com_interview_set_ids). |
send_at_requires_assessment |
Every candidate has an email. A candidate given only as a CV file is invited later, once an address is found, and cannot be scheduled. |
send_at_requires_email |
The time is a real date and time with an offset or Z, with no spaces around it, from 2 minutes to 90 days from now. |
send_at_invalid |
curl -X POST https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"com_interview_set_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"],
"send_at": "2026-11-10T09:00:00+08:00",
"candidates": [
{ "name": "Alex Doe", "email": "alex@example.com" }
]
}'
The answer is the one in section 4.5, and each invitation in it carries schedule_send_at, the due time in UTC. A request without send_at has no such field. The sample is cut to the main fields:
{
"results": [
{
"email_address": "alex@example.com",
"invitations": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"email": "alex@example.com",
"status_text": "pending",
"schedule_send_at": "2026-11-10T01:00:00.000Z"
}
]
}
]
}
- The time is the invitation's due time. An invitation that is still unsent 7 days after its due time is not sent automatically after that. Whether and when it is marked failed depends on the assessment and on the candidate, as section 4.5 describes.
- The daily limits count it twice. The invitations count against the limits of 1,000 per assessment and 5,000 per organization on the day you make the request, as before. The day they are due is checked as well. If that day would go over a limit, the request is refused with
400and the codesend_day_full, and nothing is created. Choose another day. The count for that day also includes invitations made without a schedule on or after it. The check is a read before the writes, so two requests at the same moment can both pass. The day is the calendar day on our server clock, as for the other daily limits (section 4.3).
4.9.1 Cancel a scheduled invitation
POST /interview/candidate/invites/{id}/cancel/v1 cancels a scheduled invitation, but only while its due time is still in the future and the sender has not picked it up. Required API key scope: invite_candidates. It takes no body. The answer is 201.
Find the invitation's id in the answer of the invite call, and note that it sits in a different place for each kind:
| Invitation to | Where its id is in the invite answer |
|---|---|
| An AI interview or a role-play | invitations[].id (section 4.5) |
| A Showcase, a Skill Test or a Technical Test | assessment_invites[].tal_invite_id (sections 4.6.4 and 4.8) |
Only a scheduled AI interview or role-play invitation can be cancelled. The id of a Showcase or test invitation answers 409 with the code invitation_not_scheduled.
curl -s -X POST -H "X-API-Key: $FLOWMINGO_API_KEY" \
"$BASE/interview/candidate/invites/f47ac10b-58cc-4372-a567-0e02b2c3d479/cancel/v1"
{
"tal_invite_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"cancelled": true,
"was_due_at": "2026-11-10T01:00:00.000Z"
}
- It works only while the due time is still in the future and the sender has not claimed the invitation. If the invitation is already due (even if it has not gone out yet), was already sent, is being sent right now, was started, or was already cancelled, the call answers
409with the codeinvitation_not_scheduledand changes nothing. Repeating a call that worked is also a409: there is nothing left to cancel. - The candidate is not emailed, and nothing is sent about the cancellation. The person stays in your account and on the project board, and their result stays as not started. Inviting them again makes a new invitation.
- Errors:
404with the codeinvitation_not_foundfor an id that is not in your organization, and403with the codekey_out_of_reachfor an invitation to an assessment or project the key's creator cannot open (section 2.9). The404can also mean that a key whose creator is limited to some projects cannot tell which project the invitation belongs to: that happens for an invitation made with only an assessment and no project, when this key did not make it.
4.9.2 Cancel or reschedule an interview: what the API can and cannot do
An AI interview or a role-play is not an appointment. The candidate does it whenever they like, from the link in their invitation: Flowmingo has no calendar, no meeting slot and no interview time, so there is no interview to move. What you can schedule is when the invitation email is sent (send_at, section 4.9). What you can and cannot do follows from that:
| You want to | What the API does |
|---|---|
| Stop an invitation email that has not gone out yet | Cancel it (section 4.9.1). That works only for an invitation that is scheduled, whose due time is still in the future, and that the sender has not picked up. The API's pause (section 4.10) holds only the reminders, never the invitation email itself: a recruiter can hold an invitation that has not gone out by pausing it in the dashboard, but the API cannot. |
| Move the time a scheduled invitation is sent | There is no reschedule route. Cancel the invitation (section 4.9.1), then invite the person again with the new send_at. The list below says what that costs. |
| Withdraw an invitation that was already sent | Not through the API: the cancel route answers 409 invitation_not_scheduled for it, and nothing else in this API withdraws it, so the candidate can still open the link in the invitation. |
| Set a deadline for finishing the interview | There is no field for it. An invitation to an AI interview or a role-play that you make through the API has no expiry date of its own. (An invitation to a Skill Test does carry an expires_at: section 4.6.4.) |
| Hold the reminders that follow an invitation, and let them go again later | Pause them, and resume them when you want (sections 4.10 and 4.10.1). A pause does not cancel the invitation and does not move any date, and it is refused (409 reminders_managed_by_bundle) for an invitation that belongs to a multi-assessment bundle that may still be sending its own reminders. A repeat of the pause answers 200 with already true only while the invitation can still be paused; if the reminders were resumed or restarted since (for example with "Send Flowmingo's invitation instead" in the dashboard), the repeat pauses the new ones (already false) (section 4.10). |
What "cancel, then invite again" costs. The new invitation is a different invitation:
- It has a new invitation id. The id of the cancelled invitation is not reused, so keep the id from the new answer.
- If the new invitation is a real one (not a practice invitation), Flowmingo attempts another
invitation.lifecycle.updateevent with the statusinvitation_createdfor that person and assessment (see the webhook guide). The event is sent in the background and a failure to send it does not undo the invitation. - It counts again against the key's limit of 300 invitations an hour and 1,000 a day (section 8.5): cancelling the first invitation does not give its count back.
- An invitation sent right after the cancel can be refused as already invited. The invite route's check for an existing invitation reads a copy of the data that trails a little behind, so for a moment it can still see the invitation you cancelled, and that candidate's entry in
resultscarries the messageEmail already invitedinside a201(section 8.6). A201alone does not mean that a new invitation was created: read the candidate's entry inresults(section 4.5). When it carries that message, no new invitation exists and no new event is produced. Wait a few seconds and send the invitation again. - Its reminders are its own: pause them again if you want none (section 4.10).
The candidate can see an invitation before its email is sent. When a real invitation (not a practice invitation) is created for a candidate who already has a Flowmingo account, Flowmingo also attempts an in-app notice, titled "You're invited" followed by the role (or a general "You've been invited to an interview" when no role can be named), as soon as the invitation is created, even when its email is scheduled for later. The notice is started in the background and is not guaranteed. It does not wait for send_at.
4.10 Pause and resume an invitation's reminders
After an invitation to an AI interview or a role-play, Flowmingo usually sends the candidate two automatic reminders, on the channels that carried the invitation (Flowmingo's email, a mailbox you connected, and WhatsApp). On each channel, Reminder 1 is due 2 days after the invitation email went out, and Reminder 2 is due 2 days after Reminder 1 went out; a candidate who has already started the interview is usually not sent these two. A candidate who started and did not finish can still get a separate "complete your assessment" email, which these routes do not touch. POST /interview/candidate/invites/{id}/stop-reminders/v1 pauses both of that invitation's own automatic reminders, as the Pause buttons do in the dashboard, and POST /interview/candidate/invites/{id}/resume-reminders/v1 (section 4.10.1) lets them go again. The pause route keeps its original name, stop-reminders, and its answer keeps the field reminders_stopped, so nothing that already calls it breaks; both now mean paused. A pause means the same thing everywhere: one made with this call, with a Pause button in the dashboard or with the dashboard's older Stop button can be resumed by the resume call or by a recruiter in the dashboard, and the dashboard shows when it was paused and by whom (a pause made through the API is shown by the name of its API key). It does not pause every message that may follow an invitation: see "What a 200 does not pause" below. Required API key scope: invite_candidates. It takes no body. The answer is 200, also on a repeat while the invitation can still be paused (see "Repeating" below). The id is the id of the invitation in the invite answer (invitations[].id, section 4.5), the same one the cancel route takes (section 4.9.1). The key acts with the permissions of the person who created it, as in the dashboard (section 2.9).
curl -s -X POST -H "X-API-Key: $FLOWMINGO_API_KEY" \
"$BASE/interview/candidate/invites/f47ac10b-58cc-4372-a567-0e02b2c3d479/stop-reminders/v1"
{
"tal_invite_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"reminders_stopped": true,
"stopped_at": 1793940000000,
"already": false,
"paused": true,
"paused_lanes": ["flowmingo"]
}
| Field | What it holds |
|---|---|
tal_invite_id |
The invitation (the id you sent, in lower case). |
reminders_stopped |
Always true in a 200. The name is kept from the time this route stopped reminders for good; read paused and paused_lanes for what is paused now. |
stopped_at |
When the reminders were paused, in epoch milliseconds (a number, not an ISO 8601 text like the other times of the API; the sample is 6 November 2026, 04:40 UTC). On a repeat it is the time of the earlier pause when a usable time was stored, and null otherwise. The name is kept. |
already |
true when nothing was written because the reminders were already paused (by this key, by another key, by a recruiter's Pause in the dashboard or by an older dashboard Stop), because no channel sends reminders for this invitation, or because its invitation email was cancelled in the dashboard (see "A pause is not a cancel" below). A repeat answers this while the invitation can still be paused, unless the reminders were resumed in the meantime (by the resume call or by a recruiter) or restarted (for example with "Send Flowmingo's invitation instead" in the dashboard): the call then pauses them again and answers false, and a further repeat answers true again. After the invitation has joined a live multi-assessment bundle a repeat is 409 reminders_managed_by_bundle, and after you cancelled it through the API it is 404 invitation_not_found (see "Repeating" below). |
paused |
true when at least one channel is paused after the call. false when no channel sends reminders for this invitation (there was nothing to pause) and for an invitation whose invitation email was cancelled in the dashboard. |
paused_lanes |
The channels that are paused after the call, a list of flowmingo (Flowmingo's own email), mailbox (the copy sent from the mailbox you connected) and whatsapp. Only the channels that send reminders for this invitation are ever listed. |
- Sent or not yet sent. It works for an invitation that was already sent (while its reminders are not yet due) and for one that is scheduled and not yet sent (section 4.9), so you can pause the reminders in advance. A pause made through the API never holds the invitation email back: an invitation that is waiting for its
send_atis still sent at that time, with its reminders paused. (A recruiter who presses Pause in the dashboard on an invitation that has not gone out does hold the invitation itself: it then goes out only after it is resumed, in the dashboard or with the resume call, at the next send slot.) - Both reminders pause together, on every channel. One call pauses the invitation's reminders on every channel that sends them for it; the dashboard can also pause one channel at a time, and everyone who was invited in the same batch in one press, which this route does not. An invitation to the same person for another assessment is a different invitation, with its own reminders, which this call does not touch.
- A pause does not move any date. A reminder is due 2 days after the previous email on its channel went out, whatever a pause did in between. When the reminders are resumed, one that is not due yet goes on its own date, one that is due today goes at the next pass of the reminder sender, and one whose due day ended while its channel was paused is skipped, with every later reminder of that channel (section 4.10.1 has the rule and an example).
- A reminder already picked up may still arrive. A reminder the sender had picked up at the moment of your call may still go out, and so may a mailbox copy that was already queued with the mail service. Every later one is held.
- A pause is not a cancel. It does not cancel or withdraw the invitation: the candidate can still open the link and take the interview. To cancel an invitation that has not been sent yet, use section 4.9.1. A cancel is final and a pause is not: an invitation you cancelled through the API answers
404invitation_not_foundto both the pause and the resume call, and an invitation whose invitation email a recruiter cancelled in the dashboard keeps its place but has its reminders stopped for good, so the pause call answers200withalreadytrueandpausedfalse, and the resume call answers409reminders_cancelled. Nothing can resume a cancelled invitation's reminders. - Only AI interviews and role-plays. The reminders of any other kind of assessment cannot be paused or resumed here (
409reminders_not_stoppable, nothing changed; the code keeps the word "stoppable" and now means pausable). A live multi-assessment bundle is checked before the kind and gives its own409(below). - What a
200does not pause. It pauses this invitation's own automatic reminders and nothing else. It does not pause a separate email sequence that follows the invitation (a sequence set up in the dashboard that runs beside the invitation's own reminders): the call still answers200for such an invitation, as the dashboard's button does, and those follow-ups are managed on the sequence in the dashboard. When a sequence and Flowmingo's own email both send the invitation, the call pauses Flowmingo's reminders and leaves the sequence alone; an invitation that is sent only by a sequence has no reminder of its own to pause and answers409reminders_managed_by_sequence(below). A sequence that is started at about the same moment as your call can still send, although the answer is200. - An invitation that belongs to a multi-assessment bundle that may still send reminders is refused. A multi-assessment bundle (one email that holds several assessments, sent from the dashboard) sends its own reminders for all its assessments together, and they cannot be paused from here. An invitation can belong to more than one bundle. If any one of its bundles may still be sending reminders, the call answers
409reminders_managed_by_bundle, and nothing is changed, even when the invitation also belongs to another bundle that has ended. A bundle may still be sending when it is your organization's, is not deleted and is active, has not expired, is within 10 days of its email, has had fewer than two reminders sent and has not been recorded as finished (every assessment completed). If the bundle's email has not been recorded as sent yet (it may be being sent right now, or it was never sent), the 10 days are counted from the bundle's creation, and the bundle counts in the same way, so it does not matter whether its email has gone out. A bundle that has expired, whose 10 days are over, that has had both reminders sent or that has been recorded as finished sends no more reminders. When none of the invitation's bundles may still be sending, the bundle rule refuses nothing: the call answers200and pauses the invitation's own reminders, provided the invitation is an AI interview or a role-play invitation and is not sent only by a live email sequence (otherwise409reminders_not_stoppableorreminders_managed_by_sequence). The bundle is checked before the kind of assessment, so an invitation to another kind of assessment that is in a live bundle gets the bundle's409. A bundle is recorded as finished by Flowmingo's reminder sender, not at the moment its last assessment is completed, so a bundle whose assessments are all completed can still count, and the call can still answer409for it, until the sender records that. - An invitation made without naming a project can be paused or resumed through the API only by the key that made it. When an invitation was created naming only an assessment (no project and no job post), a key whose creator is limited to some projects can pause or resume it only if it is the very key that created the invitation and the assessment is still within that creator's reach. Any other limited key, even a second key of the same person with the same scope, gets the
404below, as on the cancel route (section 4.9.1). A key whose creator has access to all hiring projects is not subject to this rule. - Repeating changes nothing, unless the reminders were resumed or restarted, and it is not always a
200. While the invitation can still be paused and the key's creator can still reach it, a repeat answers200withalreadytrueand pauses nothing further, and two calls at the same moment leave one pause (the second may answer409reminders_busy: repeat it). The exception is an invitation whose reminders were resumed (by the resume call or by a recruiter) or restarted in the dashboard in the meantime (for example with "Send Flowmingo's invitation instead"): the call then pauses the reminders again and answers200withalreadyfalse, and a further repeat answersalreadytrueagain. The checks behind the refusals in the table below run again on a repeat, before the earlier pause is looked at, so a repeat is refused if the invitation has joined a live multi-assessment bundle since your first call (409reminders_managed_by_bundle), if you cancelled it through the API in the meantime (404invitation_not_found), if the key's creator has left the company (during the grace period) or can no longer reach the invitation (the same404), or if the invitation has become one that only a live email sequence sends (409reminders_managed_by_sequence). Nothing is changed by such a refusal.
| Status | code |
What to do |
|---|---|---|
400 |
(no code) | The id is not a UUID. |
403 |
(no code), or a code of section 11 | The key lacks the invite_candidates scope, or has expired (no code), or is refused for a reason that has its own code (section 11). |
404 |
invitation_not_found |
One answer for an id that is not an invitation of yours, an invitation of another company, an invitation that you cancelled through the API (cancelling is final; also when it is a repeat of a pause that worked), and an invitation outside the projects the key's creator can open (the cancel route of section 4.9.1 answers 403 key_out_of_reach for that last case; this one does not say that the invitation exists). It is also the answer, for a key whose creator is limited to some projects, to an invitation that was made without naming a project by another key (see the rule above about an invitation made without naming a project). And it is what a key whose creator has left the company gets for every invitation, during the grace period (section 2.10): this route decides it from the key itself, before it looks for the invitation. |
409 |
reminders_not_stoppable |
The invitation is not to an AI interview or a role-play. Nothing was changed. (An invitation in a live multi-assessment bundle gets reminders_managed_by_bundle instead, because the bundle is checked before the kind.) |
409 |
reminders_managed_by_sequence |
The invitation is sent only by an email sequence that you set up in the dashboard: no Flowmingo email, mailbox copy or WhatsApp reminder of its own is there for this call to pause. Pause that person on the sequence in the dashboard. Nothing was changed. (When Flowmingo's own email also sends the invitation, the call pauses Flowmingo's reminders instead and answers 200.) |
409 |
reminders_managed_by_bundle |
The invitation is one assessment of at least one multi-assessment bundle (one email that holds several assessments, sent from the dashboard) that may still be sending its own reminders: it has not expired, is not finished (recorded as finished by the reminder sender, so a bundle whose assessments are all completed can still count until then), is within 10 days of its email (or of its creation while the email is still being sent or was never recorded as sent) and has had fewer than two reminders sent. One such bundle is enough, even when the invitation also belongs to another bundle that has ended. They cannot be paused from here. Nothing was changed. This is checked before reminders_not_stoppable and reminders_managed_by_sequence, and it is also what a repeat answers when the invitation has joined such a bundle since the first pause. |
409 |
reminders_busy |
The invitation was being sent, or changed by someone else, at that moment. Nothing was changed: try again in a moment. |
4.10.1 Resume an invitation's reminders
POST /interview/candidate/invites/{id}/resume-reminders/v1 lets a paused invitation's reminders go again, on every channel that is paused, whoever paused them: this key, another key, a recruiter's Pause in the dashboard or an older dashboard Stop. Required API key scope: invite_candidates. It takes no body. The id is the same invitation id as for the pause call, and the key acts with the permissions of the person who created it behind the same project reach (section 2.9), with the same single 404 for anything it may not reach. Any key with the invite_candidates scope can resume a pause made by a recruiter or by another key; the change is recorded with the key that made it.
curl -s -X POST -H "X-API-Key: $FLOWMINGO_API_KEY" \
"$BASE/interview/candidate/invites/f47ac10b-58cc-4372-a567-0e02b2c3d479/resume-reminders/v1"
{
"tal_invite_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"resumed": true,
"resumed_lanes": ["flowmingo"],
"skipped": [],
"already": false,
"paused_lanes": []
}
| Field | What it holds |
|---|---|
tal_invite_id |
The invitation (the id you sent, in lower case). |
resumed |
Always true in a 200: the invitation has no paused channel after the call, except the ones paused_lanes lists. |
resumed_lanes |
The channels this call resumed (flowmingo, mailbox, whatsapp). Empty when nothing was paused. |
skipped |
The reminders that were skipped because their due day ended while their channel was paused. Each entry has lane (the channel), from (the reminder, 1 or 2, that is skipped; it and every later reminder of that channel will not be sent) and due_at (when it was due, in epoch milliseconds). Empty when none was. |
already |
true when nothing was paused, so nothing was written. A repeat answers this. |
paused_lanes |
The channels that are still paused after the call. Usually empty. |
- Dates do not move, so a resume can skip a reminder. Reminder 1 is due 2 days after the invitation email went out and Reminder 2 is due 2 days after Reminder 1 went out, each channel on its own. A pause does not change any of these dates, and neither does a resume. A reminder that is not due yet when you resume goes on its own date. A reminder that is due today goes at the next pass of the reminder sender: the whole calendar day counts, and for this call the day is the UTC day. A reminder whose due day had already ended while its channel was paused is skipped, and so is every later reminder of that channel:
skippednames, for each channel, the first reminder that was skipped, and nothing more is owed to the candidate on that channel. A reminder is also skipped when it would fall after the reminder sender stops looking at the invitation (about 5 to 6 days after the invitation email went out), because it could no longer be sent. So do not resume blindly: a resume after a due date does not send the late reminder, it closes that channel's reminders. - Example. The invitation email goes out on Monday 9 November 2026 at 09:00 UTC, so Reminder 1 is due on Wednesday 11 November and Reminder 2 two days after Reminder 1 goes out. You pause on Tuesday. If you resume on Wednesday, any time of day, Reminder 1 goes at the next pass and Reminder 2 follows two days after it. If you resume on Thursday, Reminder 1 is skipped and so is Reminder 2, and the answer carries
"skipped": [{ "lane": "flowmingo", "from": 1, "due_at": 1794387600000 }](one entry per channel: it names the first reminder that was skipped;due_atis Wednesday 11 November, 09:00 UTC). - An invitation that a recruiter paused before it went out. A pause made in the dashboard on an invitation that has not gone out holds the invitation email itself. Resuming it, with this call or in the dashboard, lets it go at the next send slot, and the reminder dates then count from when it really went out; nothing is skipped for it. (A pause made through the API never holds the invitation email.)
- Repeating is safe. When nothing is paused the call answers
200withalreadytrueand changes nothing. Two calls at the same moment resume once (the second may answer409reminders_busy: repeat it). - A cancel is final. The resume call answers
409reminders_cancelledfor an invitation whose invitation email a recruiter cancelled in the dashboard, and404invitation_not_foundfor one you cancelled through the API. Nothing can resume a cancelled invitation's reminders. - The same limits as the pause call. A live multi-assessment bundle answers
409reminders_managed_by_bundleand any assessment other than an AI interview or a role-play answers409reminders_not_stoppable, with nothing changed (the bundle is checked first). An invitation made without naming a project can be resumed only by the key that made it, when that key's creator is limited to some projects, and a key whose creator has left the company gets the404for every invitation during the grace period (section 2.10).
| Status | code |
What to do |
|---|---|---|
400 |
(no code) | The id is not a UUID. |
403 |
(no code), or a code of section 11 | The key lacks the invite_candidates scope, or has expired (no code), or is refused for a reason that has its own code (section 11). |
404 |
invitation_not_found |
The same single answer as the pause call: an id that is not an invitation of yours, an invitation of another company, an invitation that you cancelled through the API, an invitation outside the projects the key's creator can open, an invitation made without naming a project by another key (for a limited key), and every invitation for a key whose creator has left the company, during the grace period (section 2.10). |
409 |
reminders_cancelled |
The invitation email was cancelled in the dashboard, so the invitation's reminders are stopped for good. A cancel is final and cannot be resumed. Nothing was changed. |
409 |
reminders_not_stoppable |
The invitation is not to an AI interview or a role-play. Nothing was changed. |
409 |
reminders_managed_by_bundle |
The invitation is one assessment of at least one multi-assessment bundle that may still be sending its own reminders (the same test as for the pause call). Nothing was changed. |
409 |
reminders_busy |
The invitation was being sent, or changed by someone else, at that moment. Nothing was changed: try again in a moment. |
5. Read results through the API
These endpoints are read-only. They are how you pull scores and the AI's written evaluation into your own system, instead of sending people to Flowmingo to read a report.
5.1 What you can read
All paths below are prefixed with https://apis.flowmingo.ai/company/integration.
| Endpoint | Scope | Returns |
|---|---|---|
GET /me/v1 |
any valid key | The key's organization and scopes (see section 2.7). |
GET /hiring/submissions/v1 |
read_hiring |
A page of results. Use it to find a submission_id. |
GET /hiring/submissions/{id}/detail/v1 |
read_hiring |
One result, with the AI evaluation. |
GET /hiring/submissions/{id}/v1 |
read_hiring |
One result, without the AI evaluation. |
GET /hiring/submissions/{id}/media/v1 |
read_hiring and read_candidates |
The transcript and the recording links of one finished AI interview or role-play (section 5.10). |
GET /hiring/submissions/{id}/integrity/v1 |
read_hiring, read_candidates and read_integrity |
Counts of what was recorded during one finished Skill Test or Technical Test, and what was being watched (section 5.13). |
GET /hiring/interview-sets/v1 |
read_hiring |
Your assessments (interview sets), each with its id and its assessment_type. The id is what you invite candidates to (see section 4.2). |
GET /hiring/job-posts/v1, GET /hiring/job-posts/{id}/v1 |
read_hiring |
Your job posts. |
GET /hiring/job-posts/{id}/publication/v1 |
read_hiring |
Whether a job post is open to the public, and how it is boosted (section 3.7.1). |
GET /hiring/projects/v1, GET /hiring/projects/{projectId}/stages/v1 |
read_hiring |
Your hiring projects (section 5.7 shows how to find a project's id) and their pipeline stages. |
GET /hiring/projects/{id}/v1 |
read_hiring |
One hiring project by its id: name, role, status, its job post (the words it displays, the words as stored, public_state, review, differs_from_sent) and its assessments (section 3.5.13). |
GET /hiring/projects/requests/{idempotency_key}/v1 |
create_project |
Where a project create call stands, found by the idempotency_key you sent with it (section 5.7). |
GET /hiring/applications/v1 |
read_hiring |
Pipeline applications. |
GET /hiring/interviews/upcoming/v1 |
read_hiring |
Invitations that are waiting to be sent, were sent, or have been started (section 5.6). |
GET /hiring/overview/v1 |
read_hiring |
Aggregate counts for a funnel view, with a breakdown by assessment type (see section 5.8). |
GET /hiring/candidates/v1, GET /hiring/candidates/{id}/v1 |
read_candidates |
Candidate name, email and CV link. The single-candidate route also returns phone and thumb_cv_link. |
GET /sequences/v1 and three more GET /sequences/... routes |
read_sequences |
Your email sequences and how they are doing (section 5.12). |
GET /events/v1 |
read_hiring |
Your webhook events, cursor-paginated. Poll these if you cannot host an endpoint (see section 7). Needs a key whose creator has access to all hiring projects (section 7). |
Every read is scoped to the organization that owns the key, so no endpoint can return another organization's
data, and to what the key's creator can reach (section 2.9). The id-addressed routes
(job-posts/{id}, submissions/{id}, submissions/{id}/detail, submissions/{id}/media, submissions/{id}/integrity, candidates/{id}) answer 404 for an id
you cannot reach; the list routes simply return no matching rows.
Lists and counts leave out practice, trial and archived runs. A practice run by a candidate, a trial run by one of your
recruiters (a try-out of one of your own interviews, or a preview of a skill test or a technical test with Preview this yourself) and an archived attempt (one that a newer attempt in the same
place replaced) are left out of the results list, out of its total, out of the overview counts and out of every
average, as they are in the dashboard. These reads leave out every sitting that Flowmingo has recorded as
practice or as a trial: the id of a practice run or of a trial run answers 404, like an id that does not exist, and a
candidate your company knows only through a practice run is not returned at all. Flowmingo can only leave out what it
has recorded as practice: a practice sitting that was not recorded as practice is read like any other attempt. An
archived attempt can still be read by its id. A sitting a candidate has started for
real and not yet submitted is not practice: it is listed as in progress, and it is left out of completed counts and
averages.
One exception: a practice invitation. An invitation sent with practice_mode creates an unstarted result
(status 5, submission_at null) that is not marked as practice, so it appears in the results list and is
counted. Do not rely on practice_mode alone to keep test invitations out of your reporting.
Candidate names and emails need the read_candidates scope (see section 2.3).
Names and CVs come from your company's own records. The name, email and phone of a candidate are the ones your company holds for that person. A change the candidate makes to their own profile later does not reach you. A CV link (cv_link, and thumb_cv_link beside it) is returned only for a CV your company was given: the candidate sent it to you (they applied, used your careers page or attached it inside one of your assessments), someone at your company uploaded it, or your own integration sent it (for example as cv_link in an invite). The candidate's own profile CV, their other CVs and later edits are never returned. When your company holds no such CV, cv_link is null.
Your own ids. Most of these reads return the external_id you gave a record when you created it, and several can be filtered by it. Section 5.9 lists each one.
5.2 Find a result
curl -s -H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
'https://apis.flowmingo.ai/company/integration/hiring/submissions/v1?com_interview_set_id=3f2504e0-4f89-41d3-9a0c-0305e82c3301&limit=20'
| Query param | Type | Notes |
|---|---|---|
com_interview_set_id |
uuid | Only results for this assessment. |
tal_candidate_id |
uuid | Only results for this candidate. |
assessment_type |
string | Only results of one kind: interview (AI interviews and role-plays together), ai_interview, role_play, cv_evaluation, skill_test, technical_test, showcase, recording or questionnaire (see section 5.3). Any other value is refused with 400. Leave it out, or send it empty, for every kind. |
sort_by |
string | created_at (default) or overall_score. Rows with no score always sort last, whichever direction you ask for. |
order |
string | asc or desc (default desc). |
completed_within_days |
integer | Completed in the last N days (max 3650). Ignored if you also send completed_from. |
completed_from, completed_to |
ISO date | Completion window. A bare YYYY-MM-DD in completed_to covers that whole day; a full timestamp is used as given. A date filter leaves out results that are not finished or are still held (section 5.5). |
with_project |
boolean | Send true or 1. Adds com_project_id to each row. It is null for an assessment that maps to no single project. |
page, limit |
integer | page defaults to 1 (max 100000), limit to 20 (max 100). Both must be positive: limit=0 is a 400, not "use the default". |
There is no filter by external_id on this list yet. Sending external_id as a query parameter is refused with 400, whatever its value and also when it is written with brackets (external_id[]=a), so that it cannot be mistaken for a lookup that matched. Filter by com_interview_set_id or tal_candidate_id, or read a result by its id: each result shows its own external_id.
The response is { "data": [ ... ], "total": n, "page": 1, "limit": 20 }:
{
"data": [
{
"id": "59065d06-a013-44c2-97d2-88e9d6b3c858",
"status": 1,
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"external_id": null,
"candidate_name": "Alex Kim",
"firstname": "Alex",
"lastname": "Kim",
"email": "alex@example.com",
"assessment_type": "ai_interview",
"assessment_name": "AI Engineer - AI Interview",
"overall_score": 7.2,
"score_status": "scored",
"submission_at": "2026-09-10T12:00:49.000Z",
"created_at": "2026-09-10T11:14:02.000Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
Each row carries id (the submission id), status, com_interview_set_id, tal_candidate_id, external_id,
assessment_type, assessment_name, overall_score, score_status, submission_at and created_at, plus
candidate_name, firstname, lastname and email when your key carries read_candidates. score_percent
appears only on a released skill-test row that has a score; test for the key rather than for null. A
candidate who was invited and has not started appears too, with status 5 and submission_at null. The
overall_score of a technical test can still be provisional for a while after its attempt ended (section 5.5).
external_id is your own id for the result. It is set only for a result made by the recordings route with an external_id for the entry (section 4.7.3), and it is null for every other result, including every result a candidate made and every result that existed before the field did. The same field is on GET /hiring/submissions/{id}/v1 and GET /hiring/submissions/{id}/detail/v1. It does not need the read_candidates scope.
5.3 Read the evaluation
curl -s -H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
'https://apis.flowmingo.ai/company/integration/hiring/submissions/{submission_id}/detail/v1'
{
"id": "59065d06-a013-44c2-97d2-88e9d6b3c858",
"submission_id": "59065d06-a013-44c2-97d2-88e9d6b3c858",
"status": 1,
"com_interview_set_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"external_id": null,
"assessment_type": "ai_interview",
"assessment_name": "AI Engineer - AI Interview",
"candidate_name": "Alex Kim",
"firstname": "Alex",
"lastname": "Kim",
"email": "alex@example.com",
"overall_score": 7.2,
"score_status": "scored",
"submission_at": "2026-09-10T12:00:49.000Z",
"created_at": "2026-09-10T11:14:02.000Z",
"evaluation": {
"summary": "A strong applied engineer who has shipped retrieval systems end to end...",
"overall_rating": 7.2,
"strengths": ["Ships production RAG pipelines", "..."],
"gaps": ["Limited exposure to evaluation tooling", "..."],
"recommendations": ["Probe observability practices in a follow-up", "..."],
"competencies": [
{ "name": "Applied AI/ML Engineering", "rating": 9 },
{ "name": "Conversational AI", "rating": 3 }
]
},
"evaluation_cv": { "summary": "...", "overall_rating": 7.9 }
}
overall_scoreis out of 10, and so is everycompetencies[].rating. It isnullwhen there is no usable number:score_statussays which:scored,cannot_evaluate(the AI ran and could not rate the attempt) ornot_scored(no score yet). Acannot_evaluateis not a zero. Ascoredstatus says there is a number, not that it is final: for a technical test, a number read after the attempt ended can still be provisional (section 5.5).statusis a number:0inactive,1active (complete),2pending,3archived,4incomplete,5needs interview (invited, not started),6retaking.assessment_typesays what kind of assessment the result is from. Read it before you compare scores, because the scales differ:assessment_typeAssessment overall_scoreai_interviewAI interview (an older interview with no recorded type reads as this) The AI's rating, 0-10. role_playRole-play The AI's rating, 0-10. cv_evaluationCV evaluation The AI's rating of the CV, 0-10. recordingRecording evaluation The AI's rating, 0-10. skill_testSkill test The test percentage divided by 10, with score_percent(0-100) beside it once the report is released.technical_testTechnical test The share of the available points the candidate earned, on a 0-10 scale (the percentage divided by 10, to two decimals): code problems are scored on their test cases, multiple-choice questions on correct answers. It can still change after the attempt ended while code is graded (section 5.5). showcaseShowcase null: a showcase is never scored by the AI.questionnaireQuestionnaire null: a questionnaire is never scored by the AI.unknownThe type of the assessment could not be determined Do not compare it. The same eight names filter the results list (
assessment_type) and the assessments list, andinterviewfilters for AI interviews and role-plays together.ai_interviewandinterviewalso keep the older interviews that have no recorded type. The assessments list returns the name inassessment_typebeside the rawset_typenumber.evaluationholds the AI evaluation for the submission: the interview evaluation on an interview set, and the CV evaluation on a CV set (assessment_type: "cv_evaluation").evaluation_cvis the separate CV evaluation attached to an interview set. Either can benull. A skill test, technical test, showcase or questionnaire has little or no AI evaluation text.evaluation_cvis alsonullwhen the CV it was written from is not one your company was given (section 5.1). So isevaluationon a CV evaluation (cv_evaluation), and on any other result that holds only a CV screening. The scores stay.Inside either object, a key you do not see is absent, not
null:competenciesis omitted entirely when there are no per-criterion rows, so testif (evaluation?.competencies)rather than comparing with[]. Both objects may also carryrating_status: "cannot_evaluate", meaning the AI ran but could not rate the attempt.competencies[].nameis thetitleof eachiai_requirements[]item you sent. Name them whatever you like; they come back under those names. A requirement you sent with an emptytitleis left out of the evaluation altogether.No transcript is returned here. The transcript and the recording links of an interview have their own route (section 5.10).
5.4 Ask for the full detail: ?include=
Two extras are opt-in. Combine them with a comma. rubric is opt-in because it costs an extra column read
most callers never need; justifications is opt-in because this same response feeds our in-product
assistant, and written reasoning about a candidate should be something you ask for.
| Value | Adds |
|---|---|
justifications |
The AI's written reasoning for each criterion, as competencies[].justification. |
rubric |
Up to 13 communication and cognitive dimensions, each with a score out of 5 and a written rationale. A dimension the evaluation did not produce is left out of the array. |
curl -s -H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
'https://apis.flowmingo.ai/company/integration/hiring/submissions/{submission_id}/detail/v1?include=justifications,rubric'
{
"evaluation": {
"competencies": [
{
"name": "Applied AI/ML Engineering",
"rating": 9,
"justification": "Described building and deploying two retrieval pipelines, including how they chunked and evaluated them."
}
]
},
"rubric": [
{ "key": "grammar", "label": "Grammar", "group": "communication", "score": 4, "score_max": 5, "rationale": "..." },
{ "key": "logical_reasoning", "label": "Logical Reasoning", "group": "cognitive", "score": 3.5, "score_max": 5, "rationale": "..." },
{ "key": "clarity", "label": "Clarity", "group": "cognitive", "score": null, "score_max": 5, "score_status": "cannot_evaluate", "rationale": "Too little sustained speech to judge." }
]
}
Two scales in one response. Your criteria (
competencies[].rating) andoverall_scoreare out of 10. The rubric is out of 5: every rubric row repeatsscore_maxso the two cannot be confused.
The rubric's group is communication (Grammar, Comprehension, Fluency, Vocabulary, Coherence) or
cognitive (Logical Reasoning, Critical Thinking, Problem Solving, Big-picture thinking, Intellectual
Self-Awareness, Insightfulness, Clarity, Decision Making). It is the same rubric shown in the report inside
Flowmingo.
score is null whenever the stored value is not a number between 0 and 5. When the AI ran and explicitly
could not rate a dimension, that row also carries score_status: "cannot_evaluate", so a null score with
no score_status simply means no usable rating was recorded. rationale can be null independently of the
score. rubric itself is null when the submission has none; it is produced for AI interviews.
An unrecognised include value returns 400 rather than being ignored, so a typo cannot look like "the
feature returned nothing".
5.5 When a score or evaluation is missing, or can still change
An AI interview or a role-play. A candidate may be given a window to improve their answers after
finishing. While that window is open the result is deliberately withheld, so you never store a score that
is about to change: status reads 6
(retaking), score_status reads "not_scored" even though a score exists, and overall_score,
submission_at, evaluation and rubric come back null. status: 6 is what distinguishes a withheld
result from one that was never scored.
For an AI interview or a role-play this is temporary and resolves itself. The interview.evaluation.update
webhook is held back for the same reason and re-sent by a job that runs every 10 minutes, so it arrives
shortly after the window closes. Read the detail endpoint when that webhook arrives rather than polling
straight after an interview ends. See the webhook guide.
A technical test: it completes first, and its score can change afterwards. The assessment.completed
event for a technical test says that the attempt ended. It never carries a score (score and
score_percent are null), so read the score from this API. Multiple-choice answers are marked when the
attempt ends, but the code a candidate submits is run against its test cases in a sandbox, and that can still
be running after the attempt ended. A multiple-choice answer that was still being saved as the attempt ended
can also be counted a moment later, so even a test without code questions can change after its event. A
technical test is not held back the way an interview is: its result is readable as soon as the attempt ends,
and until grading has settled its overall_score is provisional. A code answer that has not been graded yet
counts for nothing, so the first number you read can be lower than the one the result ends up with, and a late
change can follow. score_status reads scored for a provisional
number too, and no field, flag or second 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 the score, or read it again later
and keep the latest value; a longer wait lowers the chance of reading a provisional number but does not rule
it out. The results list (sorting by overall_score included) and the average in section 5.6 read the same stored
number and move with it; the completed and in_progress counts do not depend on its value.
5.6 The other read routes
Every list route takes page and limit (section 8.2) and answers { "data": [...], "total": n, "page": 1, "limit": 20 }, except the stage list, which answers { "data": [...] }. The project list adds truncated, note and largest_projects when more projects follow the page (section 5.7).
Assessments: GET /hiring/interview-sets/v1. Parameters: page, limit, external_id, assessment_type (one of the eight names in section 5.3, or interview for AI interviews and role-plays together; a value of up to 40 characters that is none of these returns an empty page, and a longer value is refused with 400). Each row: id, title, position, external_id, status (1 active, 2 draft, 0 inactive, 3 archived; the sender picks up an invitation to an AI interview or a role-play only while it is 1, see section 4.5), assessment_type, set_type (the same kind as a number: 1 AI interview, 2 CV evaluation, 3 skill test, 4 showcase, 5 technical test, 7 role-play, 8 recording, 10 questionnaire; null on an older AI interview), interview_duration (minutes), cfg_lingual_ids, project_ids and created_at. Question banks are never listed. Questionnaires (set type 10) are left out unless you ask for assessment_type=questionnaire. The hidden helper question sets that other assessments keep for their own after-questions are never listed, whatever you ask for, and they are not counted in total. AI assistants connected over MCP can filter the same way: the list_interview_sets tool takes an optional assessment_type. external_id is the id you gave the assessment when you created it (section 3), and it is null for an assessment made without one: in the dashboard, inside a project create call, or before the field existed. The parameter of the same name returns only the assessments whose external_id is exactly that text (section 8.3). It is not unique, so more than one assessment can match, and the answer is a normal page.
Hiring projects and their stages: GET /hiring/projects/v1 (parameters page, limit, external_id) returns each project's id, title, external_id, created_at and candidates (the number of candidates in its pipeline). external_id is the id you gave the project when you created it (null when you gave none). The parameter of the same name returns only the projects whose external_id is exactly that text, among the projects the key can reach, and total counts the matches (section 8.3). Section 5.7 shows how to find a project's id and what the list leaves out. GET /hiring/projects/{id}/v1 reads one project by its id, with its job post and assessments (section 3.5.13). GET /hiring/projects/{projectId}/stages/v1 returns the project's stages in pipeline order, each with id, name, position, stage_type, is_rejection and external_id (the id you gave that stage in stages when you created the project, and null for any other stage). A project that is not yours, or that the key cannot reach, answers an empty list, not a 404. A stage id is what you pass as com_stage_id to move an application (section 6.2).
Job posts: GET /hiring/job-posts/v1 (parameters page, limit, com_project_id, external_id) returns id, title, status, com_project_id, external_id and created_at. external_id is the id you gave the job post as job_post.external_id when you created its project (null when you gave none), and the parameter of the same name returns only the job posts whose external_id is exactly that text (section 8.3). GET /hiring/job-posts/{id}/v1 returns id, title, description, com_project_id, com_interview_set_id (the assessment candidates take for the role), external_id and created_at. It does not return status: read the status of a job post from the list. Neither says whether the job post's public page opens: for that, read its publication with GET /hiring/job-posts/{id}/publication/v1 (section 3.7.1).
Applications (the pipeline): GET /hiring/applications/v1.
| Query param | Type | Notes |
|---|---|---|
page, limit |
integer | As above. |
com_project_id, com_job_post_id, com_stage_id |
uuid | Only applications in that project, job post or stage. |
external_id |
text | Only the application whose own id is exactly this text: the application_external_id you sent to the invite route, or the external_id you sent when you added the person to the project (section 6.1). Case-sensitive, nothing trimmed, no wildcards. It is not unique, so more than one application can match, and the answer is a normal page. An empty value, one longer than 128 characters, or one sent twice is a 400 (section 8.3). |
applied_within_days |
integer | Only candidates who applied in the last N days. |
date_from, date_to |
ISO 8601 | Only candidates who applied on or after, or on or before, that moment. A date with no time means the start of that day (UTC), so write 2026-10-18T23:59:59Z to include the 18th in date_to. A value that is not a date is ignored. |
sortBy, order |
string | sortBy is applied_date or interview_score (best first); order (asc or desc, default desc) applies with sortBy=applied_date. These two names are camelCase. |
These are the only filters this route reads. Any other query parameter is ignored, not refused (section 8.3).
Each row has id, tal_candidate_id, external_id, com_project_id, project_name, com_job_post_id, stage (id, name, priority), interview_score, cv_score, interviewed, interview_score_status, cv_score_status, status and created_at. external_id is your own id for the application, null when none was set, and it does not need read_candidates. With read_candidates the row also has candidate_name, firstname, lastname and email. interview_score reflects AI interviews and role-plays only. A candidate who has only done a skill test, technical test, showcase, recording or questionnaire has interview_score null here, and interviewed is false for them. Read those results from the results list (section 5.2). interviewed is not the same as having an interview_score: it can be true while interview_score is null (an attempt the AI could not rate, with interview_score_status cannot_evaluate), so read the status words. When your organization has candidate sources switched on, each row also has source_type and source_detail; source_detail is free text and, like the names, needs read_candidates.
Invitations not yet completed: GET /hiring/interviews/upcoming/v1 (parameters page, limit, com_interview_set_id) returns id, tal_candidate_id, com_interview_set_id, external_id, status and scheduled_at, earliest scheduled first. external_id is the invitation_external_id you sent to the invite route. It is null when you set none, and for a Matched Talent invitee who has not started. There is no filter by it. status is pending (waiting to be sent), invited, sending or started. An invitation that was completed or failed to deliver is not listed, and neither is one whose candidate has been reminded or has opened the link, until the candidate starts. No email addresses are returned, and tal_candidate_id is null for a Matched Talent invitee who has not started.
Funnel counts: GET /hiring/overview/v1 (parameters com_project_id, com_interview_set_id; a project narrows every count (the result counts then cover the results on the assessments linked to that project, so a candidate of another project that shares one of those assessments can be counted too), while an assessment narrows only the result counts and leaves the stage and application counts for everything the key can reach) returns aggregate numbers only: stages (applications per stage), applications (active and rejected; rejecting moves an application into a stage, it is never deleted) and interviews (completed, in_progress and average_overall_score). completed counts finished results that carry a score, or the verdict cannot_evaluate (score_status cannot_evaluate, overall_score null); a finished result still waiting for its score counts as in progress. So it does not mean every counted result has a usable score. Showcases and questionnaires are never scored by the AI, so they count as in_progress. It also returns assessments, the same counts for each kind of assessment on its own (section 5.8).
Candidates: GET /hiring/candidates/v1 (needs read_candidates; parameters page, limit, q, email, com_project_id, external_id) returns the candidates in your pipelines with id, firstname, lastname, email, external_id, cv_link and created_at. external_id is the id you sent as external_id for the person to the invite route, kept on your company's record of them (null when none was set). To look up one person by it, send external_id: the candidates whose id is exactly that text come back (section 8.3), more than one when your system reuses an id. The lookup also finds a person your company invited to an assessment without a hiring project, who has a result (even one not started) on your assessments but no application the key can see. Those people are listed after the matches in your pipelines, and total counts both. A practice run or a try-out does not make a person visible, and a key whose creator is limited to some projects finds only the people it could already see (section 2.9). q and email narrow both groups, so a person without an application is found by their external_id together with their name, address or CV words just as one in a pipeline is. Only com_project_id limits the lookup to the pipelines, because a project is about applications. The lookup combines with the other parameters: a candidate must match all of the ones you send. The email of a row, here and on GET /hiring/candidates/{id}/v1, is the address your company holds for the person or, when it holds none, the one on the person's newest result (not a practice run) that carries an address, in an assessment of yours that the key's creator can open. An address on a result in an assessment the creator cannot open is never used, so email is null when there is no other. To look up one person by email address, send email: one address of at most 254 characters, matched exactly and without regard to capital letters. It is matched against the same address a row shows in email (the address your company holds for the person or, when it holds none, the one on their newest result that the key's creator can open), and never against an address the candidate keeps on their own Flowmingo profile. A person who deleted their Flowmingo account is found like anyone else your company holds. It looks only at the candidates this list returns, so total 0 means none of them has that address (not that Flowmingo has never seen it, and not that nobody holds it in a project you did not name when you also send com_project_id), and a value that is not a single address (blank, two addresses, no @) is refused with 400 instead of listing everyone. Compare the email of the row you get with the one you sent before you treat it as a match: a parameter spelled any other way (for example email[]) is not read and gives the ordinary list. It combines with q and com_project_id: a candidate must match all of the ones you send. To search by name or CV text, send q (at most 120 characters, matched against name, email and CV text). The query is split into words, and up to 8 are used. A candidate who matches any of the words is returned, and the ones that match more of them come first. In that case each row also carries relevance, matched_terms and total_terms. A query with no usable word in it (for example a single letter, or a name written in a non-Latin script) is matched as one piece of text instead, and those three fields are left out. GET /hiring/candidates/{id}/v1 returns one candidate, with its external_id, and adds phone and thumb_cv_link; if your organization restricts who may see a field such as the phone number, phone is null and restricted_fields lists it when the creator of the key is below that level (their role as it is now; if that cannot be checked, the phone is withheld too). A candidate you have no relationship with answers 404.
5.7 How to find a project's id
Several calls want the id of a hiring project: project_id when you create an assessment (section 3.6.4) or an interview set (section 3.2), com_project_ids when you invite (section 4.3), and com_project_id when you add a candidate (section 6.1) or filter a list. It is a UUID. GET /hiring/projects/{id}/v1 reads one project when you already hold its id (section 3.5.13), but there is no documented way to search projects by title. A project can be found by your own external_id if you gave it one when you created the project (point 2 below), and by the idempotency_key of the call that created it through the status read (point 1 below). This section lists the places the id comes from, best first.
1. The answer to the call that created the project. POST /hiring/projects/v1 (section 3.5) answers with the project's id: in the 201, in the 200 of a repeat, and in the 202 that says an assessment is still being made. For each assessment in that answer that is ready, assessment.project_id repeats the id. Save it when you get it.
If the answer never reached you, send the same request again with the same idempotency_key and the same body. You get the same project back, with idempotent_replay true (section 3.5.3). That is a way to recover an id, not a way to look one up:
- It needs the original body. A different body is a
409, and a project made in the dashboard has no key to repeat. - A repeat is checked like a first call. The person the key acts as (the person who created it, unless the key is bound to another user) must still be an Owner or Admin, or the answer is
403with the codeproject_create_requires_admin. - While the first call is still working, a repeat can answer
409idempotency_request_in_progress. Wait a few seconds and send it again. - If the first call made assessments, the repeat reports where each one has got to, can start or resume unfinished work and retry a failure marked
retryabletrue(which takes units from the generation budget, section 3.6.11), and normally waits about 45 seconds for them (section 3.5.5). - The repeat shows the project as it is now: its
statusreadsdeletedorarchivedif someone has deleted or archived it since.
Read the status of the call. If you hold the idempotency_key of the create call but not its body, GET /hiring/projects/requests/{idempotency_key}/v1 answers where the call stands, without sending it again and without creating anything. It needs the create_project scope, and the person who created the key must still be an Owner or Admin, as for the create call itself (section 3.5).
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" "$BASE/hiring/projects/requests/ats-job-48213/v1"
{
"idempotency_key": "ats-job-48213",
"status": "created",
"project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"project": {
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Backend hiring, Q4",
"external_id": "REQ-48213",
"deleted": false,
"archived": false
},
"job_post": { "state": "created", "failure_code": null },
"created_at": "2026-10-19T08:30:00.000Z"
}
statusiscreatedwhen the project and its job post are saved, which is what every project made now reads. Only a project created before 22 October 2026 can readpending(its job post is still being built) orjob_post_failed(job_post.failure_codesays why).projectis the project as it is now: itstitleandexternal_id, and whether it has since been deleted or archived.project_idis the id to use elsewhere. A key whose creator can open every project readsdeletedtrue; for a creator limited to some projects a deleted project is no longer one of theirs, so the read answers404.404with the codeproject_request_not_foundis the one answer for a key it does not know, a key that is not 8 to 128 characters fromA-Z a-z 0-9 . _ : -, a key of another organization, and a project the key's creator cannot open. It can also mean the project was not saved yet: a call that is still running, or that ended before the project was saved (a503, a429, a409or a crash), leaves no record. So a404does not prove that no call was sent. If you are unsure, send the create request again with the sameidempotency_keyand body, as described above.- The other refusals are the create call's:
403project_create_requires_adminand503project_create_role_unconfirmed(section 8.4).
2. List your projects. GET /hiring/projects/v1 needs the read_hiring scope.
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" "$BASE/hiring/projects/v1?limit=100"
{
"data": [
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Backend hiring, Q4",
"external_id": "REQ-48213",
"created_at": "2026-10-19T08:30:00.000Z",
"candidates": 56
}
],
"total": 1,
"page": 1,
"limit": 100
}
- Only
page,limitandexternal_id.pagestarts at 1 andlimitis at most 100 (section 8.2). There is no search and no sort.external_idreturns only the projects whoseexternal_idis exactly that text, andtotalcounts the matches (section 8.3). Any other parameter, such astitle,q,statusoridempotency_key, is ignored and not refused, so you get the ordinary list (section 8.3). There is no list filter byidempotency_key: the status read above is the way to find a project from its key. - Newest first, by
created_at. The list is read from a replica, so a project created a moment ago can take a few seconds to appear. To follow a create call you have just made, read its status (above). - Each row has
id,title,external_id,created_atandcandidates, the number of candidates in the project's pipeline (0when there are none).external_idisnullfor a project made without one. A page found withexternal_idnever haslargest_projects. - The projects the key can reach (section 2.9). A key made by an Owner lists every project of your organization, whoever made it. An id from the list is one the key can name, except an archived project (next bullet).
- Archived projects are listed and deleted ones are not. A row has no
status, so it does not say which are archived. The assessment routes accept only an active project and answer404project_not_foundfor an archived one. - Titles are not unique. Two projects can have the same title. Tell them apart by their
id;created_at,candidatesand the linked job posts help you recognise the one you want:GET /hiring/job-posts/v1lists each job post with itscom_project_id(section 5.6), and a project made through the API usually has a job post titled with itsposition. - When more projects follow the page you read, the answer also has
truncatedtrueand anote. The first page also haslargest_projectswhen at least one project has candidates: at most 5 projects with the most candidates, each withid,title,candidatesandhref. It never lists a project that has no candidates, so do not look for a project there. Thenoteis plain text that also serves AI assistants reading the same list; your code can ignore it. To read every project, ask forpage=1,page=2and so on untilpage * limitreachestotal. The pages are not a snapshot: a project created or deleted while you read, or two projects created at the same moment, can make you miss one or see one twice.
{
"data": [
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Backend hiring, Q4",
"external_id": "REQ-48213",
"created_at": "2026-10-19T08:30:00.000Z",
"candidates": 56
}
],
"total": 42,
"page": 1,
"limit": 20,
"truncated": true,
"largest_projects": [
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Backend hiring, Q4",
"candidates": 56,
"href": "/company/hiring-project/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/candidates"
}
],
"note": "Showing projects 1-20 of 42, newest first, and the per-row candidate counts cover ONLY those. 22 project(s) are not on this page. ..."
}
This is the first page of 42 projects. data is cut to one of its 20 rows, largest_projects to one of its 5 entries and note to its first two sentences.
3. From something that already points at the project.
| You have | Read | The project id is in |
|---|---|---|
| A job post | GET /hiring/job-posts/v1 or GET /hiring/job-posts/{id}/v1 |
com_project_id |
| A candidate in a pipeline | GET /hiring/applications/v1 |
com_project_id, with the project's title in project_name |
| An assessment | GET /hiring/interview-sets/v1 |
project_ids, a list, because an assessment can be in several projects |
| A result | GET /hiring/submissions/v1?with_project=true |
com_project_id, which is null unless the assessment is in exactly one project that has not been deleted |
| An event or a webhook | application.created, candidate.stage_changed and application.rejected |
data.com_project_id, when present. No other event type carries a project id |
project_ids lists only projects that have not been deleted. An assessment that was in a project which has since been deleted shows an empty list, or only its other projects. An archived project is not deleted, so it stays in project_ids and in the project list, although the assessment routes refuse it (section 3.6.4).
4. From the dashboard. Open the project in the Flowmingo dashboard. Its address contains the id: /company/hiring-project/<project id>, followed by the name of a tab such as /candidates.
5. Checking one id. To check an id you already hold, read the project with GET /hiring/projects/{id}/v1 (section 3.5.13). It answers the project, or a 404 with the code project_not_found when the id is not a live project of your organization or the key cannot reach it (an id that is not a UUID is a 400). You can also find the id in the list, or call GET /hiring/projects/{projectId}/stages/v1. It answers the project's stages when the id is a live project of your organization. When it is not, or the key cannot reach it, or the project has no stages, it answers an empty data list and not a 404 (an id that is not a UUID is a 400). It tells you nothing else about the project.
The names. The same id is called different things in different places:
| Where | Name |
|---|---|
POST and GET /hiring/projects/v1 |
id |
| Create an assessment or an interview set (the request), and the assessment in the answer | project_id |
| The assessments list | project_ids (a list) |
| Invite (the request) | com_project_ids (a list); the older com_project_id still works |
Job posts, applications, results with with_project, add a candidate, the com_project_id filters, the invite answer, events and webhooks |
com_project_id |
5.8 Counts and averages by assessment type
GET /hiring/overview/v1 (section 5.6) returns the funnel for everything the key can reach (section 2.9), or for one project (com_project_id)
or one assessment (com_interview_set_id). With a project, the result counts are those on the assessments linked to it (section 5.6).
{
"stages": [{ "com_stage_id": "7c1e...", "stage_name": "Interview", "count": 12 }],
"applications": { "active": 40, "rejected": 7 },
"interviews": { "completed": 28, "in_progress": 12, "average_overall_score": 6.8 },
"assessments": [
{ "type": "ai_interview", "completed": 20, "in_progress": 4, "average_score": 7.1 },
{ "type": "skill_test", "completed": 8, "in_progress": 3, "average_score": 6.2 },
{ "type": "showcase", "completed": 3, "in_progress": 2, "average_score": null }
]
}
The numbers above are illustrative. What each part means:
assessmentshas one entry for each kind of assessment your company has sittings for, always in this order:ai_interview,role_play,skill_test,technical_test,cv_evaluation,showcase,recording,questionnaire,unknown.typeis the same value asassessment_typein section 5.3. A kind with no sittings has no entry.unknowngathers any kind that has no label of its own. A questionnaire is never scored, so a finished one is counted asin_progress, notcompleted.average_scoreis on the 0-10 scale, over finished sittings only. It isnullwhen none of them has a score. A showcase is rated by hand, so its average is usuallynull.interviewsadds every kind together, and it is not a count of interviews: it includes CV screenings, skill tests and the rest. For "how many interviews", addai_interviewandrole_playfromassessments.completedandin_progress. A sitting is completed when it is finished and carries an overall score, or the verdictcannot_evaluate(which has no number and adds nothing to an average); a showcase is completed when it is submitted. Everything else isin_progress: a sitting still being answered, one whose result is held (section 5.5), and one that is finished but still waiting for its score. A sitting still being answered or held is left out of every average, even if it already carries a partial score. Because a submitted showcase counts as completed inassessments, thecompletedvalues there can add up to more thaninterviews.completed. In the example above, the 3 submitted showcases are why 31 is more than 28.
5.9 Find a record by your own id
Many records carry the external_id you gave them when you created them (the rule is in section 8.8). This table says where you set each one, where you read it back, and where you can look a record up by it. A lookup is an exact, case-sensitive match (section 8.3), and an id is not unique, so a lookup can return more than one record: tell them apart by their Flowmingo id.
| Record | You set it with | You read it on | Look up by it |
|---|---|---|---|
| Hiring project | external_id of the create call (section 3.5) |
GET /hiring/projects/v1, GET /hiring/projects/{id}/v1 (section 3.5.13), and project.external_id of the status read (section 5.7) |
GET /hiring/projects/v1?external_id= |
| Job post | job_post.external_id of the create-project call (section 3.5) |
GET /hiring/job-posts/v1 and GET /hiring/job-posts/{id}/v1, and job_post.external_id of GET /hiring/projects/{id}/v1 |
GET /hiring/job-posts/v1?external_id= |
| Stage | stages[].external_id of the create-project call (section 3.5) |
GET /hiring/projects/{projectId}/stages/v1 |
No |
| Assessment | external_id of the create call (sections 3.2 and 3.6) |
GET /hiring/interview-sets/v1 |
GET /hiring/interview-sets/v1?external_id= |
| Question or criterion of an assessment | external_id of the entry in iai_questions or iai_requirements (section 3.2) |
The answer of the create call only | No |
| Person | external_id of the invite row (section 4.3.1) |
GET /hiring/candidates/v1 and GET /hiring/candidates/{id}/v1 |
GET /hiring/candidates/v1?external_id= (also finds a person invited to an assessment without a project, unless you also send com_project_id; section 5.6) |
| Application | application_external_id of the invite row, or external_id when you add a person to a project (sections 4.3.1 and 6.1) |
GET /hiring/applications/v1 |
GET /hiring/applications/v1?external_id= |
| Invitation | invitation_external_id of the invite row (section 4.3.1) |
GET /hiring/interviews/upcoming/v1 |
No |
| Result | external_id of an entry in the recordings call (section 4.7.3) |
GET /hiring/submissions/v1, GET /hiring/submissions/{id}/v1 and .../detail/v1 (sections 5.2 and 5.3) |
No. external_id on the results list is a 400. |
| Note, rating or shortlist mark | external_id of the call (sections 6.4 to 6.6) |
The answer of the call only | No |
| Zoho Recruit order | external_id of the order call (section 9.6) |
The answer of the order call, and GET /ats/zoho-recruit/orders/{id} |
No |
A record made before the field existed, a record made without an id, and a record that a candidate or a background job made all read null.
5.10 Read the transcript and the recording links of an interview
GET /hiring/submissions/{id}/media/v1 returns, for one finished AI interview or role-play, the text the system stored for each question and answer (one page at a time) and a link to each recording that can be handed out (the clips below say which cannot, and why). One word, availability, says whether the answer is ready. It only reads. Required API key scopes: both read_hiring and read_candidates. A transcript and a recording are candidate data, so a key that may not read candidate details may not read them. No scope was added for it: a key that already holds both can call it. The {id} is the result's id from section 5.2.
Call it after the interview is finished, for example when the interview.status.update event arrives with the status completed, or the interview.evaluation.update event once the interview is scored (a one-way interview is not scored, and the assessment.completed event is not sent for AI interviews and role-plays; section 5.5 says when a result is held). Calling it earlier does no harm: it only reads, and the answer says why nothing is returned yet. The answer is sent with Cache-Control: no-store, and each call makes new links for the clips that are ready.
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" \
"$BASE/hiring/submissions/59065d06-a013-44c2-97d2-88e9d6b3c858/media/v1?page=1&limit=50"
{
"submission_id": "59065d06-a013-44c2-97d2-88e9d6b3c858",
"assessment_type": "ai_interview",
"availability": "ready",
"reason": null,
"transcript": {
"status": "ready",
"reason": null,
"total_entries": 2,
"page": 1,
"limit": 50,
"entries": [
{
"index": 1,
"question": "Tell me about a time you led a project.",
"answer": "Last year I led the migration of our payments service to a new provider...",
"truncated": false
},
{
"index": 2,
"question": "How do you decide what to do first when everything is urgent?",
"answer": "I ask what happens if each item waits a day...",
"truncated": false
}
]
},
"clips": [
{
"id": "b4f1c2d3-1111-4a2b-8c3d-5e6f7a8b9c01",
"kind": "video",
"question_index": 1,
"status": "ready",
"reason": null,
"url": "https://media.example.com/2026/11/06/10_00_00_abc/clip.mp4?X-Amz-Expires=3600",
"expires_at": "2026-11-06T11:00:00.000Z"
},
{
"id": "b4f1c2d3-2222-4a2b-8c3d-5e6f7a8b9c02",
"kind": "video",
"question_index": 2,
"status": "not_available",
"reason": "upload_never_arrived",
"url": null,
"expires_at": null
}
],
"clips_truncated": false,
"clips_truncated_reason": null
}
| Query parameter | What it does |
|---|---|
page |
The page of the transcript, starting at 1, at most 1000. Default 1. |
limit |
Transcript entries per page, 1 to 200. Default 50. A larger value is refused with a 400, not shortened. |
Send page and limit once each: a repeated key is refused with a 400, and so is a page or limit that sits beyond the first 1,000 parameters of the query string, because it cannot be read. Only the transcript is paged. The clips are not: when an answer lists clips it lists all of the interview's clips (at most 500), whatever the page.
| Field | What it holds |
|---|---|
submission_id |
The result you asked for. |
assessment_type |
The kind of result, as on the other result routes (section 5.3): ai_interview or role_play for the results this route reads. |
availability |
The whole answer in one word: see the next table. Every field of the answer is always present. |
reason |
Why availability is not ready, as a word you can branch on (the tables below). null when availability is ready and while it is held. |
transcript |
The questions and the stored answers: status (ready, processing, held or not_available), reason (why it is not ready: the words of availability that are about the interview itself, such as analysis_in_progress, or no_transcript_stored when no answer has text other than blanks; null when ready and while held), total_entries (how many questions the interview holds in all, whether or not each has stored text; null unless status is ready), page, limit and entries (empty unless status is ready). |
clips |
One entry per question that has a stored recording, in interview order, at most 500. Empty when there are none, while the result is held or processing, for a kind of result this route does not read, and when the analysis did not finish or its state is in doubt. |
clips_truncated, clips_truncated_reason |
true and clip_limit_reached when more than 500 recordings exist and only the first 500 are listed; otherwise false and null. |
availability is the answer in one word:
availability |
reason |
What it means |
|---|---|---|
ready |
null |
The data this route could resolve is present: the transcript is ready, or at least one clip is ready, and your plan's recording limit was not in doubt. If a clip could have had a link but the limit could not be confirmed, availability is not_available with lock_state_unavailable even when transcript.status is ready. Always read transcript.status and each clip's status for the detail. It does not promise that a clip's file exists: see the clips below. |
held |
null |
Your company may not see this report yet: the candidate's improvement window is open (section 5.5), or the candidate has not submitted the interview, whatever status it shows in the dashboard. Nothing is returned. An old interview that finished under an earlier version of our system, without a submission time ever being recorded, can also read held for good. |
processing |
analysis_in_progress, recently_changed or retake_incomplete |
Nothing is returned yet: ask again later. analysis_in_progress: the interview is still being analysed. recently_changed: an answer was added or changed in the last minute, as happens while a candidate's paid retake is being saved. retake_incomplete: a change to the candidate's answers, such as a paid retake, has been started and its last step has not been recorded, so the interview may be only partly replaced; nothing is returned while that record stands. |
locked |
video_retention_window_passed or video_plan_limit_reached |
Your plan's recording limits hide a recording (see the clips below; the route does not check that the file exists), and no answer has text other than blanks, so there is no transcript. |
not_available |
See the next list | Either this kind of result is not read by this route, or the analysis did not finish or its state is in doubt (no transcript and no clips are returned), or no answer has text other than blanks and no clip is ready or locked (no transcript; the clips are listed with their own reasons), or your plan's limit could not be confirmed (lock_state_unavailable: the transcript keeps its own status, which can be ready or not_available, and the clips are still listed, at most 500; the clips that would have had a link are not_available with the same reason). |
The reasons of a not_available answer:
live_interview_not_supported: a live voice-to-voice interview. It keeps one continuous recording and the whole conversation, and this route does not return them.assessment_type_not_supported: a recording evaluation, skill test, technical test, showcase, questionnaire or CV evaluation, or any result this route cannot positively identify as a standard interview.analysis_incomplete: the analysis of the interview had not finished seven days or more after the interview was submitted (or last retaken).evaluation_state_unknown: given only while the interview is still marked as being analysed. Its records disagree about whether it will be analysed, or hold a value this route does not know, so it cannot be told whether the analysis is still running. Nothing is returned.lock_state_unavailable: your plan's recording limit could not be confirmed just now (see "Your plan's recording limit" below). Ask again.no_transcript_stored: the interview is finished, but no answer has text other than blanks, and no clip is ready or locked (with a ready clip the answer isready, with a locked clip it islocked). The clips are still listed, each with its own reason. If your plan's limit could not be confirmed, the answer'sreasonislock_state_unavailableinstead, and the transcript's own reason staysno_transcript_stored.
The answer comes from checks that run in this order, and the first that applies decides it: whose result it is (404), a held report (held), the kind of result (not_available), the submission time (held again, for a supported kind), a retake under way (processing), the analysis state (processing or not_available), an answer changed in the last minute (processing). Only after all of them are the transcript and the clips read.
What this route reads. Only a standard, turn-based AI interview or role-play, whose recordings and answer text are kept per question. Anything else answers not_available with a reason, and no transcript and no clips, unless the report is held: a held result answers held whatever its kind, and a result the key cannot open answers 404, because those checks run first. A live interview's recording, the file of a recording evaluation and the answers of an interview the candidate has not submitted are not returned. The route is built to leave out the platform's own screen recording of an interview, and any file the platform converted or polished from one; it cannot tell what a candidate's own camera upload shows.
Who can read what. The key acts with its creator's reach, as on the other result routes (section 2.9). A result of another organization, a result the key's creator cannot open, a practice or trial run, a deleted result and an id that does not exist all answer the same 404 with the code submission_not_found, so the answer never tells you which.
The transcript. entries holds, for each question of this page, index (the question's 1-based position among the interview's questions, the number a clip's question_index points at), question (the question text the system stored for the answer: its question content, else its question, else its title; null when none is stored; this route does not shorten it), answer and truncated. The transcript is the text the system stored for each answer, nothing more: it is not a recording of every word. answer is null when nothing, or only blanks, was stored for the question. At most 5,000 characters are stored for an answer, so when truncated is true the stored text may have been cut when it was stored, and no longer text exists. A page beyond the last returns no entries.
The clips. Each clip is one question's recording: id (the id of the answer the recording belongs to; a retake archives that answer and inserts a new one with a new id, so the id does not identify the question across calls), kind (video or audio, judged from the file name; a container such as .webm is called video), question_index (matches the clip to its transcript entry within the same response; it is not an identity across calls or retakes, and the entry can be on a page you did not ask for, because the clips are not paged), status, reason, url and expires_at.
Clip status |
reason |
What it means |
|---|---|---|
ready |
null |
A link was produced and is in url, with expires_at one hour after it was made. The file itself is not fetched when the link is made, so it may turn out to be missing. |
locked |
video_retention_window_passed or video_plan_limit_reached |
Your plan hides this recording, by the same rule as the dashboard: it is older than your plan keeps recordings, or your plan's number of recordings is used up. No link is made. The route does not check that the file exists. |
not_available |
upload_never_arrived |
The system's own check of the stored link judges that this recording was never uploaded. It looks at the link, not at the file, so it is a judgment and not a lookup. |
not_available |
link_not_servable |
The stored address is not an address on Flowmingo's own storage of the exact form this route signs: it is on another host, or its path, file type or spelling is different. |
not_available |
screen_recording_excluded |
The stored object is on record in your organization's separate media records of an interview (the platform's own screen recording or another separate take of it), or it is a file converted or polished from one. The route is built to leave these out. |
not_available |
recording_not_verified |
The file does not pass the system's checks as an answer recording, so no link is made. For example, there is no file record for it, or the records of where a converted or polished copy was made from cannot be followed, or the job recorded for this interview that published a polished file has no source address (missing, null or blank); only the polish jobs recorded for this interview are looked at. When in doubt, no link is made. |
not_available |
lock_state_unavailable |
Your plan's recording limit could not be confirmed just now. No link is made. |
A link is made only for a stored recording address on Flowmingo's own storage that passes the route's checks: the address, the file record, the known lineage of screen recordings and your plan's limits. The route does not look at what is in the file, so this does not show whose recording it is: a link names a file that anyone who holds its address can already open.
A link is a secret. Anyone who holds it can open the file, so keep it out of logs and do not forward it to a person who should not see the recording. Every link is signed and expires an hour after it was made (expires_at), but the file's permanent address is inside the link, so access may continue after that time. Do not treat the expiry as a limit: fetch a new link each time you need one.
Your plan's recording limit. Recordings follow the same plan limit as the dashboard. The plan is asked only when at least one clip could get a link, and only when recording limits are in force. If its answer cannot be confirmed just now, each clip that would otherwise get a link is not_available with lock_state_unavailable, and availability is not_available with the same reason. The transcript stays in the answer under its own status. Ask again. Whenever the plan is asked, a plan whose answer shows no recording limit at all is withheld in the same way. A clip that would not get a link anyway keeps its own reason.
What you read is the state at the moment of the call. A retake that is saved a moment later shows in the next call. The route holds content back while the system knows a change is under way (a held report, a retake that has begun or whose last step is not recorded, an answer changed in the last minute). A candidate's own retake that stopped part-way without leaving any record cannot be known, so the answers read as they stand and the interview can be only partly replaced.
When it is busy. Each call is read from one consistent view of our data, and a server handles only a few of those at once, and no more than half of them for one organization. A call that cannot get a place within a few seconds is refused with 503 and the code media_busy, with a Retry-After header (3 seconds), before anything is read. Nothing was changed, so send the same call again after that wait. Build your client to expect it, and do not send many of these calls at once.
| Status | code |
What to do |
|---|---|---|
400 |
(no code) | The id is not a UUID, or page or limit is not a whole number in its range, is sent more than once, or cannot be read. |
401, 403 |
(no code), or a code of section 11 | The key is not accepted, lacks read_hiring or read_candidates, has expired, or is refused for a reason that has its own code (section 11). |
404 |
submission_not_found |
The same answer for each case described above. |
503 |
media_links_unavailable |
The recording links could not be made just now. Nothing was changed: send the same call again in a moment. |
503 |
media_busy |
Too many recording requests were being served. Nothing was read: send the same call again after the seconds in Retry-After. |
5.11 Ask a question about your hiring data
POST /data/ask/v1 answers a question about your own hiring data, written in plain words, for example "How many AI interviews were completed last week?". Required API key scope: read_hiring. It is the same data AI that answers questions in the Flowmingo dashboard and, through the ask_data tool of the MCP server (see the MCP guide), in your AI assistant. Read the answer as a figure Flowmingo worked out for you, not as an audited report: kind and working say how it was reached, and an exploratory answer comes from a query that Flowmingo has not reviewed.
Who is asking. The person who created the key, and nobody else. The answer is built from what that person can open in Flowmingo, narrowed by the scopes of the key, and it follows their access when it changes. What happens to a key when its creator leaves is described in section 2.10. A question that needs candidate names or email addresses needs the read_candidates scope as well. The body takes a question and an optional timezone. A field that names a company, a person or a project is refused with a 400, because the key alone decides whose data is read.
curl -s -X POST -H "X-API-Key: $FLOWMINGO_API_KEY" -H "Content-Type: application/json" \
"$BASE/data/ask/v1" \
-d '{"question": "How many applications did we get this year?", "timezone": "Asia/Singapore"}'
| Field | Type | Meaning |
|---|---|---|
question |
string, required | The question in plain words, up to 2,000 characters (some symbols, such as emoji, count as two). |
timezone |
string, optional | Your timezone as an IANA name, for example Asia/Singapore. Used when the question names dates ("last week", "yesterday"); UTC is used when none applies. A timezone named in the question itself can take precedence over this field, and some valid names fall back to UTC; when an answer has a working.date_range, it says which timezone was used. An exploratory answer may read its dates in UTC whatever you send, so check its dates. |
An answer has this shape (the values are illustrative, and the fields vary with the question):
{
"status": "ok",
"kind": "form",
"answer": {
"columns": ["applications", "people"],
"rows": [[66, 53]],
"truncated": false
},
"working": {
"metric": "candidates.count",
"filters": [],
"date_range": { "from": "2026-01-01", "to": "2026-11-09", "timezone": "Asia/Singapore" },
"group_by": [],
"as_of": "2026-11-09T08:30:00.000Z",
"text": "Counted applications and people from 1 January to 9 November 2026.",
"freshness": "current"
},
"request_id": "e3d5f7b9-2c4e-4a6b-8d0f-1b3c5e7a9d2f"
}
Read status first. Flowmingo decides about a question itself, and it answers every decision, including a refusal that is about your key, as a 200 with the reason in status, gap and message, so a client has one shape to read. A problem with the call is an HTTP error instead: 401 for a key that is missing or wrong, 403 when the key lacks read_hiring, has expired or belongs to a person who has been banned, 400 for a bad body and 429 past the limits below.
status |
What it means | What to do |
|---|---|---|
ok |
The question was answered. answer holds the columns and rows. |
Quote the answer with working.text as kind below says, and say if kind is exploratory. If answer.truncated is true, more rows matched than are shown: do not add a total up from the rows. |
cannot_answer |
The question cannot be answered as asked. gap is a stable code, message says it in words, and did_you_mean can list labels to try instead. |
Show message. If it suggests something, ask again with more detail. |
busy |
Flowmingo cannot take the question right now. retry_after_seconds and a Retry-After header say how long to wait. |
Ask again after that wait. |
unavailable |
Flowmingo could not check just now. It is never an answer of zero. | Do not report a number. Show message and follow it. |
refused |
Flowmingo will not answer this question for this key, for example because asking is not switched on for your company (gap not_enabled), the key's creator has no access to any project, the key lacks a scope, or a daily AI allowance is used up (gaps daily_ai_limit_person and daily_ai_limit_company). |
Read gap and message. Check the key with GET /me/v1 (section 2.7). |
kind and working. form is one of Flowmingo's fixed definitions; exploratory is a query the data AI wrote for this question, which Flowmingo has not reviewed: check an exploratory answer against your own data, and say that it is exploratory when you quote it. working is a short account of how an answer was reached. It is not a complete list of the rules applied, and an exploratory answer's working may carry no filters or dates at all. Quote working.text with a form answer. With an exploratory answer, do not repeat anything in working.text that says the figures are exact or complete: say only that it is an unreviewed query. Treat working.freshness as a hint about how current the data is. The gap codes are a fixed list in the OpenAPI document; branch on status and gap, and show message.
Limits. A question is usually answered, or reported as unavailable, within about 25 seconds, so give the call a timeout of at least 40 seconds. Your company may ask 20 questions a minute and 1,000 in 24 hours, counted for the whole company and not per key; each window starts at the company's first question in it, not at a UTC boundary. Past that, the route answers 429 with the code data_rate_limited and a Retry-After header, and on that answer X-RateLimit-Limit and X-RateLimit-Remaining describe that company window and X-RateLimit-Reset is a number of seconds to wait, not a time. On any 429, read Retry-After. The request limits of section 8.5 apply to this route as well.
Is it available to your key? GET /me/v1 carries features.ask_data, a hint for your client and not a permission: it is true when asking is switched on for your company and the key carries read_hiring, and false when it is not or when that could not be read just now. The route checks the key again on every call.
5.12 Read your email sequences
An email sequence is a series of emails that a recruiter writes once in Flowmingo. Flowmingo sends them from the recruiter's own connected Gmail or Outlook mailbox, and a person can be stopped when they reply, unsubscribe, bounce or start the assessment. These four routes only read. They cannot create or edit a sequence, switch one on or off, add anybody to one, or pause or stop a person. They give numbers of people, not who the people are. They do not give how many people replied or how many addresses bounced.
read_sequences is the one scope for all four. It is opt-in: a key does not have it unless it is chosen when the key is created or edited (section 2.3). No route needs read_candidates, and holding it gives a key nothing more here. The MCP server has no tool for these routes, and there is no scope that writes a sequence.
| Operation | What it answers | Cost against the limit (section 5.12.6) |
|---|---|---|
GET /sequences/v1 |
The library: your sequences, newest first, one page at a time. | 1 unit |
GET /sequences/{id}/v1 |
One sequence with its current version: its emails and stop rules. | 1 unit |
GET /sequences/{id}/results/v1 |
The numbers of one sequence over a range. | 2 units |
GET /sequences/{id}/activation-preview/v1 |
What switching a sequence on would do. | 2 units |
There is no route that gives the numbers of every sequence in one call: read the results of each sequence. A company-wide count of different people cannot be had by adding up the per-sequence counts.
A parameter in the address that a route does not name is normally refused with 400 and the code unknown_field, so a parameter that the route does not have is not quietly ignored. Send the API key in the x-api-key header, not in the address. A search is not offered.
5.12.1 The library
GET /sequences/v1 takes status (off or active), page (from 1) and page_size (1 to 100, default 50), and answers { "data": [...], "total": n, "page": 1, "limit": 50 }: total is how many sequences match and data is the page. Every page costs 1 unit of the limit, whatever page_size is.
{
"data": [
{
"id": "4f1c2a3b-5d6e-4f70-8a91-b2c3d4e5f607",
"name": "Follow up after the application",
"status": "off",
"off_reason": null,
"off_at": null,
"mode": "review_each",
"sender_policy": "fixed",
"mailbox": { "connector_type": "gmail", "address": "recruiting@example.com" },
"version_id": "a7b8c9d0-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"version_no": 1,
"created_at": "2026-10-19T08:30:00.000Z",
"updated_at": "2026-10-19T08:30:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 50
}
| Field | Meaning |
|---|---|
status |
off: switched off. active: switched on. A person switches a sequence on or off in Flowmingo. |
off_reason, off_at |
When the system switched the sequence off, off_reason says why (plan_entitlement_lost: the plan stopped including email sequences). Both are null otherwise. |
mode |
review_each: every email waits for a person to approve its exact text before it goes. auto: emails go out on their own once the sequence is on. |
sender_policy |
enroller: the emails go from the mailbox of the person who adds the people. fixed: from one named mailbox. |
mailbox |
The mailbox the emails go from when the sequence names one (connector_type is gmail or outlook, and address is the address of that mailbox's account, which can be null). null when the sequence names no mailbox, for example when it sends from the mailbox of whoever adds the people. |
version_id, version_no |
The current version of the text, numbered from 1. |
A row carries no email text and no numbers of people, on purpose: read one sequence for its emails, and its results for the people in it.
5.12.2 One sequence
GET /sequences/{id}/v1 takes no parameter. It answers the fields above with step_count, span_seconds, incomplete and the current version:
version.stepslists the emails in order. Each hasid,order,delay({ "n": 2, "unit": "days" }: the wait before the first email for step 1, and after the previous email for the others),subject,body_htmlandthread_mode(newfor a new email,replyfor an answer in the same conversation as the first email).body_htmlis the text as stored, which is HTML, with merge fields written like{{candidate_first_name}}. There is no plain-text copy.subjectof a reply step may be empty (it goes as "Re:" the first email's subject). A new email of a sequence that is not complete yet may have none either:incompletethen listsstep_no_subject, and the sequence cannot be switched on until it has one.version.stop_rulessays what ends a person early:starts,applies,replied,removed,expires(each true or false),unsubandbounce(these two aretrueon every sequence and cannot be switched off), andstage(kindisoff,any,typeorstage, withstage_typefortype).incompletelists what is missing before the sequence can be switched on, and is empty when it is complete:no_mailbox,no_steps,step_no_subject,step_no_bodyorstage_gone.span_secondsis the sum of every email's wait, the first email's wait included.step_countis how many emails the sequence has.
The company's postal address and its other settings are not part of the answer.
5.12.3 The results of one sequence
GET /sequences/{id}/results/v1 takes range (today, 7d, 30d, all or custom, default all), from and to (with custom: YYYY-MM-DD, at most 366 days) and tz (your IANA time zone, for example Asia/Singapore; UTC when missing or unknown, and window.zone says which was used). A range, from or to that is not valid is a 400 with the code invalid_range.
{
"sequence_id": "4f1c2a3b-5d6e-4f70-8a91-b2c3d4e5f607",
"range": "all",
"people": { "enrolled": 12, "waiting": 5, "held": 1, "paused": 0, "stopped": 4, "completed": 2, "stopped_by_reason": { "replied": 1, "started": 3 } },
"sent_per_step": [{ "step_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "order": 1, "sent": 10 }],
"started_assessment_within_7d": 3,
"unsubscribed": 0,
"people_emailed": 8,
"window": { "zone": "UTC", "from": null, "to": "2026-11-10" }
}
The numbers of people by state (people) are where everyone is now, whatever the range: a person who finished last month is still completed under today. Only the dated figures follow the range: sent_per_step, people_emailed, started_assessment_within_7d and unsubscribed. A person whose next email Flowmingo is holding back is counted as held. Read what each number counts before you quote it:
peoplecounts enrolments, not individual people. An enrolment is one person's place in the sequence, made each time they are added to it. A person who joined the sequence twice (for example finished it and joined it again through another application) has two enrolments and is counted once for each: inpeoplethey appear in two states (saycompletedandwaiting), andenrolledcounts both.started_assessment_within_7dandunsubscribedalso count enrolments.started_assessment_within_7dis how many enrolments started the assessment they follow within seven days of their first email, counted on the day the sitting began; one sitting can follow the emails of two enrolments and count twice.unsubscribedis how many enrolments were stopped because the person unsubscribed. An unsubscribe is counted by the date its enrolment was last updated: normally the day of the stop, but the late settlement of an email that was still being sent when the person unsubscribed can move it to a later day. Do not call any of these the number of people.people_emailedcounts candidacies, not different people. Each application or invitation that got at least one email in the range counts once (the count can include other enrolments too). A person who applied for two jobs and got mail through both is counted twice, and so is one who finished the sequence and joined it again through another application.sent_per_stepcounts emails. It lists the steps of the version the sequence is on (a step nobody was sent is a0) and any earlier step that had emails sent in the range. If more than 200 earlier steps had emails sent in the range, the answer is503with the coderesults_busy: ask for a shorter range.people.stopped_by_reasoncounts stops. It lists the reason Flowmingo recorded when it stopped a person, for the reasons that occur. A reply that did not stop the person, or a failed address that arrived after the person had finished the sequence, is not in it, so it is not a count of replies or of bounces.
These figures count the emails Flowmingo has recorded as sent: an email that a provider accepted a moment ago can show up here a few minutes later. The route has no count of opens or clicks. The people counted are the people of the projects the key's creator can open now, and a project that was deleted is not one of them. An Owner also has the people who belong to no project counted.
5.12.4 What switching a sequence on would do
GET /sequences/{id}/activation-preview/v1 takes tz and answers how many waiting people switching the sequence on would release (would_release), how many it would end because the day their next email was due has passed (would_skip, which is 0 unless your company uses fixed dates), and fixed_dates, whether that rule is on for your company:
{ "would_release": 3, "would_skip": 0, "fixed_dates": false }
Only a key whose creator is an Owner or an Admin may ask (403 otherwise). The answer is advisory: people can join or change before somebody switches the sequence on in Flowmingo, which is where that is done. Nothing in the API switches a sequence on, and this route only reads: it does not send an email. The setting that says whether your company uses fixed dates is read when you call. When it cannot be read, the answer is 503 with the code activation_preview_unavailable (nothing is guessed and nothing was changed: ask again), except that a database that ran out of time on it is the 503 sequence_reads_timed_out with Retry-After.
5.12.5 Availability and access
- Open for your company. Email sequences must be open for your company. When they are not, the routes answer
404with the codesequences_not_available, unless the call is refused for another reason first. A sequence that is not there for your company is a404too, with the codesequence_not_found.GET /me/v1has no flag for it. When the setting cannot be read just now, the answer is a503withRetry-After(sequence_reads_unavailable, orsequence_reads_timed_outwhen the database ran out of time): ask again. - Plan. Your plan is not checked: the four reads work on every plan, including for a company that has downgraded.
- Whose permissions. The key acts with its creator's permissions as they are on the call: the figures cover the projects the creator can open now, and a project that has been deleted is not one of them. An Owner also has the people who belong to no project counted.
- A creator who has left. A key whose creator has left the company is refused on the sequence routes, normally with
403, even during the grace period that other routes normally allow (section 2.10). The code is normallykey_creator_in_gracewhile the grace period runs, andkey_creator_leftafter it. - A ban. A key whose holder has been banned is refused with
403and the codeuser_banned. When that cannot be checked just now the answer is503, and nothing was read. - Checked again. The key and its creator are checked again while a request is served, and a refusal at that check comes with an error status (section 5.12.7 lists the codes).
5.12.6 Limits
Your organisation, all its keys together, may spend 30 units a minute by default on reads of email sequences (the cost of each route is in the table above). Past that, the answer is 429 with the code sequence_reads_rate_limited and a Retry-After header. While too many large requests are already being served, the answer is 429 with the code sequence_reads_busy, which counts nothing against your limit. When the limit cannot be checked, or a read takes too long, the answer is 503 with the code sequence_reads_unavailable or sequence_reads_timed_out and a Retry-After header. Asking again straight away can be 429 sequence_reads_busy. These routes only read, so asking again is safe. The limit of 600 requests a minute for each key (section 8.5) applies as well.
5.12.7 Errors on these routes
Branch on statusCode and code, never on the wording of message.
| Status | code |
Meaning |
|---|---|---|
400 |
unknown_field |
The address carries a parameter the route does not name. |
400 |
invalid_range, invalid_status, invalid_page, invalid_page_size or none |
range, from or to is not valid (invalid_range), status is not off or active (invalid_status), page or page_size is not valid (invalid_page, invalid_page_size), or the id is not a UUID (no code). |
401 |
none | The key is missing, wrong or revoked. |
403 |
key_creator_in_grace, key_creator_left, user_banned, key_reaches_nothing, key_no_creator, org_not_active or none |
The key lacks read_sequences or has expired (no code), its creator has left the company, the holder is banned, the creator can open no hiring project, the key has no creator on record, or your company is not active. The activation preview is also a 403 for a creator who is not an Owner or an Admin. |
404 |
sequences_not_available, sequence_not_found |
Email sequences are not open for your company (sequences_not_available), or the sequence is not there for your company (sequence_not_found). |
429 |
api_key_rate_limited, sequence_reads_rate_limited, sequence_reads_busy |
A limit of section 5.12.6 or 8.5. |
503 |
sequence_reads_unavailable, sequence_reads_timed_out, results_busy, activation_preview_unavailable, key_access_changed, key_reach_unavailable, or none |
A check could not be made or a read took too long. Ask again, with a shorter range for results_busy. |
When more than one of these could apply to a call, this guide does not say which one you get: handle each.
What you can rely on.
- The four routes only read. A sequence cannot be created, changed, switched on or off, or sent through them.
people,started_assessment_within_7dandunsubscribedcount enrolments,people_emailedcounts candidacies andsent_per_stepcounts emails. None of them is a count of different people.- The figures are counted from what Flowmingo has recorded, and the route does not give replies, bounces, opens or clicks.
- The figures cover the projects the key's creator can open now (section 5.12.5).
5.13 Read what was recorded during a Skill Test or a Technical Test
GET /hiring/submissions/{id}/integrity/v1 returns, for one finished Skill Test or Technical Test, counts of what the candidate's browser, camera and microphone recorded during the sitting, and what was being watched while it did. It gives counts and coverage, never a verdict, a score or an accusation: Flowmingo does not judge whether anyone broke a rule. It only reads. The {id} is the result's id from section 5.2.
Required API key scopes: read_hiring, read_candidates and read_integrity. read_integrity is opt-in: a key does not have it unless it is chosen when the key is created or edited (section 2.3). The route also needs a key whose creator is currently an Owner or an Admin. A key that does not meet this is refused with a 403, and the response body says why. That is in addition to the refusals every route of the API gives (a missing or insufficient scope, a stopped or banned key, rate limits: see sections 8.4 and 11). The MCP server has no tool for this route.
Only a Skill Test and a Technical Test are read. A result of any other kind, an AI interview included, is normally refused with a 400 and the code assessment_type_not_supported, and nothing of it is read. For example, a result that is not yours, one the key's creator cannot open, a practice run and an id that does not exist answer the same 404 with the code submission_not_found. The answer is sent with Cache-Control: no-store.
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" \
"$BASE/hiring/submissions/59065d06-a013-44c2-97d2-88e9d6b3c858/integrity/v1"
{
"schema_version": 1,
"submission_id": "59065d06-a013-44c2-97d2-88e9d6b3c858",
"assessment_type": "skill_test",
"availability": "ready",
"reason": null,
"reading_guide": "These are observations recorded by the candidate's browser, camera or microphone during the sitting. Each has ordinary explanations, for example a notification, a second monitor, a shared computer, assistive technology or a poor connection. Flowmingo does not judge whether anyone broke a rule. A count of zero does not show that nothing happened: read `monitoring` for what was watched. Do not use these as the only basis for a decision about a person; a person should look at them together with the candidate's work and context.",
"monitoring": {
"observations_recorded": true,
"platform": "desktop",
"gaps": 1,
"detectors_reported": ["clipboard_activity", "face_presence", "focus_visibility"],
"capture_coverage": null,
"codes_not_watched": null,
"counts_may_be_incomplete": true,
"counts_may_include_setup": true,
"unread_records": false
},
"observations": {
"total": 3,
"by_group": { "window_and_tab": 2, "clipboard_and_capture": 1, "camera_and_microphone": 0, "screen_share": 0 },
"by_code": { "window_focus_lost": 2, "copy_attempted": 1 },
"facts": null
}
}
Every field is always present. The answer carries no recording, no image, no text the candidate typed, no time of day and no running order of what happened, and it says nothing about whether the sitting was acceptable.
| Field | What it is |
|---|---|
schema_version |
The version of the shape of this answer. |
submission_id |
The result id you asked for. |
assessment_type |
skill_test or technical_test. |
availability |
The whole answer in one word (the next table). |
reason |
Why availability is not_available; null otherwise. |
reading_guide |
The same paragraph in every answer: how these observations may and may not be read. Show it wherever you show them. |
monitoring |
What was being watched. null unless availability is ready. |
observations |
The counts. null unless availability is ready. |
availability |
What it says |
|---|---|
ready |
The answer is present. Read monitoring first. |
held |
Nothing else is returned (for example, this result's report is not released to your company yet). Ask again later. |
not_recorded |
Nothing is known about what was recorded for this sitting in a form this route reads, so there is nothing to give. This does not say that nothing happened. |
not_available |
This route gives no count for this sitting. reason says why: too_many_observations means the sitting holds so many observations that no count is given. |
monitoring: what was watched. Read it before you read a count: a count of zero from something that was not watched is not a finding.
| Field | What it is |
|---|---|
observations_recorded |
Whether at least one observation of this sitting was recorded in a form this route reads. true does not mean that any count is above zero. false means none was, which is not the same as nothing happening. |
platform |
Skill Tests only: the device the browser reported, desktop or mobile. null when the sitting does not say, or says more than one, and null for a Technical Test. |
gaps |
How many records that watching may have been interrupted were stored. A number above zero says that such a record exists, never that watching was interrupted, and it is not a number of interruptions. A zero does not show that watching was not interrupted. |
detectors_reported |
Skill Tests only: checks that reported at least once during the sitting. A listed check was not necessarily able to see, and a check that is not listed is not shown to have been watching. null for a Technical Test. |
capture_coverage |
Technical Tests only: which capture sources (face, voice, microphone, screen) were armed. null when the attempt cannot say, which is unknown and not "none". |
codes_not_watched |
Technical Tests only: observation names for which a zero does not show that nothing happened. |
counts_may_be_incomplete |
Always true: every count is a count of at least what was recorded, and a zero does not show that nothing happened. |
counts_may_include_setup |
Skill Tests only, always true: a count can include observations made before the candidate pressed Begin. null for a Technical Test. |
unread_records |
Skill Tests only: true when the sitting also has records from an earlier version of the recorder that this route does not read, so the counts may be lower than the dashboard shows; false when it has none. null for a Technical Test. |
observations: the counts.
| Field | What it is |
|---|---|
total |
The sum of by_group: the observations counted. Not weighted, and not a length of time. Gaps are not included. |
by_group |
The same count in four groups: window_and_tab, clipboard_and_capture, camera_and_microphone and screen_share. Every group is always present. |
by_code |
The count of each observation that was counted, by name. A name that is missing was not counted, which is not the same as "did not happen". Only the names below ever appear. |
facts |
Technical Tests only: paste_count (how many times text was pasted into the workspace), largest_paste_chars (the size of the largest paste, in characters; the pasted text itself is never returned) and blur_count (how many times the window lost focus). Each can be null. null for a Skill Test. |
| Group | Name | What was recorded |
|---|---|---|
window_and_tab |
window_focus_lost |
The window the sitting was in lost focus. |
window_and_tab |
page_hidden |
The page was hidden, for example by switching to another tab or minimising the window. |
window_and_tab |
fullscreen_exited |
The browser left full screen. |
clipboard_and_capture |
copy_attempted |
The copy command was used in the page. |
clipboard_and_capture |
cut_attempted |
The cut command was used in the page. |
clipboard_and_capture |
paste_attempted |
The paste command was used in the page. |
clipboard_and_capture |
select_all_attempted |
The select-all command was used in the page. |
clipboard_and_capture |
context_menu_opened |
The context menu (the right-click menu) was opened. |
clipboard_and_capture |
capture_chord_confirmed |
A key combination that takes a screenshot was pressed and completed. |
clipboard_and_capture |
capture_chord_probable |
A key combination was pressed and the window then lost focus; it could not be confirmed that a screenshot was taken. |
camera_and_microphone |
face_not_visible |
The camera could not see a face (a dark room or looking away also produces this). |
camera_and_microphone |
multiple_faces_visible |
The camera saw more than one face (a person passing behind the candidate also produces this). |
camera_and_microphone |
voice_detected |
The microphone check recorded a sound it took to be a voice. It is not a measure of speech: a television, a second person or a noisy room can produce it. It is a recorded occurrence, not a count of separate sounds and not a length of time. |
camera_and_microphone |
microphone_muted |
The microphone stopped being usable. |
screen_share |
screen_share_ended |
Screen sharing stopped. |
screen_share |
screen_share_surface_unexpected |
A window or a tab was shared where the whole screen was asked for. |
How to read the counts.
- Each observation has ordinary explanations, for example a notification, a second monitor, a shared computer, assistive technology or a poor connection. Do not use these as the only basis for a decision about a person: a person should look at them together with the candidate's work and context.
- A count of zero does not show that nothing happened, and a count from something that was not watched is not a finding. Read
monitoringfirst. - Read a number above zero in
gapsas "watching may have been interrupted", never as an interruption that happened and never as how many times. - The times the camera could not be read are not counted in this answer: a zero in the camera group does not show that the camera could see, and
gapsdoes not include them. - Do not compare counts of different kinds of assessment, or counts from a phone with counts from a computer. The answer does not say which exam-security setting (Standard or Strict) a Skill Test was taken under, so do not compare counts from different Skill Tests directly.
Keep them inside your hiring. Use these observations yourself, within your own hiring process, together with the candidate's work and context. Do not pass them to people outside it, and do not feed them into an automatic accept or reject rule. A copy you make is not removed when the candidate asks Flowmingo to erase their data.
6. Update your pipeline and reviews through the API
Seven routes change your pipeline and reviews. They all need the write_pipeline scope, and they all act as the person who created the key (section 2.9). Each route's section says what it checks. Notes, ratings and shortlist marks can also go on results of interview sets the creator made. The ids in these samples are placeholders; take real ones from the read routes in section 5 (projects gives project ids (section 5.7), applications gives application ids, stages gives stage ids, the results list gives result ids).
Undo. The note, rate, shortlist and collection routes (6.4 to 6.7) answer with an undo field: { "change_id": "...", "undo_until": "..." }, or null when the undo could not be recorded. POST /approvals/{change_id}/undo/v1 (section 9.7) takes the change back within 24 hours, and is refused when later work has changed the same thing.
6.1 Add a candidate to a hiring project
POST /hiring/applications/v1. The candidate must already be a candidate of your organization (see GET /hiring/candidates/v1); to bring in someone new, use the invite route. The application shows in the dashboard as added by your API key.
| Field | Type | Required | Notes |
|---|---|---|---|
tal_candidate_id |
UUID | Yes | The candidate to add. |
com_project_id |
UUID | Yes | The hiring project (section 5.7 shows how to find its id). |
com_job_post_id |
UUID | Optional | The job post, when the project has more than one. It must belong to the project. |
com_stage_id |
UUID | Optional | The stage to start in. It must belong to the project. Leave it out for the first stage. |
external_id |
string | Optional | Your own id for this application (the person in this job): plain text of 1 to 128 characters with no control characters, stored exactly as you send it, scoped to your company and not unique (section 8.8). It is set when the application is created and cannot be changed afterwards. null counts as not sent. |
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/applications/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"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",
"external_id": "APP-48213-0031"
}'
{
"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",
"external_id": "APP-48213-0031",
"status": 1
}
The answer is 201. A candidate who is already in that project is refused with 400 (Candidate has already applied to this project). A stage that is not a stage of the project is refused with 400. A candidate your organization does not hold answers 404; the candidate need not be in the creator's projects. For a key limited to some projects, a project the creator cannot open answers 403 key_out_of_reach before the project is looked up, a project of another organization included; for any other key an unknown project answers 404, unless a com_stage_id is also sent: a stage that does not belong to the project is checked first and answers 400. The call sends the application.created event. An external_id that is not valid is refused with 400 (section 8.8). The answer carries the id you sent as external_id, or null. It is not an idempotency key: a person who is already in the project is refused, and their application keeps the id it has. You can read the id again on GET /hiring/applications/v1, also as a filter (section 5.6).
6.2 Move an application to another stage
POST /hiring/applications/{id}/move-stage/v1, where {id} is the application id. The stage must belong to the application's own project.
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/applications/2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3/move-stage/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{ "com_stage_id": "5b8d1e92-3a7c-4c60-b2f4-91e6a0d7c358" }'
{
"id": "2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3",
"com_stage_id": "5b8d1e92-3a7c-4c60-b2f4-91e6a0d7c358"
}
The answer is 201. A stage of another project answers 400 (com_stage_id is not a valid stage of the target project); an application that is not yours answers 404 (Application not found), and one in a project the creator cannot open answers 403 key_out_of_reach. Moving into a stage sends the candidate an email only if your team switched that on for the stage. A stage that asks for a scorecard is not asked for one through the API. The call sends the candidate.stage_changed event.
6.3 Reject an application
POST /hiring/applications/{id}/reject/v1, with no body. Rejecting moves the application into its project's Rejected stage. Nothing is deleted: the candidate stays on the board and can be moved back.
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/applications/2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3/reject/v1 \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj'
{
"id": "2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3",
"status": "rejected",
"com_stage_id": "9e7a4d31-6b02-4f85-8c19-3a5d70e2b6c4"
}
The answer is 201, and com_stage_id is the Rejected stage. Rejecting an application that is already in a rejection stage changes nothing and answers the same way. A project with no Rejected stage answers 400; add one to the project or use move-stage. The stage's email goes out only if your team switched it on. The call sends candidate.stage_changed and then application.rejected.
6.4 Add a note to a result
POST /hiring/submissions/{id}/note/v1, where {id} is the result (submission) id.
| Field | Type | Required | Notes |
|---|---|---|---|
note |
string | Yes | 1 to 2000 characters. |
rating |
number | Optional | A rating to save with the note: 0.5 to 10, in steps of 0.5. It must be a JSON number. It archives that person's earlier ratings of this result, so it becomes their current rating, except a rating that came with an earlier note: that one is not archived and stays on its note (section 6.5). |
external_id |
string | Optional | Your own id for this note: plain text of 1 to 128 characters with no control characters, stored exactly as you send it, scoped to your company and not unique (section 8.8). It names this note, not the result. It is set when the note is made and cannot be changed afterwards. It is not an idempotency key. |
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/submissions/59065d06-a013-44c2-97d2-88e9d6b3c858/note/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{ "note": "Strong systems thinking; ask about on-call experience.", "rating": 8, "external_id": "note-8841-1" }'
{
"id": "eca87aad-100e-4fe3-848e-83d55b07c78f",
"note": "Strong systems thinking; ask about on-call experience.",
"rating": 8,
"created_at": "2026-10-20T09:41:12.000Z",
"external_id": "note-8841-1",
"undo": { "change_id": "d2c4e6a8-1b3d-4f5a-9c7e-0a2b4d6f8e1c", "undo_until": "2026-10-21T09:41:12.000Z" }
}
The answer is 201. The note appears on the result in the dashboard, under the name of the person who created the key. The call sends the submission.review.update event, which never carries the text of the note. external_id in the answer is the id you sent, or null. It names this note, not the result: a rating sent with the note is stored on the note and shares its id, and a later call to the rate route does not archive it, so the note keeps its rating and its id. Sending the same call again makes another note (section 8.6). None of the read routes of section 5 returns a note, so the answer is where you get the id back.
6.5 Rate a result
POST /hiring/submissions/{id}/rate/v1 with { "rating": 8 } and, if you want one, an external_id (section 8.8). rating is required: a JSON number from 0 to 10 in steps of 0.5. Each call makes a new rating and archives the earlier ratings of this result that the person who created the key gave, so the new one is their current rating. A rating that was sent together with a note (section 6.4) belongs to that note: this call does not archive it. Text such as "" or "8" is refused with 400 (rating must be a number (a JSON number, not text)) instead of being read as a rating.
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/submissions/59065d06-a013-44c2-97d2-88e9d6b3c858/rate/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{ "rating": 8 }'
{
"id": "7241c51b-23a7-4072-b345-5484cd0b54af",
"rating": 8,
"rated_at": "2026-10-20T09:42:03.000Z",
"external_id": null,
"undo": { "change_id": "e5a7c9b1-2d4f-4a6b-8c0e-1b3d5f7a9c2e", "undo_until": "2026-10-21T09:42:03.000Z" }
}
The answer is 201. The call sends the submission.review.update event. external_id in the answer is the id you sent with this call, or null (as here, because the request sent none). It names this rating, not the result: the next rating archives this one, and the archived rating keeps its own id. Sending the same call again makes another rating and archives this one, so your current rating ends up the same. None of the read routes of section 5 returns a rating, so the answer is where you get the id back.
6.6 Shortlist or un-shortlist a result
POST /hiring/submissions/{id}/shortlist/v1 with { "shortlisted": true } to shortlist, or { "shortlisted": false } to remove the mark. Either takes an optional external_id (section 8.8). shortlisted is required and must be a JSON boolean: the text "false" is refused with 400 (shortlisted must be true or false (a JSON boolean, not text)).
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/submissions/59065d06-a013-44c2-97d2-88e9d6b3c858/shortlist/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{ "shortlisted": true }'
{
"id": "b2cf0e3f-215b-483d-97f2-4078f18bf619",
"shortlisted": true,
"external_id": null,
"undo": { "change_id": "f6b8d0c2-3e5a-4b7c-9d1f-2c4e6a8b0d3f", "undo_until": "2026-10-21T09:43:10.000Z" }
}
The answer is 201. The call sends the submission.review.update event. external_id in the answer is the id you sent with this call, or null (as here, because the request sent none). It names this call, not the result's current state: each call makes a new shortlist record and archives the earlier one, and the archived record keeps its own id. None of the read routes of section 5 returns a shortlist mark, so the answer is where you get the id back.
6.7 Add a candidate to your talent collection
POST /hiring/candidates/{id}/collection/v1, where {id} is the candidate id. Both fields are optional.
| Field | Type | Notes |
|---|---|---|
tags |
string[] | Tags for the candidate, each up to 100 characters. |
note |
string | A note kept with the candidate, up to 2000 characters. |
curl -X POST https://apis.flowmingo.ai/company/integration/hiring/candidates/7c9e6679-7425-40de-944b-e07fc1f90ae7/collection/v1 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-d '{ "tags": ["backend", "referral"], "note": "Met at the October job fair." }'
{
"id": "6db5f037-4201-413e-8d4b-5c1366c79110",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"tags": ["backend", "referral"],
"undo": { "change_id": "a7c9e1d3-4f6b-4c8d-8e2a-3d5f7b9c1e4a", "undo_until": "2026-10-21T09:44:21.000Z" }
}
The answer is 201. If the candidate is already in your collection, the tags you send replace the stored tags and a note you send replaces the stored note (what you leave out is kept), so calling it again is safe. A candidate you have no relationship with answers 404 (Candidate not found); one the creator cannot reach answers 403 key_out_of_reach.
6.8 Errors on these routes
| Status | When |
|---|---|
400 |
An id in the path is not a UUID, or the body is invalid (a missing or over-long note, a rating that is not a number in steps of 0.5 or is outside its range (0 to 10 when you rate a result, 0.5 to 10 on a note), a boolean or number sent as text, an external_id that is not plain text of 1 to 128 characters with no control characters, a stage that does not belong to the project, a project with no Rejected stage). A bad external_id has a message and no code. |
403 |
The key lacks the write_pipeline scope (API key missing required scope: write_pipeline), or the request names a record the creator cannot reach (code key_out_of_reach, section 2.9). |
404 |
The application, result or candidate is not one of your organization's: Application not found, Submission not found or Candidate not found. |
7. Poll the events feed
GET /events/v1 returns the events that webhooks deliver, for integrations that cannot host an endpoint, and for catching up after your endpoint was down. It needs the read_hiring scope, and a key whose creator has access to all hiring projects (see the end of this section). You do not need a webhook endpoint to use it.
| Query param | Type | Notes |
|---|---|---|
limit |
integer | Events per page, 1 to 200. Default 50. A value outside the range is adjusted, not refused. |
cursor |
string | The next_cursor of the previous page. Treat it as opaque and pass only a value the API gave you. A cursor that is not one of ours is not reliably handled: one that cannot be read at all is ignored (you get the first page, the newest events, again), but a damaged one that still looks like a cursor can be refused with an error. Do not rely on either outcome: a loop must stop on an error rather than assume it restarted. Leave cursor out to start again from the newest events. |
event_type |
string | Only events of this exact type, for example assessment.completed, interview.evaluation.update or application.created. The types are listed in the webhook guide. |
curl -s -H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
'https://apis.flowmingo.ai/company/integration/events/v1?event_type=assessment.completed&limit=50'
{
"data": [
{
"event_id": "cca11cbd-00fd-4e16-ba60-75625ee9d196",
"event_type": "assessment.completed",
"sub_event": "skill_test",
"timestamp": "2026-10-20T09:15:30.123Z",
"data": {
"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
}
}
],
"next_cursor": null
}
next_cursor is null here, so there is no later page. When more events exist, next_cursor is a long piece of text for you to pass back as cursor. Keep following it until it is null: a page can hold fewer events than limit, even none, and still have a next page.
Each item has the fields below. The data object is the payload a webhook delivery carries for the same event; the envelope differs from a webhook delivery's (there is no schema_version or organization_id, and sub_event is added). For a technical test, data.score and data.score_percent are null, exactly as in a webhook delivery: the event says the attempt ended, and the score is read from the results routes (section 5.5). The feed and a redelivery return the payload as it was stored, so a technical test event created before 20 Oct 2026 can still carry a score: ignore the score of every technical test event and read the result from the results routes.
| Field | Meaning |
|---|---|
event_id |
The id of the event. It is the same event_id a webhook delivery of this event carries, so you can dedupe across both. |
event_type |
The kind of event. |
sub_event |
A finer name inside the event type, such as the assessment type of an assessment.completed event, or null. |
timestamp |
When the event was recorded. |
data |
The payload of the event. |
How the feed behaves:
- Newest first, one page at a time. Pass
next_cursorof one answer ascursorof the next to read older events, and stop whennext_cursorisnull. There is no "since" parameter: to read only what is new, remember theevent_idvalues you have handled and stop paging when you reach one. - Events are kept for about 30 days. Older events are removed by a daily clean-up, so some may still be returned until it has run, and you should not rely on them. Test events (from Send Test Event in the dashboard) are not in the feed.
- Candidate details are removed unless the key has
read_candidates. Without it,candidate_emailandsource_detailare taken out ofdata. An event about a candidate who was invited through Matched Talent and has not started yet names neither the email address nor the candidate id, whatever the key can read. A webhook delivery goes to the endpoint your own admin configured and is not trimmed this way. - Poll regularly and dedupe. Keep the
event_idvalues you have handled, so reading the same event twice never processes it twice. - The feed covers the whole company. An event does not reliably say which project it belongs to, so the feed cannot be narrowed to a person's projects. A key created by someone limited to some projects is refused with
403and the codekey_events_need_all_projects: use a key whose creator has access to all hiring projects (an Owner always does). A creator who was given the projects one by one is limited to a list, even if it names every project today, so the feed refuses that key too. - Approval decisions are read by every key that can read the feed. An
approval.status.updateevent (see the webhook guide) describes a decision on a change that one of your API keys or your AI assistant asked for. Every key of your company that can read the feed sees it, not only the key that asked. It carries the change's id, what was asked for, the decision and its times, and no names. - The score of a recorded practice sitting never appears. Neither this feed, nor your webhook deliveries, nor the Recent Deliveries of an endpoint in Settings -> Integrations -> Webhooks carries one. What Flowmingo raises for each kind of sitting that it has recorded as practice or as a try-out:
- A candidate's practice attempt: no
initiated,startedorcompletedstatus event, nointerview.evaluation.updateand noassessment.completed. - A try-out that someone on your team runs: no
startedorcompletedstatus event, nointerview.evaluation.updateand noassessment.completed. A preview of a skill test or a technical test (Preview this yourself) sends noassessment.completedeither. - One exception, with no score in it: an
initiatedevent is sent when an invitation's link is opened. A try-out opened with an email that also holds an invitation to that interview can therefore still raise one. It carries no score and no submission id. Aninitiatedevent for a practice open that was recorded before Flowmingo stopped raising them cannot be told apart from a real one; it carries no score and no submission id either, and it leaves the feed with the rest of its history.
- A candidate's practice attempt: no
A polling loop in Python that reads what is new once a minute:
import os
import time
import requests
BASE = "https://apis.flowmingo.ai/company/integration"
HEADERS = {"X-API-Key": os.environ["FLOWMINGO_API_KEY"]}
seen = set() # keep these ids in a database in production
def handle(event):
print(event["event_type"], event["event_id"])
def new_events():
cursor, fresh = None, []
while True:
params = {"limit": 200}
if cursor:
params["cursor"] = cursor
page = requests.get(f"{BASE}/events/v1", headers=HEADERS, params=params, timeout=30)
page.raise_for_status()
body = page.json()
batch = [e for e in body["data"] if e["event_id"] not in seen]
fresh.extend(batch)
# Newest first: a page that held an event we already handled means we reached last run's position.
if len(batch) < len(body["data"]) or not body["next_cursor"]:
return list(reversed(fresh)) # oldest first
cursor = body["next_cursor"]
while True:
for event in new_events():
handle(event)
seen.add(event["event_id"])
time.sleep(60)
On its first run this loop reads back through every event the feed still holds (about 30 days). If you only want events from now on, fill seen from one pass first and skip handle for it.
8. How the API behaves: ids, pages, errors, limits and retries
8.1 Ids and formats
- Bodies and answers are JSON. Send
Content-Type: application/jsonwith a body. Use the exact field names documented for each request and response. Business data usessnake_case; error responses usestatusCode(section 8.4). - Keep ids exactly as the API returns them and follow each field's documented type. Assessment, project, job post, candidate, application, result and invitation ids are UUIDs, and so is
event_id, which is not always a version-4 UUID.organization_idand the configuration ids (cfg_importance_id,cfg_lingual_ids) are whole numbers. The invite route accepts only version-4 ids for assessments, projects and job posts (section 4.3), which is what Flowmingo returns for them; every other route accepts a UUID of any version, and an id in a path that is not a UUID answers400instead of reaching the database. - Some ids are yours, not ours.
external_id(andapplication_external_idandinvitation_external_idon an invite),ats_candidate_id,client_refandidempotency_keyare text you choose. None of them is a UUID or is checked as one, and each has its own rule:external_idand its two variants are plain text of 1 to 128 characters that is stored exactly as sent (section 8.8),ats_candidate_idis up to 250 characters (section 4.3),client_refis 1 to 128 letters, digits and. _ : -(section 4.7.3), and anidempotency_keyis 8 to 128 characters fromA-Z a-z 0-9 . _ : -(sections 3.5.3, 3.6.2 and 8.6). They do not replace each other: anexternal_idis never a retry key. - Times are ISO 8601 strings in UTC, for example
2026-10-20T09:15:28.000Z. - A boolean or a number that you send must be a real JSON boolean or number. Text such as
"false"or"30"is refused with400on the routes that read these fields (invite, create-set, rate, note and shortlist).
8.2 Pagination
Every list route (interview-sets, job-posts, projects, applications, interviews/upcoming, submissions and candidates) takes page (starting at 1, at most 100000) and limit (default 20, at most 100) and answers:
{ "data": [], "total": 0, "page": 1, "limit": 20 }
total counts every matching row, not just the page. To read everything, ask for page=1, page=2 and so on until page * limit reaches total. Send positive whole numbers within those bounds: a page or limit that is zero, negative, above its maximum or wholly non-numeric answers 400. Some other malformed values, such as 1.5 or 5abc, are cut to the whole number they start with (1 and 5), so do not rely on that. The stage list answers { "data": [...] } with no paging. The events feed pages with a cursor instead (section 7).
8.3 Filters
Use only the filters this guide lists for a route. What a route does with any other query parameter is not defined, and it is not refused, so a misspelt filter can return the unfiltered list. For example GET /hiring/candidates/v1?mail=jane@example.com (a misspelt email) returns the ordinary list of your newest candidates, not a lookup of Jane (she may happen to be on that page): the exact lookup is spelled email (section 5.6). The applications list has no documented tal_candidate_id filter. Use GET /hiring/submissions/v1?tal_candidate_id=... to read the results for a candidate id. Check that the rows you got back are the ones you asked for before you act on them, above all before you move, reject or rate someone.
One filter is checked strictly: external_id. It is an exact, case-sensitive match on your own id, with no wildcards and no trimming, so req-1 does not find REQ-1. A value that is empty, only spaces, longer than 128 characters, holds a control character, is sent twice (?external_id=a&external_id=b) or is written with brackets (?external_id[]=a or ?external_id[x]=a, also when the brackets are percent-encoded) is refused with 400 and never means "no filter". Section 5.9 lists the routes that take it. On the results list it is refused whatever its value and however it is spelled (section 5.2). A query string with more than 1,000 parameters (counted as the pieces between & signs; a trailing & is not counted) is refused with 400 on every list that takes the filter and on the results list, whether or not external_id is among them. The server reads only the first 1,000 parameters, so it could not check that the filter was sent once and without brackets, and refuses instead of returning a list that may be unfiltered. The message is "Too many query parameters: only the first 1000 are read, so the external_id filter could not be checked and nothing was filtered. Send fewer parameters, once each." A query of exactly 1,000 parameters is read in full. An id is not unique, so a lookup can return several records, paged like any list: read total, and tell two matches apart by their Flowmingo id. Every other query parameter that a route does not list is still ignored, as above, so check that the rows you got back carry the external_id you asked for before you act on them.
Dates are ISO 8601. A date without a time is read as the start of that day in UTC. The results list's completed_to treats a bare date as covering that whole day; the applications list's date_to does not (section 5.6).
8.4 Errors
Every error answers with a JSON body of the same shape:
{
"statusCode": 404,
"message": "Interview set 3f2504e0-4f89-41d3-9a0c-0305e82c3301 not found",
"timestamp": "2026-10-20T08:15:00.000Z",
"path": "/integration/interview/candidate/invite/v1"
}
path is the route you called. The query string is not repeated in it.
A few errors also carry a code, a short stable token your code can branch on:
code |
Status | Meaning |
|---|---|---|
assessment_type_not_invitable_via_api |
400 | The invite request names a Recording evaluation, Questionnaire or question bank. Nothing was created (section 4.2). |
assessment_test_requires_single_set |
400 | A Skill Test or Technical Test was named together with another assessment, or two tests were named. Nothing was created (section 4.6.1). |
assessment_test_requires_single_project |
400 | A Skill Test or Technical Test request named more than one hiring project (section 4.6.1). |
project_required, project_ambiguous |
400 | The test is attached to no active hiring project, or to several and the request did not say which (section 4.6.1). |
assessment_test_field_not_supported |
400 | A Skill Test or Technical Test request carried a value in ats_candidate_id or metadata, or a practice_mode other than null (section 4.6.1). |
assessment_test_email_required |
400 | A candidate in a Skill Test or Technical Test request has no email and carries none of the three ids (a candidate with an id and no email gets external_id_requires_email instead; section 4.6.1). |
assessment_test_too_many_candidates |
400 | More than 100 candidates in a Skill Test or Technical Test request (section 4.6.1). |
too_many_candidates |
400 | An invite request other than a Skill Test, Technical Test or Showcase one names more than 20,000 candidates. Nothing is created. The message says the limit and how many you named. Split the call into several (sections 4.3 and 8.5). A Skill Test, Technical Test or Showcase request has the lower limit of the row above instead. |
candidate_field_too_long, candidate_field_invalid |
400 | A value in a candidate row of the invite route does not fit what Flowmingo stores: it is too long, is not text, holds a NUL character, or holds half of a surrogate pair (in an ats_candidate_id or an email, and in the name, source and thumb_file_link of a candidate sent with an ats_candidate_id). The same codes answer the partner order routes for an address, a candidate id, an application id or a report id, and candidate_field_invalid for a NUL character in the Zoho Recruit callback_url. Nothing was created (sections 4.3.2 and 9.6). |
attachment_not_yours |
400 | attachment_sys_file_ids on the invite route names a file that is not available to you: the person the key acts for did not upload it, and neither a template that your organization shares nor a private template of that same person carries it. Nothing was created (section 4.3). |
invitation_note_empty, invitation_note_too_long, invitation_note_too_large |
400 | The invitation_message of the invite route has no text left once unsupported content is removed, has more than 2,500 characters of text, or is over 5,000 characters once unsupported content is removed, formatting and link addresses included. A note over 50,000 characters as sent gets a plain 400 with no code. Nothing was created (section 4.3). |
external_id_invalid |
400 | An external_id, application_external_id or invitation_external_id on the invite route is not plain text of 1 to 128 characters with no control characters (an empty or whitespace-only value, a number, a list and an object are refused too). Nothing was created (sections 4.3.1 and 8.8). The other routes that take an external_id, a Zoho Recruit order included, refuse a bad one with a 400 and the same sentence in message, with this code or with none: branch on the status there. |
external_id_requires_email |
400 | A candidate with no email carries one of the three ids on the invite route, a Skill Test or Technical Test request included. Nothing was created (sections 4.3.1 and 4.6.1). |
application_external_id_requires_project, application_external_id_ambiguous_project |
400 | application_external_id is sent and the invite request names no hiring project, or more than one (the list and the deprecated single field together count every distinct id). Nothing was created (section 4.3.1). |
invitation_external_id_requires_invite, invitation_external_id_ambiguous_assessment |
400 | invitation_external_id is sent and the invite request names no assessment or has send_invite false, or names more than one assessment (counted over the list and the deprecated single field together). A Skill Test or Technical Test request meets only the send_invite false case. Nothing was created (sections 4.3.1 and 4.6.1). |
external_id_not_supported_when_staging |
400 | A candidate row of POST /approvals/invite/v1 carries an external_id, application_external_id or invitation_external_id that is not null. A staged invitation keeps only each recipient's name and email, so nothing was staged. Send the invitation to the invite route to store your ids, or leave them out (sections 8.8 and 9.7). |
api_key_has_no_acting_user |
400 | The key has no user and no creator to send a test, create or edit a project, or upload a recording as (sections 4.6.2, 3.5, 3.5.14 and 4.7.1). A key with no creator on record is normally refused first, with a 403 (section 11). |
project_field_not_supported |
400 | A create-project body carried a dashboard-only setting such as file_link or attributes. Nothing was created (section 3.5.1). An edit-project body carried a field that route does not take, such as external_id, status or expected_updated_at. Nothing was changed (section 3.5.14). |
nothing_to_update |
400 | An edit-project body carried none of title, position, description, job_post.title and job_post.description (an empty job_post object {} counts as none; for a key whose creator has left the company, during the grace period, it is a 403 key_out_of_reach instead). Nothing was changed (section 3.5.14). |
job_post_pay_range_invalid |
400 | An edit-project call would change the words of a job post whose saved pay range has a minimum above its maximum. Correct the pay in the dashboard first. Nothing was changed (section 3.5.14). |
stage_name_unknown, stage_name_duplicated, stage_field_not_supported |
400 | In stages of a create-project call: a name that is not one of the four default stage names (the message lists them), the same stage named twice, or a key other than name and external_id. Nothing was created (section 3.5). |
project_create_requires_admin |
403 | The person who created the key is not an Owner or Admin of the organization, so the key cannot create a project (section 3.5), edit one (section 3.5.14) or ask for one to be deleted (section 9.7.2). |
idempotency_key_reused |
409 | The idempotency_key of a create-project, create-assessment or create-set call was already used with a different body (an external_id is part of the body), or, for a create-set call, the repeat comes from an API key whose creator cannot open the assessment the first call made. Nothing was created (sections 3.5.3, 3.6.2 and 8.6). |
idempotency_request_in_progress |
409 | A create-project call with this key is still building the job post, or a create-set call with this key is still being worked on (its answer then carries a Retry-After header). Wait a few seconds and send the same request again (sections 3.5.3 and 8.6). |
interview_set_was_deleted |
409 | The assessment that this create-set idempotency_key made has been deleted since. It is not made again under the same key: use a new key (section 8.6). |
idempotency_key_not_supported_with_addons |
400 | A create-set request has both an idempotency_key and addons. Nothing was created: send it without one of them (section 8.6). |
question_mode_required |
400 | A create-set request for an AI interview names no question_mode and sends no interview question. Send realtime or predefined, with at least one interview question for predefined. Nothing was created (section 3.2). |
interview_questions_required |
400 | A create-set request for a predefined AI interview has no iai_questions entry whose question_mode is interview (pre-screening entries do not count). Nothing was created (section 3.2). |
question_generation_failed |
503 | A create-set request with question_mode realtime and no interview question of your own: Flowmingo could not prepare the questions just now. Nothing was created. Send the same request again, or send predefined with your own questions (section 3.2). |
job_post_generation_failed |
503 | A create-project call: the AI could not draft the job post just now, so no project was created. Send the same request again with the same idempotency_key after the Retry-After wait (sections 3.5.2 and 3.5.10). |
api_key_rate_limited |
429 | The key made more than 600 requests in the current UTC minute. Nothing was done. Wait the seconds in the message and the Retry-After header, then send the same request again (section 8.5). |
project_create_limit_reached |
429 | 10 projects a minute or 300 a day reached for the organization. Nothing was created (section 3.5.4). |
api_org_rate_limited, api_org_daily_limit_reached, api_key_daily_limit_reached |
429 | Since 5 November 2026 (section 8.5): your organization made more than 1,500 API calls in the current UTC minute, or more than 30,000 today (counted across all your keys), or this one key made more than 20,000 calls today. Nothing was processed. Wait as long as the Retry-After header says, in seconds (for a daily limit, until 00:00 UTC; the message gives the same wait in words), then send the same request again (section 8.5). |
api_key_hourly_invite_limit_reached, api_key_daily_invite_limit_reached |
429 | Since 5 November 2026 (section 8.5): this call would take the key past 300 invitations in the current UTC hour or 1,000 today (the invitations it already made plus the ones this call would make), or this one call alone would make more than 300. The whole call is refused and nothing was sent. Wait as long as the Retry-After header says, then send the same call again; if this one call alone would make more than 300 invitations, waiting never helps: split it into calls of 300 invitations or fewer, spread over several hours (section 8.5). |
api_key_limit_reached |
409 | Your organization already has 50 active API keys. Nothing was created. Turn off or delete a key you no longer use (section 2.2). |
project_create_limit_unavailable, project_create_role_unconfirmed |
503 | The project limit, or the role of the key creator, could not be checked just now. Nothing was created. Try again in a moment (section 3.5). |
job_post_name_unknown, job_post_location_ambiguous, job_post_value_invalid, job_post_field_not_supported |
400 | A value in the job_post block of a create-project call cannot be used. The message explains the problem; for an unknown name it lists the accepted names (or the closest ones). Nothing was created (section 3.5.6). On an edit-project call, job_post_field_not_supported means that job_post carries a key other than title and description. Nothing was changed (section 3.5.14). |
job_post_catalogue_unavailable |
503 | The names in job_post could not be checked just now. Nothing was created. Send the same request again after the Retry-After wait (section 3.5.6). |
project_has_no_job_post |
409 | An edit-project body carries job post words (job_post.title or job_post.description) and the project has no job post to edit. Nothing was changed (section 3.5.14). |
job_post_is_off |
409 | An edit-project call would change the words of a job post that is switched off or has expired. Nothing was written, and the project fields of the same body were not written either. Switch the job post on first (section 3.7.2) and then edit it, or edit it in the dashboard (section 3.5.14). |
job_post_not_saved |
500 | On an edit-project call the dashboard's save refused the job post's words before writing anything. The message says which project fields of the same call were saved, if any. Read the project, then send the same body again if no one has changed the same fields in the meantime (section 3.5.14). |
job_post_outcome_unknown |
503 | An edit-project call could not tell whether the job post's words were saved. The message says which project fields were saved. Read the project and compare job_post.sent_title and job_post.sent_description (of the project's newest job post) with what you sent, after removing leading and trailing blanks from it, then send the same body again if it is still needed and no one has changed the same fields in the meantime. When the call was changing the description, the message also says that the title-only-draft marker may still be set if the description was saved, and that sending the same description again will not clear it (section 3.5.14). |
project_outcome_unknown |
503 | An edit-project call could not tell whether the project fields were saved: the write ended in an error that does not say whether it went through. The job post was not attempted. Read the project, then send the same body again if it is still needed and no one has changed the same fields in the meantime (section 3.5.14). |
edit_saved_answer_unavailable |
503 | An edit-project call was saved but its answer could not be built. The message names what was written, and says so when the title-only-draft marker could not be cleared. Read the project to confirm (reading needs read_hiring); job_post.sent_title and job_post.sent_description are the words as stored, after leading and trailing blanks were removed from what you sent (section 3.5.14). |
job_post_edit_budget_reached |
429 | The organization has used its 50 AI-checked edits of job post words for today (UTC). Nothing was changed. Wait as long as the Retry-After header says, in seconds (until 00:00 UTC), then send the same request again (sections 3.5.14 and 8.5). |
job_post_edit_budget_unavailable |
503 | The daily allowance for edits of job post words could not be read just now. Nothing was changed. Try again in a moment (section 3.5.14). |
job_post_not_found |
404 | No such job post in your organization (section 3.7). |
project_not_open |
409 | The hiring project of the job post is a draft, archived or deleted, so the post cannot be published or boosted. Open the project first (sections 3.7.2 and 3.7.5). |
job_post_edited_while_off, job_post_changed, job_post_not_active, company_profile_required |
409 | A job post cannot be switched on or boosted as things stand: it was edited while it was off and has no check on its new wording, it changed while it was being checked, it is off, or the company profile has no logo or description. The code says which. Nothing was changed (sections 3.7.2 and 3.7.4). job_post_changed also answers an edit-project call whose job post was edited, or switched on or off, while the call was saving it: its words were not changed, and the message says which project fields were saved (section 3.5.14). |
publish_review_rejected, boost_review_rejected |
422 | The AI content check refused the job post, with a rejection_reason. A refused boost is not applied, and the post stays as it was. A refused publish is not switched on, or Flowmingo tries to switch the post back off, and the message says if that failed too (section 3.7). |
publish_review_limit_reached, boost_limit_reached |
429 | The 50 publish checks or the 50 boost raises per UTC day for the organization are used. Try again after 00:00 UTC (sections 3.7.2 and 3.7.4). |
job_post_review_unavailable, publish_review_limit_unavailable, boost_limit_unavailable, company_profile_unavailable |
503 | The AI check, a daily counter or the company profile could not be used just now. Nothing was boosted. A publish was not switched on, or Flowmingo tried to switch it back off, and if that failed too the message says the post may still be live. Try again in a moment (section 3.7). |
job_post_description_empty |
400 | A boost was asked for a job post with no description to check (section 3.7.4). |
assessment_field_not_supported, language_not_supported |
400 | A create-assessment body carried a dashboard-only setting, or a language that is not supported. Nothing was created (section 3.6.4). |
assessment_create_requires_admin |
403 | The person who created the key, or the person the assessment request runs as, is not an Owner or Admin of the organization, so the key cannot create an assessment. The person the request runs as is the user the key is bound to, when it has one, and otherwise the person who created the key. Both are checked and the code does not say which failed (section 3.6.1). |
assessment_creator_not_authorised |
403 | An assessment request that was accepted was stopped because the person it runs as is no longer an Owner or Admin. That person is the user the key is bound to, when it has one, and otherwise the person who created the key; the two can be different people. Create the key again from an Owner or Admin account (section 3.6.12). |
assessment_compose_invalid_input |
none: it is inside failure |
Not an HTTP error. A create-project call that makes assessments answers 201 (200 for a repeat, 202 while any assessment is still pending), and this code is in the failure of the assessment whose kind does not fit the project's position or description; the project is created all the same. Make that assessment with its own route and a job title that fits (sections 3.5.5 and 3.6.12). |
project_not_found |
404 | The project_id of a create-assessment call is not an active hiring project of yours that the key can use. Nothing was created (section 3.6.4). A key whose creator is limited to some projects gets a 403 instead for a project outside theirs, before this lookup (section 3.6.4). The same code answers POST /hiring/projects/{id}/status/v1 for a project that is not in your organization (section 3.7.5), and GET /hiring/projects/{id}/v1 and POST /hiring/projects/{id}/update/v1 for an id that is not a live project of yours (the read gives the same answer for a project the key's creator cannot open: sections 3.5.13 and 3.5.14). |
assessment_request_not_found |
404 | No create-assessment request with this idempotency_key exists in your organization (section 3.6.3). |
project_request_not_found |
404 | No project create request with this idempotency_key was found that this key can read, or the key is not in the allowed shape. It can also mean that the project was not saved yet (section 5.7). |
assessment_create_limit_reached |
429 | The generation budget of 60 units a minute or 1,500 a day is spent. Nothing was started (section 3.6.11). |
assessment_capacity_busy |
429 | Generation is busy. Nothing was started. Send the same request again shortly (section 3.6.11). |
assessment_create_limit_unavailable, assessment_create_role_unconfirmed, assessment_languages_unavailable |
503 | The generation budget, the role of the key creator or the list of interview languages could not be checked just now. Nothing was started. Try again in a moment (section 3.6.12). |
assessment_generation_failed, assessment_project_link_failed, technical_test_not_publishable |
502 | An assessment that was accepted could not be finished. The code says whether the same request can finish it (section 3.6.12). |
assessment_not_ready |
500 | An assessment was made but did not pass its final check. Send the same request again to finish it (section 3.6.12). |
assessment_was_deleted, project_already_has_cv_evaluation |
409 | The assessment was deleted since, or the project already has a CV Evaluation (section 3.6.12). |
assessment.length_exceeds_plan, interview_language_not_supported, technical_test_language_not_supported, showcase_tasks_invalid, role_play_scene_invalid, cv_evaluation_criteria_missing |
400 | A value of a create-assessment request cannot be used (section 3.6.12 has each). |
skill_test.variants.published_required, technical_test.published_required |
400 | The test has no published version. Publish it in the dashboard (section 4.6.1). |
skill_test_invite.placeholder_recipient_not_allowed, technical_test_invite.placeholder_recipient_not_allowed |
400 | An address that ends in @placeholder.local, or a preview address, cannot be invited to a test. |
technical_test_invite.duplicate_recipient |
400 | Two requests for the same address reached the same Technical Test at the same moment and the invitation could not be created. This call emailed nobody, and invitations created earlier in the same request remain. Repeat the call after a moment: invitations that already exist are found again (section 4.6.5). |
skill_test_invite.already_invited_to_test |
409 | The candidate already holds an invitation to this Skill Test through another hiring project. The call sent no email, but it is not undone: candidates and pipeline entries for the request were created, and invitations created for the rows before the conflict remain, not emailed by this call. The same request answers 409 again until you remove that candidate from it (section 4.6.5). |
assessment_test_invite_limit_reached |
429 | The daily limit of 1000 invitations to one test or 5000 for your organization is reached. Nothing was sent. Try again after 00:00 UTC (section 4.6.6). |
assessment_test_invite_limit_unavailable |
503 | The daily limit could not be checked just now. Nothing was sent. Try again in a moment (section 4.6.6). |
showcase.invite.set_not_published, showcase.invite.set_archived |
400 | The Showcase named in an invite request is not switched on, or is archived. Nothing was created (section 4.8.1). |
showcase.invite.placeholder_recipient_not_allowed |
400 | An address that ends in @placeholder.local, or a preview address, cannot be invited to a Showcase (section 4.8.1). |
send_at_invalid, send_at_requires_send_invite, send_at_not_supported_for_assessment, send_at_requires_assessment, send_at_requires_email |
400 | send_at breaks one of the rules of section 4.9. Nothing was created. |
send_day_full |
400 | The day a scheduled invitation is due would go over a limit: 1,000 invitations for the assessment or 5,000 for your organization. The count can also include invitations made without a schedule on or after that day, and two requests at the same moment can both pass the check. Nothing was created. Choose another day (section 4.9). |
invitation_not_found |
404 | No invitation with this id in your organization. It can also mean that a key whose creator is limited to some projects cannot tell which project the invitation belongs to (section 4.9.1). On the stop-reminders and resume-reminders routes it is the one answer for an unknown id, another company's invitation, an invitation you cancelled through the API and an invitation outside the key creator's projects, and, for a key whose creator is limited to some projects, an invitation made without naming a project by another key, and, for a key whose creator has left the company (during the grace period, section 2.10), every invitation (section 4.10). A repeat of a pause that worked answers it too once the invitation was cancelled through the API (a cancel is final). |
invitation_not_scheduled |
409 | The invitation cannot be cancelled: it has no due time in the future, it was already sent or is being sent right now, it was started, or it was already cancelled. Nothing was changed (section 4.9.1). |
reminders_not_stoppable, reminders_managed_by_sequence, reminders_managed_by_bundle, reminders_cancelled, reminders_busy |
409 | The stop-reminders or resume-reminders route cannot change the reminders of this invitation: it is not to an AI interview or a role-play (reminders_not_stoppable), or it is sent only by an email sequence set up in the dashboard (reminders_managed_by_sequence, pause call only: pause that person on the sequence there), or it is one assessment of at least one multi-assessment bundle that may still be sending its own reminders (reminders_managed_by_bundle: not expired, not finished, within 10 days of its email, fewer than two reminders sent; one such bundle is enough), or its invitation email was cancelled in the dashboard (reminders_cancelled, resume call only: a cancel is final), or it was being sent or changed at that moment (reminders_busy: try again). A repeat of a pause that worked answers reminders_managed_by_bundle too when the invitation has joined such a bundle since. A live bundle is checked before the kind of assessment and the sequence. Nothing was changed (section 4.10). |
consent_required, invalid_request, batch_empty, batch_too_large, set_not_recording |
400 | A recording request is refused as a whole: consent_confirmed is not true, a list or an id is invalid, a list is empty or longer than 50, or the assessment is not a recording evaluation. Nothing was created (section 4.7.4). |
recording_requires_scale |
403 | The organization is below the Scale plan, so a recording route, or making a recording evaluation, is refused. Nothing was made (section 4.7). |
set_not_found |
404 | No assessment with this com_interview_set_id exists in your organization. Nothing was created (section 4.7.4). |
recording_api_disabled, upload_service_unavailable, recording_plan_check_failed, idempotency_store_unavailable |
503 | A recording route cannot do its work just now. Nothing was made for the entries it had not reached. Send the same request again in a moment (section 4.7.4). |
invalid_external_id, duplicate_not_accessible, duplicate_read_failed |
201, inside one entry | An entry of a recordings call is rejected with one of these codes while the call itself succeeded. invalid_external_id: its external_id is not valid. duplicate_not_accessible: its client_ref already made a submission that the key's creator can no longer open, or that was deleted since (neither id is shown). duplicate_read_failed: the original could not be read just now, so send the entry again in a moment (section 4.7.4). |
order_needs_reconciliation |
409 | A partner order (Recruitee send_candidate, Greenhouse-format send_test, Zoho Recruit POST orders) exists but cannot be confirmed: the invitation may or may not have gone out, and its result was not recorded. That is the case when a send failed after the invitation was saved, and also when the request that placed the order has recorded no result for more than two minutes (it may have died or only be slow). Nothing is sent again for this report, and every retry answers the same. The body carries order_id: give it to Flowmingo support (section 8.6). Reading such an order through the status read (Zoho Recruit GET /ats/zoho-recruit/orders/{id}, Greenhouse-format GET /ats/greenhouse/test_status) answers the same 409 with the order_id, never a failed status. |
order_in_progress |
503 | Another request is still sending the same partner order and has not finished. A call that places the order waits for it for up to about 30 seconds before it answers this; the status read of an order whose first request is less than two minutes old answers it at once, without waiting. Send the same request again in a moment: placing the order then answers with the same order and no second invitation, and the status read answers once the order is settled (section 8.6). |
submission_not_found |
404 | The transcript and recording links route (section 5.10): the result is not in your organization, the key's creator cannot open it, it is a practice or trial run, it was deleted, or the id does not exist. All of these answer with the same body. The integrity route (section 5.13) answers it, for example, for a result that is not yours, one the key's creator cannot open, a practice run and an id that does not exist. |
assessment_type_not_supported |
400 | The integrity route (section 5.13) does not read this kind of result: only a Skill Test and a Technical Test are read. The answer is normally this 400, and nothing of the result was read. |
media_busy |
503 | Too many recording requests were being served at that moment (section 5.10). Nothing was read. Wait the seconds in the Retry-After header and send the same request again. |
media_links_unavailable |
503 | The recording links could not be made just now (section 5.10). Nothing was changed. Send the same request again in a moment. |
user_banned |
403 | The person who created the key has been blocked from Flowmingo. Contact support. |
key_creator_left |
403 | The person who created the key has left the company or no longer has access, so the key cannot be used. A key keeps working for a grace period after its creator leaves (normally 7 days). It stops then, or at once when no other Owner is left, and it then shows as turned off. An Owner or Admin can take it over in Settings -> Integrations -> API Keys, and turn it back on if it is off (section 11). |
key_reach_unavailable |
503 | We could not check which projects the key may use just now. Nothing was read or changed. Wait the seconds in the Retry-After header and send the same request again; that is safe for a POST too, because nothing ran (section 11). |
key_no_creator |
403 | The key has no creator on record, so it carries nobody's permissions. Create a new key, or ask an Owner to take it over (section 11). |
key_reaches_nothing |
403 | The person who created the key cannot open any hiring project, so the key cannot read or change anything. Ask an Owner to give them access or to take the key over (section 11). |
key_out_of_reach |
403 | The request names a hiring project, candidate or assessment result that the person who created the key cannot open (section 11). On the edit-project route it is also the answer for a body that carries job post words when the creator's role, as it is now, no longer lets them edit the job post, and, during the grace period after the creator has left the company, for any body with a job_post object, even an empty {} (sections 2.10 and 3.5.14). On a partner order route it is an assessment the key's creator cannot send to; that is refused before the order is reserved, so no order exists and nothing is kept (section 8.6). |
key_must_name_target |
403 | The key was created by someone limited to some hiring projects, so an invite must say which project or interview set it is for (section 4.3). |
key_events_need_all_projects |
403 | The events feed covers the whole company, so a key created by someone limited to some projects cannot read it (section 7). |
key_creator_role_lost |
403 | The person who created the key is no longer an Owner or Admin, so a key with the create_set scope cannot create interview sets. The other two creator-role refusals keep their codes project_create_requires_admin and assessment_create_requires_admin. |
org_not_active |
403 | The company account is not active, for example because it was archived, so its keys cannot be used. Contact support. |
approval_changed_since, approval_key_revoked |
409 | Answered to the person who approves a staged change on the approval page, not to a key (sections 9.7.1 and 9.7.2). approval_changed_since: the hiring project no longer has exactly the candidate applications and job posts, under the same name, that were listed when its deletion was staged (an application the candidate has not accepted yet is not counted: section 9.7.2). Nothing was deleted and the change was closed. approval_key_revoked: the key that asked was turned off, or, for a removal of candidates or a project deletion, its creator can no longer open the project or has left the company, or, for a project deletion, is no longer an Owner or an Admin. The change can no longer be approved and nothing was done. |
tracking_link_requires_admin, growth_required, custom_share_links_off, key_creator_left |
403 | The tracking-link routes (section 3.8): the key's creator is not an Owner or an Admin, making a link needs the Growth plan, Custom share links are not switched on for your company, or the creator has left the company. |
tracking_link_kind_not_supported, tracking_link_field_not_supported, tracking_link_colleague_not_supported, tracking_link_text_not_allowed, tracking_link_needs_person_or_source |
400 | The body of a tracking-link call is not one the API accepts (section 3.8.1). |
tracking_link_write_limit_reached, tracking_link_read_limit_reached, tracking_link_reads_busy |
429 | A limit of the tracking-link routes (section 3.8.5). |
tracking_link_saved_not_read_back, tracking_link_may_be_saved |
500 | A tracking-link call may have been saved even though it answered an error: see section 3.8.5 before you send it again. |
sequences_not_available |
404 | Email sequences are not open for your company (section 5.12.5). |
sequence_not_found |
404 | The sequence is not there for your company (section 5.12.5). |
key_creator_in_grace |
403 | The email-sequence routes (section 5.12.5): the key's creator has left the company and the grace period is running. |
sequence_reads_rate_limited, sequence_reads_busy |
429 | A limit of the email-sequence routes (section 5.12.6). |
sequence_reads_unavailable, sequence_reads_timed_out, results_busy, activation_preview_unavailable, key_access_changed |
503 | A read of the email-sequence routes could not be made or took too long: ask again (sections 5.12.3 to 5.12.6). |
unknown_field, invalid_range, invalid_status, invalid_page, invalid_page_size |
400 | The address of an email-sequence call carries a parameter the route does not name, or a value that is not valid (sections 5.12 to 5.12.3). |
The rows above that name a Skill Test or a Technical Test (assessment_test_requires_single_set, assessment_test_requires_single_project, project_required, project_ambiguous, assessment_test_field_not_supported, assessment_test_email_required, assessment_test_too_many_candidates, external_id_requires_email, api_key_has_no_acting_user, assessment_test_invite_limit_reached and assessment_test_invite_limit_unavailable) answer for a Showcase request too (section 4.8).
The status codes:
| Status | Meaning |
|---|---|
400 |
The request is not valid: an id or body that does not validate, an assessment the invite route cannot send, a daily limit reached on an interview, a Skill Test, Technical Test or Showcase request that breaks its rules (sections 4.6 and 4.8), a send_at that breaks the rules of section 4.9, a candidate value that does not fit or an external_id that is not valid (sections 4.3.1 and 4.3.2). message says which. |
401 |
The key was not accepted: it is missing, malformed, not recognised (including a wrong secret), deleted or disabled (section 11). A key turned off because its creator left answers 403 instead, once the secret is right (section 2.10). |
403 |
The key is valid but expired, its creator has left or can open no hiring project, the request names something outside the projects its creator can open, or it lacks a scope the route needs (sections 2.9, 2.10 and 11). |
404 |
There is no such record in your organization. An id that belongs to another organization answers exactly like an id that does not exist. |
409 |
A Skill Test invitation conflicts with one the candidate already holds (section 4.6.5), a create-project, create-assessment or create-set call reuses an idempotency_key (sections 3.5.3, 3.6.2 and 8.6), an assessment was deleted or a project already has a CV Evaluation (section 3.6.12), the assessment of a create-set key was deleted, a job post cannot be switched on or boosted as things stand (section 3.7), an invitation cannot be cancelled because its due time is no longer in the future or the sender has taken it (section 4.9.1), the reminders of an invitation cannot be paused or resumed (section 4.10), an edit-project call finds that the job post changed under it, that the project has no job post, or that the words of its job post cannot be edited because it is switched off or has expired (section 3.5.14), or a partner order cannot be confirmed, when it is placed or when its status is read (section 8.6). |
422 |
The AI content check refused a job post you tried to publish or boost (code publish_review_rejected or boost_review_rejected, with a rejection_reason). A refused boost leaves the post as it was. A refused publish is not switched on, or Flowmingo tries to switch the post back off (section 3.7). |
429 |
Your key made more than 600 requests in the current minute (code api_key_rate_limited, section 8.5): wait the seconds in Retry-After. Or a volume or capacity cap is reached: the daily limit for a Skill Test, Technical Test or Showcase (sections 4.6.6 and 4.8), the limit on creating projects (section 3.5.4), the daily checks for publishing and boosting a job post (section 3.7), the daily allowance of AI-checked edits to a job post's words (code job_post_edit_budget_reached, section 3.5.14), the generation budget for assessments, or the room to generate them (section 3.6.11). |
503 with a code |
The daily limit for a Skill Test, Technical Test or Showcase, the limit for creating projects, the generation budget for assessments, the names in a job_post, the role of the key creator, the AI check or a daily counter of a job post publish or boost, or the projects the key may use, could not be checked. Nothing was sent or created. Retry shortly (sections 4.6.6, 3.5, 3.6.12 and 3.7). A partner order that is still being sent also answers 503, with the code order_in_progress, when it is placed and when its status is read (section 8.6). An edit-project call answers 503 with job_post_outcome_unknown or project_outcome_unknown when it cannot tell whether the job post's words or the project fields were saved, and with edit_saved_answer_unavailable when the edit was saved but its answer could not be built: in all three, read the project before you decide. With job_post_edit_budget_unavailable the daily allowance of edits could not be read and nothing was changed (section 3.5.14). The transcript and recording links route answers 503 with media_links_unavailable when the links could not be made, and with media_busy and a Retry-After header when too many of its requests are being served: nothing was changed, and nothing was read for media_busy, so send the same request again (section 5.10). |
502 or 500 with a code |
A create-assessment call that was accepted could not be finished: the code (section 3.6.12) says whether sending the same request again can finish it. An edit-project call answers 500 with job_post_not_saved when the job post's words were not saved (section 3.5.14). Without a code, treat it as a 5xx. |
5xx |
A problem on our side. Wait and retry with a growing delay. If it persists, contact Flowmingo support and quote the x-correlation-id response header. |
Branch on statusCode and code, never on the wording of message: the wording can change, and it is translated when your request carries an Accept-Language header. A 201 on the invite route can still hold a message for one candidate, or a warnings item with the code assessment_not_active (a warning, not an error: section 4.5). When a step fails inside Flowmingo that message is one fixed sentence with a short reference, never the failure's own text.
8.5 Limits
- Each API key can make 600 requests a minute. The minute is the UTC clock minute, and the count starts again at the top of it. The 601st request of a minute is refused with
429and the codeapi_key_rate_limited: themessagesays how many seconds to wait and theRetry-Afterheader says the same. Nothing is done for a refused request, so you can send it again after the wait, aPOSTincluded. When we count a request in time, its answer carriesX-RateLimit-Limit(600),X-RateLimit-RemainingandX-RateLimit-Reset(when the count starts again, in epoch seconds), on a success and on an error alike. The one exception is the429of the data-question route that saysdata_rate_limited(section 5.11), whose headers describe your company's question window. The limit is per key: two keys each have their own 600. A request whose key is missing, malformed, unknown, turned off, expired, held by a banned person or stopped because its creator left (section 2.10) uses none of it; a valid key that lacks the scope of the route does use it. If we cannot count just now, the request is let through instead of being refused, and its answer then has noX-RateLimit-headers. 600 a minute is far above what an integration normally needs (a poll every 5 seconds is 12 a minute): space out bulk imports, and slow down whenX-RateLimit-Remainingruns low. The limits below are separate, each with its own code, and the headers above describe only this one. Also write your client so that it slows down and retries later when it receives a429or a5xx. - The tracking-link routes and the email-sequence routes have limits of their own, counted for your whole company and not for one key: 30 reads and 30 changes a minute (and 300 changes a UTC day) for tracking links (section 3.8.5), and 30 units a minute by default for the four email-sequence reads (section 5.12.6). The limit of 600 requests a minute for each key applies to them as well.
- Your organization and each key also have ceilings that sit far above normal use: 1,500 API calls a minute and 30,000 a day for your organization, counted across all your keys, and 20,000 calls a day for any one key. Since 5 November 2026 a call past one is refused with
429and the codeapi_org_rate_limited,api_org_daily_limit_reachedorapi_key_daily_limit_reached. Nothing is processed, so you can send it again after the wait inRetry-Afterand in themessage. Build your client to expect the429and the codes. - One key can make 300 invitations an hour and 1,000 a day (UTC). Every invitation a call asks for counts: the candidates it names times the assessments it names (100 candidates and 5 assessments is 500), counted when the call is accepted for processing, whether or not each one is then sent. A call that only adds candidates (
send_invitefalse) counts none. An invitation you approve in the Flowmingo app (one an AI assistant prepared with your key) counts when you approve it, and an order from your applicant tracking system counts when it is placed, in both cases against the key it was made with. Since 5 November 2026 a call past a limit answers429with the codeapi_key_hourly_invite_limit_reachedorapi_key_daily_invite_limit_reachedand nothing is sent, and a single call that would make more than 300 invitations can never pass: send smaller calls. These apply in addition to the organization invitation caps below: a call has to pass both. - One invite call can name at most 20,000 candidates. A call other than a Skill Test, Technical Test or Showcase one that names more is refused with
400and the codetoo_many_candidates(themessagesays the limit and how many you named) and nothing is created, so you can send it again as several smaller calls. Other checks, such as the limits above and the size of the request body, can refuse a call before this one does. A Skill Test, Technical Test or Showcase call has its own, lower limit of 100 candidates (sections 4.6.1 and 4.8). - E-mail is spread so one organization cannot crowd out the rest (in force since 5 November 2026): the invitation, reminder and message e-mails that Flowmingo sends for your organization go out at up to 1,500 recipients an hour and 5,000 a day (UTC clock hour and day; e-mail you send through your own connected mailbox is not part of this). An e-mail that would take your organization past that number in the hour or the day is held as a whole (a batch is never split, so recipients that would still have fitted wait too), and is sent shortly after the next hour or the next 00:00 UTC, and held again if that hour is full too; nothing is dropped and the API call that asked for it is not refused. A held invitation can reach a candidate late, and an invitation with a deadline can arrive after it has expired.
- An organization can hold 50 active API keys (section 2.2).
- What a limit answer tells you: the
429answers for the key's minute limit, the Skill Test, Technical Test and Showcase caps and the project limits already say in theirmessagewhich limit you reached and its number, that nothing was done, when to try again and whom to write to. The organization and key ceilings and the key invitation limits say the same. The key-count answer (409) and a single call that is too large say what to change instead of a time, and the older daily invitation caps (400, below) do not carry the address. For a higher limit of any kind, write to contact@flowmingo.ai with your organization's name and what you are building. - Invitations have daily caps: 5000 a day for your organization and 1000 a day for any one assessment, checked when
send_inviteistrue. A request that names several assessments is checked on their combined count, so send one request per assessment when you are close to a limit (section 4.3). - Skill Tests, Technical Tests and Showcases have their own daily caps of the same size: 1000 a day for one assessment and 5000 a day for your organization, counted per UTC day across all your keys. A request past a cap answers
429with the codeassessment_test_invite_limit_reachedand nothing is sent; try again after 00:00 UTC (sections 4.6.6 and 4.8). - A scheduled invitation is checked against the invitation caps twice: on the day you make the request, and on the day it is due. A due day that is full answers
400with the codesend_day_full(section 4.9). - Publishing and boosting a job post run an AI check, so each has a daily cap: 50 publish checks and 50 boost raises per organization per UTC day, counted across all your keys. A call past a cap answers
429with the codepublish_review_limit_reachedorboost_limit_reached(section 3.7). - Editing the words of a job post that is switched on runs an AI check, so it has a daily allowance: 50 such edits per organization per UTC day, counted across all your keys, on a counter of its own. A call refused because the job post is switched off or has expired (
409job_post_is_off), an edit of project fields only and a call that changes no word do not use it. A call past the allowance answers429with the codejob_post_edit_budget_reachedand aRetry-Afterheader with the seconds until 00:00 UTC, and changes nothing (section 3.5.14). - Creating projects has its own caps: 10 a minute and 300 a day (UTC) for your organization, counted across all your keys. A repeat of a finished call (same
idempotency_key) does not count. A call past a cap answers429with the codeproject_create_limit_reached(section 3.5.4). - Creating assessments has a generation budget: 60 units a minute and 1,500 a day (UTC) for your organization, counted across all your keys and the assessments that project calls make. A CV Evaluation or a Showcase costs 1 unit, an AI Interview or a Role-play 2, a Skill Test 3 and a Technical Test 6. A repeat of a finished call costs nothing. A call past the budget answers
429with the codeassessment_create_limit_reached(section 3.6.11). - Recordings have caps: 50 files in one call to the upload route, 50 entries in one call to the create route, 1 GiB for each file, and 120 minutes for each recording, which is checked when it is processed (section 4.7).
- Reading a transcript and its recording links shares a few places between callers: a server handles only a few of these requests at once, and no more than half of them for one organization. A request that cannot get a place within a few seconds answers
503with the codemedia_busyand aRetry-Afterheader (3 seconds), before anything is read (section 5.10). - Page size is capped: 100 rows on the list routes, 200 events on the events feed, 200 transcript entries on the transcript route (section 5.10).
8.6 Retrying safely
A request that timed out may still have succeeded. Before you repeat a call, look at what the first one did, and keep in mind that the create-project route and the six create-assessment routes take an idempotency key (sections 3.5.3 and 3.6.2), the create-set route takes one as an option (section 3.2), the recording create route has a client_ref for each entry instead (section 4.7.3), and a partner order is recognised by the partner's report id. An external_id is never a retry key (section 8.8):
| Call | If you repeat it |
|---|---|
Any GET |
Safe. |
| Invite | An email address that already has an active invitation to the same assessment is normally not invited twice: that candidate's entry in results carries an error message (in English, Email already invited) inside a 201. An invitation that is inactive can be reactivated and queued again by a new request with send_invite true. Read the result of the first call before you retry. Your own ids do not make the request safe to repeat, but they are safe to send again: an id already stored as you sent it is confirmed, a different one already there is kept, and a blank is filled (section 4.3.1). The records behind ats_candidate_id are saved again by every request, so record_id is different each time (section 4.5). |
| Invite to a Skill Test or Technical Test | Usually safe, but not a guarantee. A candidate who already holds the invitation is returned with reused_existing true, and the API tries not to email them twice; that protection lasts about ten minutes from the start of an attempt. After a failed: value, a delivery_in_progress or a timeout, check the invitation in the dashboard before you repeat the call, or wait about ten minutes (section 4.6.5). The API never revives a cancelled or used-up invitation. Repeating a Skill Test call can update an unstarted invitation to the test's current settings. |
| Invite to a Showcase | Safe in the normal case: the first email of an invitation is claimed before it is sent, so a retry or a second key cannot email the same person twice, and the API never revives a cancelled or expired invitation. After failed:delivery_unknown, check the invitation in the dashboard before you resend it there (section 4.8.2). |
| Cancel a scheduled invitation | The first call cancels it. A repeat answers 409 invitation_not_scheduled and changes nothing (section 4.9.1). |
| Publish, unpublish or boost a job post, or set a project's status | Safe. changed false means the switch of the post, the boost tier or the status of the project was already as you asked. An unpublish can still withdraw the boost, and a publish can still run the content check of a post that was never checked. After a timeout, read the job post's publication before you repeat it (section 3.7). |
| Edit a hiring project and its job post | Safe when no one else has changed the same fields in between. The call sets fields to values, so the same body again leaves the values that are already saved as they are (changed false, nothing written, no new check, nothing counted against the daily allowance). The last write wins, so a repeat sent after a colleague changed the same field writes your value over theirs: send one edit per job at a time. Each repeat that changes the words of a switched-on job post uses one unit of the daily allowance again, and a unit taken by an edit that then failed is not given back. After a timeout, a 5xx or a 503, read the project (section 3.5.13) to see what was saved, comparing job_post.sent_title and job_post.sent_description (of the project's newest job post) with what you sent, after removing leading and trailing blanks from it, then send the same body again if needed (section 3.5.14). A 409 job_post_is_off changed nothing and will answer the same until the job post is switched on. |
| Pause an invitation's reminders | Safe, and a repeat while the reminders are paused changes nothing, but it is not always a 200. While the invitation can still be paused, a repeat answers 200 with already true and pauses nothing further; if the reminders were resumed in the meantime (by the resume call or by a recruiter) or restarted in the dashboard (for example with "Send Flowmingo's invitation instead"), the repeat pauses the new ones and answers 200 with already false (a further repeat is true again); if the invitation has joined a live multi-assessment bundle since, the repeat answers 409 reminders_managed_by_bundle; if you cancelled it through the API in the meantime, 404 invitation_not_found; if it became one that only an email sequence sends, 409 reminders_managed_by_sequence (section 4.10). |
| Resume an invitation's reminders | Safe to repeat while nothing is paused (200 with already true), but do not resume blindly: a resume after a reminder's due day skips that reminder and every later one of that channel, so a retry that comes late can skip what an earlier resume would have sent (section 4.10.1). |
| Add to a hiring project | Refused with 400 (already in the project), so no duplicate. An external_id is not an idempotency key: the repeat is refused, and the application keeps the id it was created with. |
| Move to a stage | Safe. Moving an application to the stage it is already in changes nothing. |
| Reject | Safe. An application that is already rejected stays as it is. |
| Add to your collection | Safe. The tags and note are updated. |
| Rate | Your current rating ends up the same: the new rating archives your earlier ratings (one that came with a note stays on its note). Each call makes a new rating record, so an external_id you send names the new record, and the archived one keeps its own (section 6.5). |
| Shortlist | The state ends up the same. Each call also adds an entry to the result's activity, and an external_id you send names that entry (section 6.6). |
| Add a note | Not safe. Each call adds another note. An external_id names the note; it does not make the call safe to repeat. |
| Create a hiring project | Safe with the same idempotency_key and the same body: you get the original project back, with idempotent_replay true, and its assessments are reported again, never made twice (sections 3.5.3 and 3.5.5). A different key creates a second project. If you did not get the answer and do not have the body, read the status of the call by its key (section 5.7). |
| Read where a project create call stands | Safe. It only reads, and creates nothing (section 5.7). |
| Create an assessment | Safe with the same idempotency_key and the same body: you get an assessment that is already ready back (200, idempotent_replay true) at no extra cost, and no second assessment is made. Repeating a request that is still pending or has failed resumes it: it can take another attempt's units and answer 201, 202 or an error (sections 3.6.2 and 3.6.11). A different key makes a second assessment. |
| Create submissions from recordings | Safe in the normal case, with the same client_ref and upload_id for each entry, for seven days: an entry that already created a submission comes back as duplicate with the original submission_id. It is not a guarantee. After seven days the client_ref is forgotten, and in the rare case where the record of a created submission could not be saved, a repeat can create a second submission. When you are not sure an entry was created, look for it before you send it again (section 4.7.3). A retry that sends a different external_id gets the original submission back with the id it was created with. A duplicate can also come back rejected: with duplicate_not_accessible if the key's creator can no longer open the original or the original was deleted, or with duplicate_read_failed, which is worth sending again (section 4.7.4). |
| Get upload URLs for recordings | Safe, but each call makes new upload URLs, and an upload that is never used is simply left (section 4.7.2). |
| Create an interview set | Not safe without an idempotency_key: each call creates another set. With the optional idempotency_key (8 to 128 characters from A-Z a-z 0-9 . _ : -), repeating the same request with the same body returns the assessment the first call made (200, idempotent_replay true) and makes no second one; its questions and criteria are listed as they are now, in their current order, so never pair them with your input by position; an external_id matches a row only where you sent one that no other entry in the list carries (section 3.2.6). If the first call could not attach the assessment to the project_id you named, the repeat attaches it. The first answer does not say whether the attachment worked, so check project_ids of the assessment on GET /hiring/interview-sets/v1 (section 5.6). The key is reserved before anything is made, so two identical requests at the same moment make one assessment: the second answers 409 idempotency_request_in_progress with a Retry-After header, and a repeat a few seconds later gets the assessment. The same key with a different body (an external_id included) is 409 idempotency_key_reused, and an assessment that was deleted since is 409 interview_set_was_deleted and is never remade under the same key. A request that is refused or fails does not use its key up. A request with a key cannot also send addons (400 idempotency_key_not_supported_with_addons). There is no status read for this route: repeat the request. |
Place a partner order (Recruitee send_candidate, Greenhouse-format send_test, Zoho Recruit POST orders) |
Safe when the request carries the partner's report id. A retry with the same report id (Recruitee report_id, the last part of the Greenhouse record url, Zoho Recruit request_id) returns the first order and emails nobody, and two identical calls at the same moment are one order: the invitation is sent once and both answer with the same order. A call that arrives while the order is still being sent waits for it (up to about 30 seconds), and answers 503 order_in_progress if it is still not settled: send it again. If the invitation may have gone out but could not be confirmed, the order is kept for Flowmingo to check, and that call and every retry answer 409 order_needs_reconciliation with an order_id: nothing is sent again, so give the order_id to Flowmingo support. The same 409 answers an order whose first request has recorded no result for more than two minutes. That request may have died or may only be slow, and nobody can tell which, so a retry does not release the order for a new send and does not send it again. If the first request does finish after all, it settles the order itself and later requests get the order as placed. If a send failed and nothing was created, the report id stays free and the same request can be sent again. A retry of an order that already exists gets that order back (or the 409) without the length and NUL check of its email, candidate id, application id or callback_url being made again; only the report id is checked first (section 9.6). The retry body must still pass the route's request validation: a Zoho Recruit external_id of 129 characters, for example, or an email that is not an address, is a 400 on a retry too. A new order for an assessment that the key's creator cannot send to is refused with the invite route's 403 key_out_of_reach before anything is reserved, so no order exists and nothing is kept for Flowmingo to check. A request with no report id (a Greenhouse send_test with no usable url) cannot be recognised as a retry, so every call is a new order. Only the Zoho Recruit route takes an external_id: the Greenhouse and Recruitee bodies are the partners' fixed formats, and nothing in them is read as one (section 9.6). |
Read a partner order's status (Zoho Recruit GET /ats/zoho-recruit/orders/{id}, Greenhouse-format GET /ats/greenhouse/test_status) |
Safe, it only reads. An order that is only a reservation has no status to report, so it is never read as failed or sent. While its first request is less than two minutes old (the invitation is still being sent) the read answers 503 order_in_progress at once, without waiting: ask again in a moment. For an order kept for Flowmingo to check, and for one whose first request has recorded no result for more than two minutes, it answers 409 order_needs_reconciliation with the order_id, the same as placing it again would: ask again later, and give the order_id to Flowmingo support if it does not clear. Once the first request has finished, or Flowmingo has confirmed the order, the same call answers its status (section 9.6). |
Finding out what a first call did. For a project, read GET /hiring/projects/requests/{idempotency_key}/v1: it needs only the key (section 5.7). For an assessment made with the AI, read GET /assessments/requests/{idempotency_key}/v1 (section 3.6.3). For an interview set there is no status read: repeat the request with the same key. No list can be filtered by an idempotency_key; filter by your own external_id where section 5.9 allows it.
8.7 Staying compatible
Responses and webhook payloads gain fields over time. Read the fields you need and ignore the rest; a new field is not a breaking change. The invite response still returns its older single-value fields beside the arrays for integrations written against it. Section 12 lists the changes that can affect an integration.
8.8 Your own ids: external_id
Most things the API creates, invites or submits can carry an id of your own, so you can find them again by the number your own system uses (a requisition number, a job id, a candidate id) without keeping a table of Flowmingo's ids. The field is always called external_id. This section states its rules once; the section of each route says where to send it and where it comes back.
The rules. They are the same for every kind of record.
- Optional. Leave it out, or send
null, and the record has none. The one exception is an entry ofstagesin the create-project call, which exists only to carry an id (section 3.5.11). - Plain text, 1 to 128 characters. It must be a JSON string: the number
48213is refused, not turned into text, so send"48213". Characters are counted as Unicode code points, not as bytes: a simple emoji is one, so 128 of them are accepted and a 129th is refused. An emoji made of several code points, such as a flag or a family, counts as each of them. - No control character, and not only whitespace. A tab, a line break, NUL, DEL and the other Unicode control characters are refused, and so is text made only of spaces or other whitespace. Half of a surrogate pair on its own (for example the lone code unit U+D800, sent as a JSON unicode escape) is refused too; an emoji, which is a whole pair, is fine. Spaces around or inside other text are allowed.
- Stored exactly as you send it. Nothing is trimmed, lower-cased or normalised, so
REQ-1,req-1andREQ-1(with a leading space) are three different ids. What you read back is what you sent. - Yours, and not unique. It is scoped to your company: another company can use the same text for something unrelated. Flowmingo does not make it unique, because a reopened requisition can become a new project with the same number and an ATS id can repeat. Two records can carry the same text, so a lookup returns every match, one page at a time, and the Flowmingo
idtells two matches apart. - Set when the record is created, and it cannot be changed afterwards. No route changes an id that is there. Records that existed before this field did, and records that a candidate or a background job creates (such as a result a candidate produces by taking an assessment), hold
null. The invite route can name a person, an application or an invitation that already exists: it fills an id that is blank and keeps one that is there (section 4.3). - Returned wherever the record is shown, as
external_id:nullwhen the record has none. - Shown to every key that may read the record, so keep personal data out of it. An
external_idis your own text, and every route that returns the record shows it to a key that is allowed to read the record: a key withread_hiringreads theexternal_idof an application, for example, withoutread_candidates. Do not put a name, an email address or a phone number in one. Use an opaque id from your own system. - Never copied. A duplicated project, and a copy of an assessment, a question or a criterion, start without an id, even when the original has one.
Which records take one.
| Record | Where you send it | Where it comes back | Filter | Special rules |
|---|---|---|---|---|
| Hiring project | external_id in POST /hiring/projects/v1 (section 3.5.11) |
The create answer and its replay; each row of GET /hiring/projects/v1; GET /hiring/projects/{id}/v1 (section 3.5.13); project.external_id in GET /hiring/projects/requests/{idempotency_key}/v1 (section 3.5.12) |
GET /hiring/projects/v1?external_id= |
Part of the request: the same idempotency_key with another id is a 409. |
| Job post | job_post.external_id in the same call |
job_post.external_id in the create answer; each row of GET /hiring/job-posts/v1 and GET /hiring/job-posts/{id}/v1; job_post.external_id in GET /hiring/projects/{id}/v1 |
GET /hiring/job-posts/v1?external_id= |
The job post of a duplicated project holds null. |
| Pipeline stage | stages in the same call: up to 4 entries { "name", "external_id" }, the name one of Unassigned, Shortlisted, In Progress, Rejected, spelled exactly like that |
stages[].external_id in the create answer; each row of GET /hiring/projects/{projectId}/stages/v1 |
None | Only the four default stages of the project being created can take one. A stage you leave out gets null. |
| Assessment made with the AI: AI Interview, CV Evaluation, Role-play, Skill Test, Showcase, Technical Test | external_id in the body of the matching POST /assessments/<type>/v1 (section 3.6.4) |
external_id in the answer of the create call and of its replay; assessment.external_id in GET /assessments/requests/{idempotency_key}/v1; each row of GET /hiring/interview-sets/v1 |
GET /hiring/interview-sets/v1?external_id= |
Part of the request: the same idempotency_key with another id is a 409. The answer shows null once the assessment has been deleted. An assessment made inside a create-project call gets none (section 3.5.5). |
| Assessment made from your own questions | external_id in the body of POST /interview/set/v1 (section 3.2.5) |
results.external_id in the create answer; each row of GET /hiring/interview-sets/v1 |
GET /hiring/interview-sets/v1?external_id= |
Part of the request when you send an idempotency_key (section 3.2.6). |
| Question | external_id on an entry of iai_questions in the same call |
results.iai_questions[] in the create answer |
None | No other route reads a question back. A keyed repeat lists the current questions in their current order: never pair rows with your input by position, and match by external_id only where you sent an id that no other question carries. A question with no id, one with a repeated id and one Flowmingo prepared for a realtime interview cannot be matched (section 3.2.6). |
| Criterion | external_id on an entry of iai_requirements in the same call |
results.iai_requirements[] in the create answer |
None | No other route reads a criterion back. The same rule as for a question: never pair by position, and a criterion with no id, or with an id another criterion carries, cannot be matched by external_id. |
| Person | candidates[].external_id in POST /interview/candidate/invite/v1 (section 4.3) |
external_id in that candidate's entry of results (section 4.5); each row of GET /hiring/candidates/v1 and GET /hiring/candidates/{id}/v1 |
GET /hiring/candidates/v1?external_id= |
Kept on your company's record of the person, never on Flowmingo's shared candidate profile, so another company sees nothing of it. The candidate needs an email. Reading it back needs read_candidates. The lookup also finds a person invited to an assessment without a hiring project, unless you also send com_project_id (section 5.6). |
| Application (the person in one job) | candidates[].application_external_id in the invite call, which must name exactly one hiring project; or external_id in POST /hiring/applications/v1 (section 6.1) |
application_external_id in the invite result; external_id in each row of GET /hiring/applications/v1 and in the answer of POST /hiring/applications/v1 |
GET /hiring/applications/v1?external_id= |
It is your id, not candidate data: a key with read_hiring and without read_candidates sees it. |
| Invitation | candidates[].invitation_external_id in the invite call, which must name exactly one assessment and send the invitation |
invitation_external_id in the invite result; external_id in each row of GET /hiring/interviews/upcoming/v1 |
None | null in the upcoming list for an invitee who is still anonymous to your company (Matched Talent). |
| Recording submission | items[].external_id in POST /interview/recording/v1 (section 4.7.3) |
That entry of results; external_id in GET /hiring/submissions/v1, GET /hiring/submissions/{id}/v1 and GET /hiring/submissions/{id}/detail/v1 |
None yet: ?external_id= on the results list is a 400 |
Separate from client_ref. null on every other result, and visible to a key without read_candidates. |
| Note, rating or shortlist mark | external_id in the body of POST /hiring/submissions/{id}/note/v1, /rate/v1 or /shortlist/v1 (sections 6.4 to 6.6) |
The answer of that call only: no route lists these records | None | It names that event, not the result's current state. A later rating or shortlist call archives the earlier record, which keeps its own id. |
| Zoho Recruit order | external_id in the body of POST /ats/zoho-recruit/orders (section 9.6) |
The answer of that call and of GET /ats/zoho-recruit/orders/{id} |
None | A retry with the same request_id returns the first order with the id it was placed with. The Greenhouse and Recruitee routes take none. |
Finding records by your id. A filter is a query parameter named external_id on a list in the table.
- It is an exact match, case-sensitive, with no wildcards (
%,_and*are ordinary characters) and no trimming. - It is applied after your company and your key's reach (section 2.9): a key whose creator is limited to some projects finds only what it could already see, and another company's record with the same text is never found.
- It is not unique, so the answer is the list's usual page (
data,total,page,limit; section 8.2), wheretotalcounts the matches. It combines with the list's other filters. - A blank value, a value of only whitespace, one over 128 characters, one with a control character, a repeated one (
?external_id=a&external_id=b) and a bracketed one (?external_id[]=a,?external_id[x]=a, or the same with the brackets percent-encoded) are a400and never mean "no filter". Send the filter once, as?external_id=your-id. Only the nameexternal_idfollowed by[counts as bracketed: any other bracketed name is not this filter and is ignored as section 8.3 says. - A misspelt name (
externalId,external-id) is not this filter. It is ignored like any unknown query key (section 8.3), and the unfiltered list comes back. Spell itexternal_idand check the rows you get back.
curl -s -H "X-API-Key: $FLOWMINGO_API_KEY" "$BASE/hiring/projects/v1?external_id=REQ-48213"
{
"data": [
{ "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Support Engineer - Q4 hiring", "external_id": "REQ-48213", "created_at": "2026-11-02T04:18:44.051Z", "candidates": 0 }
],
"total": 1,
"page": 1,
"limit": 20
}
When a value is refused. Nothing is created in any of these.
| Where | Answer |
|---|---|
A body field of a create route: the project, its job post and its stages, the six assessment routes, create_set and its questions and criteria, add a candidate to a project, note, rate, shortlist, a Zoho order |
400, with no code, and the message "external_id must be plain text of 1 to 128 characters with no control characters. It is stored exactly as sent." |
The invite route (external_id, application_external_id, invitation_external_id) |
400 with the code external_id_invalid. The message names the row and the field, for example candidates[2].external_id, and never repeats the value. |
| An entry of the recording create route | Only that entry comes back rejected, with the code invalid_external_id. The other entries carry on. |
| A filter | 400, with no code. A blank, over-long, control-character or repeated value gets the same message as a body field. A bracketed spelling (external_id[]=a) gets a message that says brackets are not accepted and that nothing was filtered. On the results list every spelling gets the sentence of section 5.2. A query string of more than 1,000 parameters gets the "Too many query parameters" sentence of section 8.3, on these lists and on the results list. |
Next to the keys you already use. external_id is none of these, and they keep their meaning.
| Key | What it is for | How external_id differs |
|---|---|---|
idempotency_key (projects, the six assessment routes, create_set) |
Makes a retry safe: the same key with the same body returns the first record. | external_id is never a retry key. It is part of the request those calls compare, so the same idempotency_key with a different id, an id added or an id dropped is a 409 with the code idempotency_key_reused (sections 3.5.3, 3.6.2 and 3.2.6). |
client_ref (recordings) |
Tells a retry from a new recording; it is never stored. | external_id is stored and never used to recognise a retry. A retry that sends another external_id gets the original submission back, with the id it holds. |
ats_candidate_id (invite) |
Your candidate id on the invite route, kept in the record of that invitation. It is plain text of up to 250 characters, not a UUID. | It is answered on the invite and not readable later through any public integration route. external_id is the id you can read back and filter by. |
A moment's delay. Three reads can lag the write.
- A create call writes an assessment's
external_ida moment after the assessment exists, and its answer, and a replay of it, always show it. A list read from another process at that very moment can still shownull. - On the invite route, the ids of the person, the application and the invitation are written right after those records exist, not inside the same step. In that moment, and after a crash until you send the invite again, a read can show
null. GET /hiring/projects/v1and the job post list are read from a replica: a project made a moment ago can take a few seconds to appear, also when you filter. To follow a create-project call that has just been made, read its request by key (section 3.5.12).
Not in this release. Do not build on these.
- Changing an id. Nothing edits one after the record exists.
- Erasing the ATS copy of a candidate. The copy Flowmingo keeps of the email, names and CV link of a candidate you invite with an
ats_candidate_idis not changed by this release. - A repeat of an invite. The invite route has no idempotency key: a repeat with an
ats_candidate_idsaves new ATS records each time, as before, and the three ids do not change that. - A filter on the results list.
GET /hiring/submissions/v1has noexternal_idfilter yet (the table has no index for it); the parameter is a400. Read a result by its id, or filter bycom_interview_set_idortal_candidate_id. - A project-list filter by
idempotency_key. There is none. The way to recover a project from its key is the status read (section 3.5.12). - A "link failed" signal on
create_set. The answer does not say whether the project link worked; a keyed repeat repairs a link that failed (section 3.2.6). - Ids on partner orders other than Zoho. The Greenhouse and Recruitee routes take none, because their bodies are the partners' fixed formats.
- Ids on the assessments of a create-project call. They get
null. - Staged invitations.
POST /approvals/invite/v1(section 9.7) keeps only each recipient's name and email, so it stores none of the three ids and refuses a candidate row that carries one. A row whoseexternal_id,application_external_idorinvitation_external_idis notnull(an empty or an invalid value counts too) answers400with the codeexternal_id_not_supported_when_staging, and nothing is staged. The message names the row and the field, for examplecandidates[1].external_id, and never repeats the value. Send an invitation that carries your ids to the invite route (section 4.3.1), or leave the ids out of the staged one; anullis the same as leaving the field out. - The MCP server has no field for an id of your own: none of its tools takes an
external_id, so it cannot set one (its invite tool stages only each recipient's name and email, section 9.7). Its list tools hand back the API's rows as they are, so a row they list carries itsexternal_idwhere the list route returns one. - Other languages. The API guide exists in English only; nothing here is translated.
Older servers. A server that does not have this release does not know the filter. It ignores ?external_id= and answers 200 with the ordinary, unfiltered page. Before you act on a row, check that its external_id is the one you asked for: a row from such a server has no external_id key at all.
9. All 77 operations
Every path is relative to https://apis.flowmingo.ai/company/integration. The OpenAPI document describes the same 77 operations, each with its summary, scope, parameters, request body and response shape.
9.1 Account
| Operation | Scope | What it does | Section |
|---|---|---|---|
GET /me/v1 |
none | Shows the organization, scopes and expiry of the key you sent. | 2.7 |
9.2 Create assessments, invite candidates and hand over recordings
| Operation | Scope | What it does | Section |
|---|---|---|---|
POST /hiring/projects/v1 |
create_project |
Creates a hiring project with its four default stages and an AI-drafted job post, optionally with its assessments, job post details set by name, and your own external_id on the project, the job post and the stages. |
3.5, 3.5.11 |
GET /hiring/projects/requests/{idempotency_key}/v1 |
create_project |
Reads where a create-project call has got to: whether the project was saved, its id, and its title and external_id as they are now. Changes nothing. |
3.5.12 |
POST /hiring/projects/{id}/update/v1 |
create_project |
Edits a hiring project's title, position and description, and the words of its job post. The job post's words go through the AI content check, which can reword them: read differs_from_sent and review. Idempotent when no one else has changed the same fields in between (section 8.6); the last write wins. Changes the words only of a job post that is switched on (otherwise 409 job_post_is_off); such an edit counts against a daily allowance of 50. The answer shows the project only to a key that also holds read_hiring. |
3.5.14 |
POST /hiring/job-posts/{id}/publish/v1 |
create_project |
Switches a job post on, after the AI content check. Answers whether its page is open (public_state). Takes no body. |
3.7, 3.7.2 |
POST /hiring/job-posts/{id}/unpublish/v1 |
create_project |
Switches a job post off, and by default its boost too (withdraw_boost). |
3.7, 3.7.3 |
POST /hiring/job-posts/{id}/boost/v1 |
create_project |
Sets the boost of a job post: off, flowmingo or extended. Free; a raise runs the AI check. |
3.7, 3.7.4 |
POST /hiring/projects/{id}/status/v1 |
create_project |
Opens, drafts or archives a hiring project. | 3.7, 3.7.5 |
POST /assessments/cv-evaluation/v1 |
create_assessment |
Creates a CV Evaluation with the AI, published and ready to use. Takes your own external_id. |
3.6, 3.6.5 |
POST /assessments/ai-interview/v1 |
create_assessment |
Creates an AI Interview with the AI, published and ready to send. Takes your own external_id. |
3.6, 3.6.6 |
POST /assessments/role-play/v1 |
create_assessment |
Creates a Role-play with the AI, published and ready to send. Takes your own external_id. |
3.6, 3.6.7 |
POST /assessments/skill-test/v1 |
create_assessment |
Creates a Skill Test with the AI, published and ready to invite candidates to. Takes your own external_id. |
3.6, 3.6.8 |
POST /assessments/showcase/v1 |
create_assessment |
Creates a Showcase with the AI, published and ready to invite candidates to. Takes your own external_id. |
3.6, 3.6.9 |
POST /assessments/technical-test/v1 |
create_assessment |
Creates a Technical Test with the AI, its coding problems proved in a sandbox, published and ready to invite candidates to. Takes your own external_id. |
3.6, 3.6.10 |
GET /assessments/requests/{idempotency_key}/v1 |
create_assessment |
Reads where a create-assessment request has got to: pending, ready with the assessment (and its external_id), or failed with a coded reason. |
3.6.3 |
POST /interview/set/v1 |
create_set |
Creates an assessment (an AI interview or a CV evaluation) with your own questions and criteria, and your own external_id on the assessment, each question and each criterion. An optional idempotency_key makes a retry safe. |
3.1 to 3.4 |
POST /interview/candidate/invite/v1 |
invite_candidates |
Adds candidates and emails each an invitation to an AI interview, a role-play, a Skill Test, a Technical Test or a Showcase, or orders a CV evaluation from a cv_link. An AI interview or role-play invitation can be scheduled with send_at. Each candidate can carry your own external_id, application_external_id and invitation_external_id. |
4, 4.6, 4.8, 4.9 |
POST /interview/candidate/invites/{id}/cancel/v1 |
invite_candidates |
Cancels a scheduled invitation while its due time is still in the future, for example one made with send_at. |
4.9.1 |
POST /interview/candidate/invites/{id}/stop-reminders/v1 |
invite_candidates |
Pauses the two automatic reminders of one AI interview or role-play invitation, sent or scheduled, on every channel that sends them. Never holds the invitation email back. Does not cancel the invitation. | 4.10 |
POST /interview/candidate/invites/{id}/resume-reminders/v1 |
invite_candidates |
Lets a paused invitation's reminders go again: one not due yet goes on its date, one due today goes at the next pass, one whose due day has passed is skipped. | 4.10.1 |
POST /interview/recording/uploads/v1 |
submit_recordings |
Step 1 of handing over recordings of interviews you ran yourself: gives an upload URL and an upload_id for each file. |
4.7, 4.7.2 |
POST /interview/recording/v1 |
submit_recordings |
Step 2: turns the uploaded files into submissions of a recording evaluation, which are then transcribed and scored. Each entry can carry your own external_id. |
4.7, 4.7.3 |
9.3 Read your hiring data
| Operation | Scope | What it does | Section |
|---|---|---|---|
GET /hiring/overview/v1 |
read_hiring |
Funnel counts: applications per stage, active and rejected, completed and in-progress results. | 5.6 |
GET /hiring/job-posts/v1 |
read_hiring |
Lists your job posts, each with its external_id; ?external_id= finds the ones that carry your id. |
5.6, 8.8 |
GET /hiring/job-posts/{id}/v1 |
read_hiring |
One job post with its description and external_id. |
5.6 |
GET /hiring/job-posts/{id}/publication/v1 |
read_hiring |
Whether a job post is open to the public (public_state), its public address and its boost. Changes nothing. |
3.7.1 |
GET /hiring/interviews/upcoming/v1 |
read_hiring |
Invitations candidates have not completed yet, each with your external_id for the invitation. |
5.6 |
GET /hiring/interview-sets/v1 |
read_hiring |
Lists your assessments with their assessment_type and external_id; ?external_id= finds the ones that carry your id. |
5.6, 8.8 |
GET /hiring/projects/v1 |
read_hiring |
Lists your hiring projects, each with its external_id; ?external_id= finds the ones that carry your id. |
5.6, 5.7, 8.8 |
GET /hiring/projects/{id}/v1 |
read_hiring |
One hiring project by its id: name, role, status, its job post (the words it displays, the words as stored, public_state, review, differs_from_sent) and its assessments. Changes nothing. |
3.5.13 |
GET /hiring/projects/{projectId}/stages/v1 |
read_hiring |
The pipeline stages of one project, each with its external_id. |
5.6 |
GET /hiring/applications/v1 |
read_hiring |
Lists the candidates in your hiring projects, with stage, scores and the application's external_id; ?external_id= finds the ones that carry your id. |
5.6, 8.8 |
GET /hiring/submissions/v1 |
read_hiring |
Lists assessment results of every kind. Each carries external_id; the list cannot be filtered by it yet. |
5.2 |
GET /hiring/submissions/{id}/detail/v1 |
read_hiring |
One result with its AI evaluation. | 5.3 |
GET /hiring/submissions/{id}/v1 |
read_hiring |
One result without the evaluation. | 5.1 |
GET /hiring/submissions/{id}/media/v1 |
read_hiring and read_candidates |
The transcript (the stored question and answer text, one page at a time) and a signed link to each recording of one finished AI interview or role-play that can be handed out, each clip with a status (at most 500 are listed), and one availability word. Changes nothing. |
5.10 |
GET /hiring/submissions/{id}/integrity/v1 |
read_hiring, read_candidates and read_integrity |
Counts of what the candidate's browser, camera and microphone recorded during one finished Skill Test or Technical Test, with what was being watched. Needs a key whose creator is currently an Owner or an Admin. Changes nothing. | 5.13 |
GET /hiring/candidates/v1 |
read_candidates |
Lists candidates with name, email, CV link and your external_id; search with q, look up one address with email, or find the ones that carry your id with external_id. |
5.6, 8.8 |
GET /hiring/candidates/{id}/v1 |
read_candidates |
One candidate, with your external_id. |
5.6 |
POST /data/ask/v1 |
read_hiring |
Answers a question about your hiring data written in plain words, with a working that says how the answer was reached. Answers as the person who created the key; a refusal, a busy answer and an unavailable one are 200s with a status. Limited to 20 questions a minute and 1,000 a day for your company. |
5.11 |
9.4 Update your pipeline and reviews
| Operation | Scope | What it does | Section |
|---|---|---|---|
POST /hiring/applications/v1 |
write_pipeline |
Adds a candidate you already hold to a hiring project. Takes your own external_id for the application. |
6.1 |
POST /hiring/applications/{id}/move-stage/v1 |
write_pipeline |
Moves an application to another stage of its project. | 6.2 |
POST /hiring/applications/{id}/reject/v1 |
write_pipeline |
Moves an application into the project's Rejected stage. | 6.3 |
POST /hiring/submissions/{id}/note/v1 |
write_pipeline |
Adds a note, with an optional rating, to a result. Takes your own external_id for the note. |
6.4 |
POST /hiring/submissions/{id}/rate/v1 |
write_pipeline |
Rates a result from 0 to 10. Takes your own external_id for the rating. |
6.5 |
POST /hiring/submissions/{id}/shortlist/v1 |
write_pipeline |
Shortlists or un-shortlists a result. Takes your own external_id for the call. |
6.6 |
POST /hiring/candidates/{id}/collection/v1 |
write_pipeline |
Saves a candidate to your talent collection with tags and a note. | 6.7 |
9.5 Events
| Operation | Scope | What it does | Section |
|---|---|---|---|
GET /events/v1 |
read_hiring |
The events that webhooks deliver, newest first, one page at a time. Needs a key whose creator has access to all hiring projects. | 7 |
9.6 Partner endpoints for ATS integrations
These ten operations serve the Recruitee and Zoho Recruit integrations, which call them with your API key. The Greenhouse operations follow the format Greenhouse defines for assessment partners and are not a published integration. Request and response formats follow what each partner program defines, and these operations are not meant for general use, so this guide does not describe them further. The OpenAPI document does.
| Operation | Scope | Used by |
|---|---|---|
GET /ats/recruitee/validate_token |
read_hiring and invite_candidates |
Recruitee: checks the pasted key. |
GET /ats/recruitee/tests |
read_hiring |
Recruitee: lists the assessments it can send. |
POST /ats/recruitee/send_candidate |
invite_candidates |
Recruitee: sends an assessment to a candidate. |
GET /ats/greenhouse/list_tests |
read_hiring |
Greenhouse format: lists the assessments it can send. |
POST /ats/greenhouse/send_test |
invite_candidates |
Greenhouse format: sends an assessment to a candidate. |
GET /ats/greenhouse/test_status |
read_hiring |
Greenhouse format: reads the status of an order. |
POST /ats/greenhouse/request_errors |
none | Greenhouse format: reports a malformed answer, which is logged. |
GET /ats/zoho-recruit/tests |
read_hiring |
Zoho Recruit: lists the assessments it can send. |
POST /ats/zoho-recruit/orders |
invite_candidates |
Zoho Recruit: sends an assessment to a candidate. |
GET /ats/zoho-recruit/orders/{id} |
read_hiring |
Zoho Recruit: reads one order. |
Six things a caller of the order routes (POST /ats/greenhouse/send_test, POST /ats/recruitee/send_candidate, POST /ats/zoho-recruit/orders and the two status reads) can meet, whatever the partner:
- Two identical requests at the same moment send one invitation and are both answered with the same order. A request that finds the same order still being sent waits for it, for up to about 30 seconds, and then answers
503with the codeorder_in_progress: send the same request again, and you get the same order and no second invitation. - A send that failed frees the report id for a retry only when nothing was left behind. The report id is the Recruitee
report_id, the last path segment of the Greenhouseurl, or the Zohorequest_id. When the invitation may have gone out but its result could not be recorded, the order is kept, and that call and every later request with the same report id answer409with the codeorder_needs_reconciliationand theorder_idin the body. Nothing is sent again for that report id, and no other organization can claim it. Give theorder_idto Flowmingo support, who confirm the order. Before, a failed send always freed the report id, even when the candidate had already been invited. - An order whose first request has recorded no result for more than two minutes is kept the same way. That request may have died, or may only be slow and still about to send, and nobody can tell which. So a retry never releases the order for a new send and never sends the invitation again: it answers the same
409order_needs_reconciliation, the first time and on every later request. If the first request does finish, it settles its own order and later requests get the order as placed. Being wrong in this direction costs a support request; the other direction would send an assessment to a candidate twice. - A value that cannot be stored is a
400before anything is created:candidate_field_too_longfor an email, candidate id, application id or report id over 250 characters, andcandidate_field_invalidfor a NUL (zero) character, also one in the Zoho Recruitcallback_url. Before, such a value ended in a500. The report id is checked first, because it is what an existing order is looked up by. The lengths and the NUL check of the other values are made only for an order that does not exist yet: a retry of an order that exists gets that order back (or the409above), so those values never turn the retry into a400. The shape of the body (an email that is not an address, anexternal_idthat is not valid text, such as one of 129 characters, a Zohocallback_urlthat is not an https address on a Zoho domain) is still checked on every request, a retry included, before any of this. - Reading an order's status never reports an order that is only a reservation as
failedorsent.GET /ats/zoho-recruit/orders/{id}andGET /ats/greenhouse/test_statusanswer503with the codeorder_in_progressat once, without waiting, while the first request of the order is less than two minutes old and still sending. For an order kept for Flowmingo to check, and for one whose first request has recorded no result for more than two minutes, they answer409order_needs_reconciliationwith theorder_idin the body, as placing it again would. Do not read either answer as a failed order: ask again later. Once the first request has finished, or Flowmingo has confirmed the order, the same call answers the order's status. - A key sends only to an assessment its creator may send to. A new order for an assessment that the key's creator may not send to (section 2.9) is refused with the invite route's
403and the codekey_out_of_reachbefore anything is reserved: no order exists, and nothing is kept for Flowmingo to check.
GET /ats/greenhouse/test_status answers 400 ("Validation failed (uuid is expected)") for a partner_interview_id that is not a UUID. It answered 500 before. Only the Zoho Recruit order route takes an external_id (section 8.8): the Greenhouse and Recruitee bodies are the partners' fixed formats.
9.7 Approval routes for AI assistants
These ten operations are what AI assistants connected through MCP use for anything that emails, rejects, moves or removes a candidate or deletes a hiring project, and for the status, withdrawal and undo of what they asked for. Nothing here approves a change: a person approves it at an approval link, signed in to Flowmingo, within 15 minutes, and the API key cannot. They act with the permissions of the key's creator (section 2.9). The routes for the same actions in sections 4 and 6 still act at once, for your own systems. Sections 9.7.1 and 9.7.2 describe the two routes that ask for something to be deleted. This guide does not describe the other approval routes further. The OpenAPI document does.
| Operation | Scope | What it does |
|---|---|---|
POST /approvals/invite/v1 |
invite_candidates |
Stages an invitation to one assessment for up to 50 candidates. Nothing is sent until a person approves it. A staged invitation keeps only each recipient's name and email, so a candidate row with a non-null external_id, application_external_id or invitation_external_id is refused with 400 and the code external_id_not_supported_when_staging, and nothing is staged (section 8.8). |
POST /approvals/applications/{id}/reject/v1 |
write_pipeline |
Stages the move of one application into its project's Rejected stage. |
POST /approvals/applications/reject/v1 |
write_pipeline |
Stages the same for 1 to 50 applications, as one approval. |
POST /approvals/applications/{id}/move-stage/v1 |
write_pipeline |
Moves an application to another stage of its project. A stage that emails the candidate is staged for approval instead. The stage's email setting is read once, just before the move, so a setting switched on at that moment can still let a move email without approval. |
POST /approvals/applications/move-stage/v1 |
write_pipeline |
The same for 1 to 50 applications. |
POST /approvals/remove-candidates/v1 |
write_pipeline |
Stages the removal of 1 to 50 applications from their hiring project, as one approval. It is what Remove on a candidate's card does: the application is deleted, and the candidate's profile stays in your talent pool and in other projects. It is not a reject. There is no undo (section 9.7.1). |
POST /approvals/delete-project/v1 |
create_project |
Stages the deletion of one active hiring project for an Owner or an Admin to approve. The project and its job posts are deleted; its candidate applications are not deleted one by one. There is no undo (section 9.7.2). |
GET /approvals/{id}/v1 |
none | Where a change this key asked for stands. |
POST /approvals/{id}/cancel/v1 |
none | Withdraws a change that still waits for a person. |
POST /approvals/{id}/undo/v1 |
write_pipeline |
Takes back a reversible change this key made (a stage move that emailed nobody, a note, a rating, a shortlist or a tag), within 24 hours (section 6). It refuses a removal and a project deletion. |
When a person approves or declines a change that a key asked for, Flowmingo can tell your webhook endpoints with the approval.status.update event, and the same event is in GET /events/v1 (see the webhook guide for what it carries, when it is sent and when it is not). The event is a hint and it can be missing: GET /approvals/{id}/v1, with the key that asked, is the record of where a change stands.
9.7.1 Remove candidates from a hiring project
POST /approvals/remove-candidates/v1 asks for applications to be removed from their hiring project, as Remove on a candidate's card does in the dashboard. It needs write_pipeline. It never removes anyone itself: it stages the removal, and a person approves it at the approval_url.
What is removed. The application is deleted from the project. The candidate's profile and your contact for them stay in your talent pool and in every other hiring project. Nobody is emailed. Removing a candidate sends the same application.rejected webhook event that Remove in the dashboard has always sent, so a webhook consumer should not treat that event as a rejection by itself. This is not a reject: a reject (section 6.3) moves the application into the project's Rejected stage, where the candidate stays on the board and can be moved back, and a removal deletes the application. There is no undo. That is why this route is always staged for a person, whatever stage the candidate is in, and why POST /approvals/{id}/undo/v1 refuses it with 400.
| Field | Type | Required | Description |
|---|---|---|---|
application_ids |
list of UUIDs | Yes | The applications to remove, 1 to 50, each an id from GET /hiring/applications/v1. They can be in different projects. Several are one pending change and one approval. |
curl -X POST "$BASE/approvals/remove-candidates/v1" \
-H 'Content-Type: application/json' \
-H "X-API-Key: $FLOWMINGO_API_KEY" \
-d '{ "application_ids": ["2d1c0f6a-8b3e-4f57-9a62-7c41d5e0b8f3"] }'
The answer is 201. details is cut to some of its fields:
{
"status": "pending_approval",
"change_id": "8c1e5b27-3f4a-4d69-a0b2-6e7f9c1d2a34",
"action": "remove_candidates",
"summary": "Remove \"Alex Doe\" (now in the \"In Progress\" stage) from the hiring project (project \"Support Engineer - Q4 hiring\"). Their profile stays in your talent pool and in any other hiring project. There is no undo.",
"details": {
"count": 1,
"candidate_name": "Alex Doe",
"from_stage_name": "In Progress",
"project_names": ["Support Engineer - Q4 hiring"]
},
"expires_at": "2026-11-05T08:30:00.000Z",
"approval_url": "https://team.flowmingo.ai/approve/8c1e5b27-3f4a-4d69-a0b2-6e7f9c1d2a34"
}
- Hand the
approval_urlto a person. They sign in to Flowmingo and see each candidate, the stage they are in and their project, and that the profile stays and there is no undo. They have untilexpires_at, 15 minutes after your request. An Owner, an Admin, or a Member whose role lets them remove candidates in the dashboard, with access to the projects, can approve. The key, the AI and the connection that asked cannot. - Names need
read_candidates.summaryanddetailsleave out candidate names and email addresses unless the key also has that scope. The approval page shows them. - Already removed. An application that was already removed is left out and listed in
detailsasalready_removed. When all of them were, there is nothing to approve and the call answers400. - Moved before the approval. The person approves a removal of a candidate in the stage they were shown. A candidate who has been moved to another stage by then is left alone and is not removed. When every candidate has moved, nothing is removed and the change is closed as
failed: ask again, and the new request starts from where they are now. - Asking again. The same request while the first one still waits returns that change with
already_pendingtrue, not a second one, as long as what it would remove has not changed. If it has, the old request is retired and a fresh one is staged.POST /approvals/{id}/cancel/v1withdraws a change that still waits. - After the approval. Read
GET /approvals/{id}/v1. Itsactionisremove_candidates.executedsays a person approved it: readdetails.outcomebefore you tell anyone the whole batch was removed.doneis a count of the applications removed.skipped(moved since, or already gone) andfailedlist the application ids, each with a reason. - Errors.
400:application_idsis missing or empty, has more than 50 entries or an id that is not a UUID, or every application was already removed.404: an application is not one of your organization's.403: the key lackswrite_pipeline(API key missing required scope: write_pipeline), or an application is in a project the key's creator cannot open (this403carries no code; section 2.9).503: a candidate who was only just added cannot be named yet, so ask again in a few seconds. Nothing is staged after any of them.
9.7.2 Delete a hiring project
POST /approvals/delete-project/v1 asks for a hiring project to be deleted, as Delete on a hiring project does in the dashboard. It needs create_project, which is opt-in (section 2.3), and the person who created the key must be an Owner or an Admin. No new scope was added: a key that already has create_project can ask for this. It never deletes anything itself: it stages the deletion, and an Owner or an Admin approves it at the approval_url.
What is deleted, and what is not. The project and its job posts are deleted. The project's candidate applications are not deleted one by one: they stay stored under the deleted project. Candidate profiles and contacts, your assessments and your invitation links are kept. There is no undo. That is why this route is always staged for a person, and why POST /approvals/{id}/undo/v1 refuses it with 400.
| Field | Type | Required | Description |
|---|---|---|---|
project_id |
UUID | Yes | The hiring project, an id from GET /hiring/projects/v1. It must be active. |
curl -X POST "$BASE/approvals/delete-project/v1" \
-H 'Content-Type: application/json' \
-H "X-API-Key: $FLOWMINGO_API_KEY" \
-d '{ "project_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" }'
The answer is 201:
{
"status": "pending_approval",
"change_id": "b7e3a9d5-1c2f-4e86-93a4-5d6c7b8a9e01",
"action": "delete_project",
"summary": "Delete the hiring project \"Support Engineer - Q4 hiring\". Deleting it also removes its 1 job post. Its 12 candidate applications are not deleted one by one: they stay stored under the deleted project. Candidate profiles and contacts, your assessments and invitation links are kept. There is no undo.",
"details": {
"project_names": ["Support Engineer - Q4 hiring"],
"project_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"application_count": 12,
"job_post_count": 1
},
"expires_at": "2026-11-05T08:30:00.000Z",
"approval_url": "https://team.flowmingo.ai/approve/b7e3a9d5-1c2f-4e86-93a4-5d6c7b8a9e01"
}
- Hand the
approval_urlto an Owner or an Admin. They sign in to Flowmingo and see the project's name, how many candidate applications and job posts it has, and that there is no undo. They have untilexpires_at, 15 minutes after your request. A Member cannot approve it. The key, the AI and the connection that asked cannot either. - Only an active project. An archived or draft project is refused with
400: move it back to Active first (section 3.7.5), then ask again. A project that is already deleted is a400too. A project that is not one of your organization's answers404, like an id that does not exist. - Held to what was listed. The deletion goes ahead only if the project still has exactly the candidate applications and job posts that were listed when it was staged, under the same name. Applications the candidate has not accepted yet (an unanswered invitation) are not counted. If any of the counted ones were added, removed or replaced, or the project was renamed, the approval is refused with
409and the codeapproval_changed_since, nothing is deleted, and the change is closed. Ask again: the new request shows what is there now. - If the key loses its standing. When the person who created the key can no longer open the project, is no longer an Owner or an Admin, or has left the company, or the key was turned off, the pending request can no longer be approved: the approval is refused with
409and the codeapproval_key_revoked, and nothing is deleted. - Asking again. The same request while the first one still waits returns that change with
already_pendingtrue, as long as what it would delete has not changed. If it has, the old request is retired and a fresh one is staged.POST /approvals/{id}/cancel/v1withdraws a change that still waits. - After the approval. Read
GET /approvals/{id}/v1. Itsactionisdelete_project.executedsays a person approved it and the project was deleted;details.outcomegives theapplication_countandjob_post_countthe project had. - Errors.
400:project_idis not a UUID, or the project is not active or is already deleted.404: the project is not your organization's.403: the key lackscreate_project(API key missing required scope: create_project), the person who created the key is not an Owner or an Admin (project_create_requires_admin), or cannot open the project (this403carries no code; section 2.9). Nothing is staged after any of them.
9.8 Tracking links
| Operation | Scope | What it does | Section |
|---|---|---|---|
GET /tracking-links/v1 |
read_hiring and read_candidates |
Lists your tracking links (custom share links), newest first, at most 2,000, each with where it points (address) and what it opens today (target). Does not say who applied. |
3.8, 3.8.3 |
GET /tracking-links/sources/v1 |
manage_tracking_links and read_candidates |
Lists the sources a link can credit. Can create default sources. | 3.8, 3.8.2 |
POST /tracking-links/v1 |
manage_tracking_links |
Makes a link for one job post, for 2 to 50 job posts or for your careers page. Needs the Growth plan. | 3.8, 3.8.1 |
PATCH /tracking-links/{id}/v1 |
manage_tracking_links |
Changes a link's label, source, details or person, or switches it on or off. | 3.8, 3.8.4 |
DELETE /tracking-links/{id}/v1 |
manage_tracking_links |
Deletes a link. | 3.8, 3.8.4 |
9.9 Email sequences (read only)
| Operation | Scope | What it does | Section |
|---|---|---|---|
GET /sequences/v1 |
read_sequences |
The library: your email sequences, newest first, one page at a time. Costs 1 unit of the limit. | 5.12, 5.12.1 |
GET /sequences/{id}/v1 |
read_sequences |
One sequence with its current version: its emails and stop rules. Costs 1 unit. | 5.12, 5.12.2 |
GET /sequences/{id}/results/v1 |
read_sequences |
The numbers of one sequence: people by state, emails sent per step, and the dated figures over a range. Costs 2 units. | 5.12, 5.12.3 |
GET /sequences/{id}/activation-preview/v1 |
read_sequences |
What switching a sequence on would do to the people it holds. Only for a creator who is an Owner or an Admin. Costs 2 units. | 5.12, 5.12.4 |
10. Recipes: Zapier, Make and n8n
Zapier, Make and n8n connect tools to each other without code. There is no ready-made Flowmingo app in any of them. These recipes use the generic webhook and HTTP steps that all three have, together with the webhook guide and the API in this guide. Check that your plan includes those steps: in Zapier, for example, Webhooks by Zapier is listed as a premium app. The names of buttons and modules below are the tools' own and change from time to time; look for the same idea if yours read differently.
Whichever tool you use, a call to Flowmingo has the same three parts:
- the address of the route, such as
https://apis.flowmingo.ai/company/integration/hiring/submissions/{submission_id}/detail/v1; - a header named
X-API-Keywith your key. (If the step only offers "Bearer token" authentication, paste the key there: a Flowmingo key works as a bearer token, section 2.6.) - for a
POST, a body written as raw JSON with the headerContent-Type: application/json.
10.1 Recipe 1: when a candidate finishes an assessment, add a row to Google Sheets or post to Slack
The flow: Flowmingo sends a webhook when a candidate finishes; your tool catches it; your tool asks Flowmingo for the details of that result; your tool writes a row or a message.
Step 1. Make the tool listen. Create the trigger that catches a webhook and copy the address it gives you.
- Zapier: create a Zap whose trigger is Webhooks by Zapier, with the event Catch Hook. Copy the Custom Webhook URL.
- Make: create a scenario whose first module is Webhooks -> Custom webhook. Add a webhook and copy its address.
- n8n: add a Webhook node as the trigger, with the method
POST. Copy its Production URL (the Test URL only works while you are listening for a test). The workflow has to be active for the Production URL to work, and an n8n installation that cannot be reached from the internet (for example one onlocalhost) cannot receive events, because Flowmingo refuses private addresses (webhook guide, section 4.5).
Flowmingo waits at most 10 seconds for a 2xx answer (webhook guide, section 4), so leave the catch step as the tool provides it and do your own work in the steps after it.
Step 2. Tell Flowmingo where to send events. In Flowmingo open Settings -> Integrations -> Webhooks and click Create Endpoint. Give it a name, paste the address from step 1 as the Endpoint URL, choose Subscribe to Specific Events, and tick:
assessment.completed: sent when a candidate completes a skill test, technical test, showcase or questionnaire, or when the result of a recording evaluation or a CV evaluation is stored (a technical test event carries no score: see the end of step 6);interview.evaluation.update: sent when the score of an AI interview or role-play is ready (and when the result of a recording evaluation or a CV evaluation is stored).
If you would rather not pick events, choose Subscribe to All Events instead and add the filter in step 4. Your tool will then receive every kind of event, which can use up more of your plan's tasks. Click Create Endpoint. Flowmingo shows the signing secret once: copy it, because you need it to check the signature (see "About the signature" below).
Step 3. Send a test event. Open the endpoint and use Send Test Event with the event type assessment.completed. Make the tool read it: in Zapier use Test trigger, in Make run the scenario once, in n8n click Listen for test event before you send it. The tool now shows the fields of the event. The ids in a test event are not real, so a lookup in step 5 fails for a test event. That is expected.
Step 4. Keep only the events you want. Add a filter step (Zapier Filter, Make's filter on the link between two modules, n8n IF) that continues when event_type is assessment.completed, or when event_type is interview.evaluation.update and evaluation_type is not cv. A recording evaluation and a CV evaluation send both events for the same result, on purpose, so if you subscribe to both, check the submission_id against the rows you already have before you add a new one.
Step 5. Look up the candidate. The event carries ids and not personal data: an assessment.completed event has no name or email at all, and an interview.evaluation.update event has only the email address. So ask Flowmingo for the result. Add an HTTP step:
- Zapier: Webhooks by Zapier, action Custom Request, method
GET. - Make: HTTP -> Make a request, method
GET. - n8n: an HTTP Request node, method
GET.
Set the address to https://apis.flowmingo.ai/company/integration/hiring/submissions/{submission_id}/detail/v1, filling {submission_id} with the submission_id from the event (it is inside data), and add the header X-API-Key. The key needs the scopes read_hiring and read_candidates (section 2.3). The answer has the fields you will want: candidate_name, email, assessment_name, assessment_type, overall_score, score_status, submission_at and evaluation.summary (section 5.3).
Step 6. Write the row or the message.
Take every value from the answer of the lookup in step 5, not from the event itself.
- Google Sheets: Zapier Google Sheets -> Create Spreadsheet Row, Make Google Sheets -> Add a Row, n8n Google Sheets node with the operation Append Row. Map the columns: Date to
submission_at, Candidate tocandidate_name, Email toemail, Assessment toassessment_name, Type toassessment_type, Score tooverall_score. - Slack: Zapier Slack -> Send Channel Message, Make Slack -> Create a Message, n8n Slack node with Send a message. A message that works:
{candidate_name} finished {assessment_name}. Score: {overall_score}/10. {evaluation.summary}.
overall_score is empty for a showcase and a questionnaire, which the AI never scores, and evaluation is empty or short for assessments that have no AI evaluation text. A skill test also has a percentage in score_percent. Build the row or message so that an empty value does not break it (section 5.3 lists what each assessment returns).
A technical test is different. Its event carries no score, and the lookup in step 5 runs right after the attempt ended, so overall_score can still be provisional (section 5.5): code the candidate submitted may not be graded yet. Either leave the score empty when assessment_type is technical_test and fill it in with a second step that looks the result up again later, or add a wait step (Zapier Delay, Make Sleep, n8n Wait) before the lookup and write what it returns. A wait lowers the chance of a provisional number but does not rule it out, and nothing in the answer says the number is final, so when you look again, keep the latest value.
Step 7. Turn it on. Publish the Zap, switch the scenario on, or set the n8n workflow to active. Then finish an assessment yourself and watch the row appear.
About the signature. Every delivery carries an X-Webhook-Signature header, and these tools cannot check it on their own: checking needs a code step (Zapier Code, an n8n Code node, or a small receiver of your own with the code from section 5 of the webhook guide). Check the signature against the raw request body before your tool looks anything up or writes anything, and stop the run when it does not match. Without that check, anyone who learns the address and a result id can make your tool add a row or post a message. Keeping the address from step 1 secret is not a substitute for checking the signature. If your tool cannot check it, do not use a webhook: poll the events feed instead (see below).
If your tool cannot receive webhooks, or cannot check the signature, poll the events feed instead (section 7). Schedule a step every few minutes that calls GET https://apis.flowmingo.ai/company/integration/events/v1?limit=200 with your key (scope read_hiring, and a key whose creator has access to all hiring projects: an Owner's key does). The events come newest first, one page at a time: follow next_cursor until it is null or until you reach an event you have already handled, so that a burst of more than 200 events is not missed. Keep the event_id of every event you have handled (for example in a spreadsheet column or the tool's own storage) and skip those. Apply the filter of step 4 inside your tool, because the feed's event_type filter takes only one type, and use submission_id to recognise the two events that a recording evaluation or a CV evaluation sends for one result. assessment.completed is not sent for AI interviews or role-plays: their completion arrives as interview.status.update and interview.evaluation.update. Events are kept for about 30 days.
10.2 Recipe 2: invite a candidate from a form submission
The flow: someone submits a form (Google Forms, Typeform, Tally, Jotform and so on); your tool sends Flowmingo a request; Flowmingo creates the invitation to an AI interview or a role-play, and a separate sender may then queue an email to that person, before or after the answer comes back (queued means handed over for sending, not delivered, and the sender can skip an invitation; section 4.5). (A CV evaluation is ordered by sending the candidate's cv_link and sends no invitation email: section 4.2.)
Step 1. Create a key. In Settings -> Integrations -> API Keys create a key with the scope Invite candidates (invite_candidates). Add Read hiring data (read_hiring) while you set this up, so that you can look up the ids in step 2, then remove it by editing the key. Only an Owner or an Admin can create keys. Create it as an Owner: that key reaches every assessment and project of your organization (section 2.9).
Step 2. Find the ids. You need the id of the assessment: call GET https://apis.flowmingo.ai/company/integration/hiring/interview-sets/v1 (the curl command in section 1.3, or an HTTP step in your tool) and copy the id of one whose assessment_type is ai_interview or role_play and whose status is 1 (published). An invitation to a draft, inactive or archived assessment is created but not emailed (section 4.5). This recipe sends an interview. A Skill Test, a Technical Test or a Showcase can be sent the same way with the extra rules in sections 4.6 and 4.8 (one assessment per request, an email for every candidate), and a Recording evaluation or Questionnaire cannot be invited through the API (section 4.2). To also put each candidate into a hiring project, copy a project id from GET .../hiring/projects/v1 (section 5.7 if you have many projects) and add "com_project_ids" to the body (see below).
Step 3. Choose the trigger. The form tool's new submission trigger, with a test submission that has your own name and email address in it.
Step 4. Add the HTTP step that sends the invitation.
- Zapier: Webhooks by Zapier, action Custom Request. Method
POST, URLhttps://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1. Put the JSON below in Data, and add the headersContent-Typewith the valueapplication/jsonandX-API-Keywith your key. - Make: HTTP -> Make a request. Method
POST, the same URL, headersX-API-KeyandContent-Type, Body typeRaw, Content typeJSON (application/json), and the JSON below as the request content. - n8n: an HTTP Request node. Method
POST, the same URL, Send Headers withX-API-Key, Send Body on, Body Content TypeJSON, Specify BodyUsing JSON, and the JSON below.
The body (replace the ids with yours, and map the form's answers into name and email):
{
"com_interview_set_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"],
"candidates": [
{ "name": "Alex Doe", "email": "alex@example.com" }
],
"invitation_message": "Hi Alex, thank you for applying. Please complete the short interview in the next 48 hours."
}
Four things go wrong most often:
- Send the body as raw JSON, not as form fields. The lists (
com_interview_set_ids,candidates) cannot be built from separate fields, and a value that arrives as text where Flowmingo expectstrue,falseor a number is refused. That is why the steps above use "Custom Request", "Raw" and "Using JSON". - Leave
send_inviteout. It defaults totrue, which asks for the email to be queued. If you do include it, writetrueorfalsewithout quotes. - Keep the ids exactly as Flowmingo gave them. They must be ids of your own organization, and an id with a typo is refused. To put the candidate into a hiring project as well, add
"com_project_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"](with your own project id) to the body. - Watch for quotes and line breaks in mapped answers. A name or message that contains a double quote or a line break can make the JSON invalid, and so can any other mapped value,
nameandemailincluded. Make sure every mapped value is JSON-escaped (use the tool's JSON or escape function, or build the body as an object instead of as text), and never paste form answers straight into JSON text.
Step 5. Test with your own address. Run the step. You should get 201, with your invitation under results, in invitations (section 4.5). Read the entry for your address before you look at your inbox: an error_message, an invitation_error_messages entry or a warnings entry (the assessment is not published) means that this call did not set up a new email for that address. With none of those, a separate sender may queue the email for sending, shortly before or after the call answers, so allow a few minutes and check the spam folder too. The sender can still skip an invitation (for example when the candidate already has a result for that assessment) or reject the address, an email can be delayed, and neither a 201 nor an empty warnings proves that it arrived.
Errors you may see in the tool's log:
| What you see | What it means | What to do |
|---|---|---|
401 API key is required |
No key reached Flowmingo. | Check the header name X-API-Key, or the Bearer setting. |
401 Invalid API key |
The key is wrong, deleted or disabled, or you pasted the masked version (fl_live_abcd12345.****). A key turned off because its creator left answers 403 (section 11). |
Paste the full key (it is shown only once, when you create it), or create a new key. |
403 API key missing required scope: invite_candidates |
The key cannot invite. | Edit the key and tick Invite candidates. |
400 each value in com_interview_set_ids must be a UUID |
The id is mistyped, or is not a version-4 id from Flowmingo. | Copy it again from GET .../hiring/interview-sets/v1. |
404 Interview set ... not found (or Project ... not found) |
The id is not one of your organization's. | Use an id from your own account. |
400 code assessment_type_not_invitable_via_api |
The assessment is a kind that the API cannot send (a Recording evaluation, Questionnaire or question bank). | Send it from the Flowmingo dashboard, or pick another assessment. |
400 send_invite must be true or false (a JSON boolean, not text) |
send_invite arrived as text. |
Remove it, or write it as true or false without quotes. |
400 code invitation_note_empty, invitation_note_too_long or invitation_note_too_large |
The note (invitation_message) has no text left once unsupported content is removed, has more than 2,500 characters of text, or is over 5,000 characters once unsupported content is removed, formatting and link addresses included. A note over 50,000 characters as sent gets a plain 400 with no code. |
Shorten it, or send plain text, or remove the field; an empty string means no note, and within these limits so does a blank one (section 4.3). |
400 Rate limit exceeded: maximum ... |
You reached a daily cap (5000 invitations for your organization, 1000 for one assessment, with several assessments in one request counted together). | Wait for the next day, or send one request per assessment (section 4.3). |
201, but no email arrives |
The request was accepted, but one candidate may have an error, or the assessment may not be published (a warnings entry), or the sender may have skipped the invitation or delayed the email. |
Open the answer and read results: error_message or invitation_error_messages say why (for example, Email already invited), and warnings lists an assessment that is not published. Allow a few minutes and check the spam folder too: the email may be queued before or after the call answers, may be skipped, and is not guaranteed to be delivered. |
The same form submitted twice normally does not invite a person twice: the second answer is a 201 whose entry for that person carries an error message.
11. Troubleshooting
401 and 403 mean different things, and the message usually tells you which problem you have. Checks run
in order: key present, well-formed, recognised (the secret must match), not turned off, unexpired, creator still here, within the rate limit, correctly scoped, then the projects it may reach. The first failure is the
one reported, so an expired key reports expiry even if something else is also wrong. A caller with a wrong secret is told nothing about the key beyond Invalid API key:
| Status | Message | What it means |
|---|---|---|
| 401 | API key is required |
No key reached us. The header name is wrong, or you sent Authorization: Bearer with a value that is not a Flowmingo key (a Flowmingo key starts with fl_). See section 2.6. Generating a new key will not help. |
| 401 | Invalid API key format |
The key is malformed: it must be the full prefix.secret value, including the .. |
| 401 | Invalid API key |
The key is not recognised, the secret is wrong, or it was deleted or disabled. A key turned off because its creator left answers 403 (below) once the secret is right. A masked value copied from the key list (fl_live_abcd12345.****) also lands here: it is well-formed, but the secret is not the real one. |
| 403 | API key expired |
The key passed its expiry date. Keys created in the dashboard do not expire, so you will not normally see this. |
| 403 | API key missing required scope: <scope> |
The key is valid but lacks that scope. GET /me/v1 lists the scopes the key has, and section 2.3 says which route needs which. |
| 403 | code user_banned |
The person who created the key has been blocked from Flowmingo. Contact support. |
| 403 | The person who created this API key has left the company or no longer has access, so the key can no longer be used. An Owner can take it over in Settings > API keys. |
The creator left and the grace period is over, or the creator could open no hiring project when they left. An Owner or Admin takes the key over; create a new key if nobody can. Code key_creator_left. |
| 403 | This API key was turned off because the person who created it has left the company or no longer has access. An Owner or Admin can take it over and turn it back on in Settings > API keys, or you can create a new key. |
Same cause, and the key was stopped at once because the company had no other Owner left when the creator left (Admins do not count for this). Taking it over does not turn it on: turn it on too. Code key_creator_left. |
| 403 | The person who created this API key cannot open any hiring project, so the key cannot read or change anything. ... |
Give the creator project access, or take the key over. Code key_reaches_nothing. |
| 403 | The person who created this API key does not have access to that hiring project, candidate or assessment result, so this key cannot act on it. |
Use a key whose creator can open it. Code key_out_of_reach. On a partner order route the assessment is one the creator may not send to (section 8.6). |
| 403 | This API key was created by someone limited to some hiring projects, so the request must say which project or interview set it is for. |
Add com_project_ids or com_interview_set_ids (section 4.3). Code key_must_name_target. |
| 403 | The event feed covers the whole company and cannot be narrowed to a person’s projects, so a key created by someone limited to some projects cannot read it. Use a key created by an Owner, or by an admin with access to every project. |
GET /events/v1 needs a key whose creator has access to all hiring projects (section 7). Code key_events_need_all_projects. |
| 403 | The person who created this API key is no longer allowed to create interview sets (it takes an Owner or an Admin), so this key cannot do it any more. Ask an Owner to take the key over in Settings > API keys, or use a key created by an Owner or an Admin. |
A key with create_set, create_project or create_assessment asked to create an interview set (section 3), a hiring project (section 3.5) or an assessment (section 3.6), and its creator is no longer an Owner or an Admin, for example an Admin who was made a Member. The sentence reads "create hiring projects" or "create assessments" on those routes. Code key_creator_role_lost for interview sets, project_create_requires_admin for projects and assessment_create_requires_admin for assessments. Create a new key as an Owner or an Admin: asking an Owner to take the key over does not help while the creator, now a Member, can still open a hiring project, because a take-over is refused then (section 2.10). The key's other scopes keep working inside the creator's projects. |
| 403 | This API key has no creator on record, so it carries nobody's permissions and cannot be used. ... |
Create a new key. Code key_no_creator. |
| 403 | This company account is not active, so its API keys cannot be used. |
The account is archived or closed. Contact support. Code org_not_active. |
| 503 | We could not check which projects this API key may use just now. Nothing was read or changed. Try again in a moment. |
Code key_reach_unavailable. Wait the Retry-After seconds and send the same request again. |
There is no separate "enable API access" setting on an account: a key that exists, is active, has not
expired, was made by someone who is still with the company and can open at least one hiring project (and, for
create_set, create_project and create_assessment, is still an Owner or an Admin), and carries the right scope works.
- 400
com_interview_set_ids must be a list of ids, even for one: ["<id>"](invite): a list field arrived as one text value. Send a list, also for one id. The same rule applies tocom_project_ids,com_job_post_idsandattachment_sys_file_ids. - A route answers 403 although the key works on
/me: the key has no scope for that route. Edit the key in Settings -> Integrations -> API Keys and tick it (section 2.3). - Names or email addresses are missing from results: the key lacks
read_candidates(section 2.3). - A list is shorter than you expect, or a record you know exists answers 404: the key acts with its creator's permissions, so it sees what its creator can open (section 2.9). Compare with what that person sees in Flowmingo.
- The list of tests in Recruitee or Zoho Recruit is empty or shorter than you expect: the key acts with its creator's permissions (section 2.9). Create the key as an Owner.
- A lookup returned the wrong people, or everybody: a misspelt or unsupported query parameter is not refused and can leave the list unfiltered (section 8.3). Use
qto find a candidate, and check the rows before you act on them. - 400 "
external_idmust be plain text of 1 to 128 characters with no control characters. It is stored exactly as sent.": anexternal_idyou sent in a create call, a note, a rating, a shortlist mark, an added application or a Zoho order, or in a filter (?external_id=), is empty, only whitespace, over 128 characters, holds a control character or half of a surrogate pair, is not a JSON string (the number48213is refused: send"48213"), or, in a filter, is given twice. A filter written with brackets (?external_id[]=a) is a400too, with a message that says brackets are not accepted: send it once, as?external_id=a. On the invite route the code isexternal_id_invalid, and on an entry of the recording create routeinvalid_external_id. Nothing was created (section 8.8). - A lookup by
external_idreturned everybody, or nobody: everybody means the parameter is misspelt (externalId,external-id) and was ignored (section 8.3), or the server does not have the November 2026 release yet and ignores the filter; check that each row'sexternal_idis the one you asked for. Nobody means the match is exact: case-sensitive, no wildcards, no trimming. Check the record carries an id (records made before the field existed, and records a candidate or a background job creates, holdnull), that your key's creator can open it (section 2.9), and, for a project made seconds ago, that the project list is read from a replica and can lag by a few seconds (section 3.5.12 reads the primary). - An
external_idI sent comes backnull, or another id is there: the route does not take one (the Greenhouse and Recruitee order routes, the assessments a create-project call makes); the invite named a person, an application or an invitation that already held a different id, so it was kept and the result saysexternal_id_kept_existing,application_external_id_kept_existingorinvitation_external_id_kept_existing(send the same id again to confirm it, or read what is stored); the assessment has been deleted; or you read in the moment between the call and the write of the id (section 8.8). - 400 "Too many query parameters: only the first 1000 are read, so the external_id filter could not be checked and nothing was filtered." (a list): the query string of one of the lists that take
external_id, or of the results list, has more than 1,000 parameters. The server reads only the first 1,000, so it cannot tell whether the filter was sent once and without brackets, and refuses instead of returning a list that may be unfiltered. This happens whether or notexternal_idis among the parameters. Send only the parameters the route lists, each once (section 8.3). A query of exactly 1,000 is read in full. - 400 "The external_id filter is not available on the results list yet" (results list):
GET /hiring/submissions/v1cannot be filtered byexternal_idyet, and says so instead of returning everything. Filter bycom_interview_set_idortal_candidate_id, or read a result by its id: each result shows its ownexternal_id(sections 5.2 and 8.8). - 404
Interview set ... not found,Project ... not foundorJob post ... not found: the id in your request is not one of your organization's, or the record was deleted. The message names the id. When a request names several ids, one wrong id refuses the whole request and nothing is created. - 429
api_key_rate_limited: the key made more than 600 requests in one UTC minute. Nothing was done, so send the same request again after the seconds themessageor theRetry-Afterheader gives; slow the loop down, or watchX-RateLimit-Remaining(section 8.5). - 403
project_create_requires_admin, 409idempotency_key_reusedoridempotency_request_in_progress, 429project_create_limit_reached, 503project_create_limit_unavailable(create a project): the person who created the key is not an Owner or Admin; theidempotency_keywas used with a different body, or its first call is still being worked on; the limit of 10 a minute or 300 a day is reached; or a check could not be made. None of them creates a project. After the409idempotency_request_in_progressthe first call is still working, so wait and send the same request again to get its project (section 3.5). - The project was created but
job_postisnull(create a project): readjob_post_error. With a code in it, the job post could not be built (section 3.5.2): add the post in the dashboard. Repeating the call with the same key returns the same project. Withjob_post_errornull, a post existed and was deleted in the dashboard since; add one if you need it. - A create-project call answers
202, or takes much longer than before: a call that omitsassessmentsalso makes a CV Evaluation and an AI Interview, and waits up to about 45 seconds for them; one that names a Technical Test answers202while that test is still being made. Send the same request again with the sameidempotency_keyto read how far they got, or sendassessments: []to create the project alone (section 3.5.5). - A create-project answer has
partial_failuretrue: the project exists and is complete; one entry ofassessmentsisfailed. Read itsfailure: withretryabletruesend the same request again with the same key, otherwise follow the remedy for its code in section 3.6.12: a request of its own may not resolve it (an existing CV Evaluation still conflicts, and a key whose creator is no longer an Owner or Admin is still refused). - 400
job_post_name_unknown(create a project): a name injob_postis not one the dashboard shows. The message lists the accepted names; correct it and send the request again. Nothing was created (section 3.5.6). The otherjob_post_codes and503job_post_catalogue_unavailableare in the same section. missing_detailslists something (create a project): the saved job post lacks that detail.job_postis read only when the project is created, so complete the post in the dashboard (section 3.5.7).- 400
stage_name_unknown,stage_name_duplicated,stage_field_not_supported, or "each entry of stages needs ..." (create a project):stagesis a list of at most four entries, each exactly{ "name": ..., "external_id": ... }, the name one ofUnassigned,Shortlisted,In ProgressandRejected, spelled exactly like that (case matters), each stage once, with no other key (constructor,__proto__andprototypecount as keys). Nothing was created: correct the entry and send the request again (section 3.5.11). A key thatjob_postdoes not take has the codejob_post_field_not_supported(section 3.5.6). - 409
idempotency_key_reusedafter you added or changed anexternal_id(create a project, an assessment or an interview set): the ids are part of the request. The same key with another id, an id added or an id dropped is a different request; send the original body, or use a new key (sections 3.5.3, 3.6.2 and 3.2.6). - 404
project_request_not_found(read a create-project call by its key): no call of your organization used this key, the key is malformed or belongs to another organization, or the call's project is one that the creator of the key cannot open. A call that is still running, or that ended before its project was saved, has no record yet, so the project may also simply not be saved (yet): send the same create request again with the same key and the same body (section 3.5.12). An empty key, or one with an invalid percent-escape such as%ZZ, never reaches the route: the web layer answers404or400with nocode. - 403
key_reaches_nothingonGET /hiring/projects/requests/{idempotency_key}/v1, although the create call worked: the person who created the key can no longer open any hiring project (the last one they were given was deleted or taken from them), so every route refuses the key. Give them project access, or take the key over (section 2.10). A creator limited to some projects reads the request of a deleted project as the404above, not asdeletedtrue(section 3.5.12). - A create-assessment call answered
202(create an assessment): it is not an error. The assessment is still being made. Ask again with the sameidempotency_key, or read the status route, until its status isready(sections 3.6.2 and 3.6.3). A new key would make a second assessment. - 403
assessment_create_requires_admin, 404project_not_found, 409idempotency_key_reused, 429assessment_create_limit_reachedorassessment_capacity_busy, 503assessment_create_limit_unavailable(create an assessment): the person who created the key, or the person the request runs as (the user the key is bound to, when it has one, otherwise the person who created the key), is not an Owner or Admin (the code does not say which of the two); theproject_idcannot be used (an id taken fromGET /hiring/projects/v1can be refused when the project is archived, section 5.7; a403with the codekey_out_of_reachmeans the creator is limited to some projects and the project is not theirs); the key was used with a different body; the generation budget is spent or generation is busy; or the budget could not be checked. None of them starts any work (sections 3.6.11 and 3.6.12). - 404
project_not_foundonGET /assessments/requests/{idempotency_key}/v1(create an assessment): the request was made for a project that the key's creator can no longer open (it was archived or deleted, or taken out of their access). This does not mean nothing was made: an assessment may exist. Ask a colleague who can open every project, such as an Owner, to look in the assessments list, by yourexternal_idif you sent one (section 3.6.3). 502or500with a code startingassessment_ortechnical_test_(create an assessment): the assessment was accepted and could not be finished. The table in section 3.6.12 says for each code whether sending the same request again with the same key can finish it; fortechnical_test_not_publishableuse a new key.- A Technical Test has fewer problems than I asked for: a problem that does not pass the sandbox check is thrown away.
generationin the answer says how many wererequested,keptanddiscarded(section 3.6.10). - 400
assessment_type_not_invitable_via_api(invite): the request names a Recording evaluation, Questionnaire or question bank. Only AI interviews, role-plays, Skill Tests, Technical Tests and Showcases can be invited, and CV evaluations ordered, through the API (section 4.2). Nothing was created. - 400 with a code beginning
send_at_, orsend_day_full(invite):send_atbreaks one of the rules of section 4.9: it is for an AI interview or a role-play only, needssend_invitetrue, an assessment named in the request, an email for every candidate, and a time with an offset orZfrom 2 minutes to 90 days from now.send_day_fullsays the day you chose would go over a limit of 1,000 invitations for the assessment or 5,000 for your organization. Choose another day. Nothing was created. - 409
invitation_not_scheduled(cancel a scheduled invitation): a scheduled invitation can be cancelled only while its due time is still in the future and the sender has not picked it up. This one is already due, was sent or is being sent right now, was started, or was already cancelled. Nothing was changed (section 4.9.1). - 409
reminders_not_stoppable,reminders_managed_by_sequence,reminders_managed_by_bundle,reminders_cancelledorreminders_busy, or 404invitation_not_found(pause or resume an invitation's reminders): only the reminders of an AI interview or a role-play invitation can be paused or resumed; an invitation that is sent only by an email sequence has its follow-ups paused on the sequence, in the dashboard (when Flowmingo's own email also sends it, the pause call pauses Flowmingo's reminders and answers200); an invitation that is one assessment of a multi-assessment bundle that may still be sending reminders (not expired, not finished, within 10 days of its email; one such bundle is enough, even if the invitation also belongs to a bundle that has ended) has its reminders sent by the bundle, and they cannot be paused from here (a live bundle is checked before the kind of assessment and the sequence);reminders_cancelledmeans a recruiter cancelled the invitation email in the dashboard, which is final and cannot be resumed;reminders_busymeans the invitation was being sent or changed at that moment, so repeat the call; and the404is one answer for an unknown id, another company's invitation, an invitation you cancelled through the API, one outside the projects the key's creator can open, and every invitation during the grace period after the key's creator has left the company (section 2.10). For an invitation made without naming a project, a key whose creator is limited to some projects gets the404unless it is the key that made the invitation. A repeat of a call that worked can also meet these answers if the invitation joined a live multi-assessment bundle, was cancelled, the creator's access changed or the invitation became a sequence-only one in the meantime. Nothing was changed (section 4.10). - A reminder still arrived after the stop-reminders call answered
200: the call pauses only the invitation's own two automatic reminders. A reminder the sender had already picked up when you called may still go out, and so may a mailbox copy that was already queued. A teammate or your own system may have resumed the reminders after your call (the resume call, or Resume in the dashboard): a resume before a reminder's due day lets it go. A separate email sequence that follows the invitation is not paused by this call, although it answers200for such an invitation: check the sequence in the dashboard. A candidate who started the interview and did not finish can also get a separate "complete your assessment" email, which this call does not pause. An invitation that is one assessment of a multi-assessment bundle that may still be sending reminders is not paused at all: it answers409reminders_managed_by_bundle(section 4.10). And if a recruiter restarted the invitation's reminders in the dashboard after your call (for example with "Send Flowmingo's invitation instead"), they are back on: call the pause again, which pauses the new ones and answers200withalreadyfalse. - A repeat of the stop-reminders call answered
200withalreadyfalsealthough it had worked before: the reminders were resumed or restarted since, by the resume call, by a recruiter's Resume in the dashboard, or by "Send Flowmingo's invitation instead", which gave the invitation its reminders back. The call paused the new reminders, and a further repeat answersalreadytrueagain. Nothing is wrong (section 4.10). - A reminder I resumed never went: its due day had already ended while its channel was paused (the whole UTC calendar day counts, so a resume on the due day still sends it), so that reminder and every later one of that channel were skipped. The resume answer lists the first skipped reminder of each channel in
skipped. A skipped reminder is not sent later: dates do not move (section 4.10.1). - An invitation was not sent although it was due: if a recruiter paused it in the dashboard before it went out, the invitation is held until it is resumed. Resume it in the dashboard or with the resume call (section 4.10.1) and it goes at the next send slot. A pause made through the API never holds the invitation email back (section 4.10).
- 409
job_post_changed, 500job_post_not_saved, 503job_post_outcome_unknownor 503project_outcome_unknown(edit a project): the project fields of the call may already be saved, and themessagesays which (withproject_outcome_unknown, the project write itself ended in an error that does not say whether it went through, and the job post was not attempted).job_post_changedmeans that the job post was edited, or switched on or off, while the call was saving it, so its words were not changed: read the project and send thejob_postpart again.job_post_not_savedmeans that the words were not saved.job_post_outcome_unknownmeans that they may or may not have been: read the project and comparejob_post.sent_titleandjob_post.sent_description(of the project's newest job post; the words as stored, before any rewording) with what you sent, after removing leading and trailing blanks from it. When you were changing the description, the message also says that the title-only-draft marker may still be set if the description was saved, and that sending the same description again will not clear it: change the description once more, or clear the marker in the dashboard. Then send the same body again if no one has changed the same fields in the meantime (section 3.5.14). - The job post shows words I did not send (edit a project): the AI content check reworded them, and the job post shows its checked copy.
job_post.differs_from_sentistrueandjob_post.descriptionorjob_post.titleholds the checked wording. The warningjob_post_rewritten_by_reviewreports a rewrite of the title or the description that your edit changed; a rewrite of the other field shows only indiffers_from_sent. Your words are stored after leading and trailing blanks are removed from them (job_post.sent_titleandjob_post.sent_descriptionshow the stored words), but this route cannot make the job post display them (section 3.5.14). - 429
job_post_edit_budget_reachedor 503job_post_edit_budget_unavailable(edit a project): an edit that changes the words of a job post that is switched on is checked by AI, and your organization may make 50 such edits per UTC day across all its keys. The 51st answers429with the seconds until 00:00 UTC in theRetry-Afterheader, and nothing is changed. When the allowance could not be read just now the call answers503, fails closed and changes nothing: try again in a moment. A call refused because the job post is switched off or has expired (job_post_is_off), an edit of project fields only and a call that changes no word do not use the allowance (section 3.5.14). - 403
key_out_of_reachon an edit although the key works for other calls (edit a project): the body has ajob_postobject, even an empty{}, and the key's creator has left the company (the key is in its grace period), or the body carries job post words and their role no longer lets them edit the job post. Nothing was written. Send the project fields without ajob_postobject, or use a key whose creator is still here (sections 2.10 and 3.5.14). - An edit answers
reviewpendingand the warningjob_post_awaiting_review(edit a project): a check of the words this call changed had not been seen to finish when the call answered (also when the check cannot run, for example because the checking service is busy). The answer sayspendingfor the job post the call edited, when it is the job post the answer shows (the project's newest: if a newer one exists, or the edited one was deleted, the answer shows another job post, or none, with its ownreview). A later read reports the last check, so a job post that was alreadyflaggedorrejectedbefore your edit can keep showing that earlier result for a while.reviewis a best-effort reading of the last check: a check of older words that was still running when you edited can finish afterwards, and the page can then keep older checked wording. To know what the page shows, read the project and comparetitleanddescription(what it displays) withsent_titleandsent_description(what you sent, trimmed); if they are not what you expect, change the words (even by one character; you can change them back afterwards) or change them in the dashboard: sending or saving the same words again starts no check, and each change made through this route to the words of a switched-on job post uses one unit of the daily allowance (sections 3.5.13 and 3.5.14). - 409
job_post_is_off(edit a project): the call would change the words of a job post that is switched off or has expired. Nothing was written, and the project fields of the same body were not written either. Switch the job post on first (section 3.7.2) and then edit it, or edit it in the dashboard; a call with project fields only is not refused (section 3.5.14). - The edit's answer has
projectnull(edit a project): the key holdscreate_projectbut notread_hiring, so the answer does not show the project. The edit itself was made, andwarningsstill say what happened to the job post's words. Read the project with a key that holdsread_hiring(sections 3.5.13 and 3.5.14). - The warning
job_post_may_go_offline_if_rejected(edit a project): the job post was switched on and approved but has never been posted (it has no posting date, as with some job posts made in the dashboard and switched on later). If the check refuses your new words, it is taken offline instead of being kept serving, so read the project after the edit and look atreviewandpublic_state. The warning is made only while the page of the job post you edited is open when the answer is built: never for a project that is a draft or archived, not for a job post switched off after the save, and not when the check had already refused the words within the wait (reviewis thenrejected, the page is not open, andjob_post_review_rejectedis made instead) (sections 3.5.14 and 3.7.1). - 400
project_field_not_supportedafter you addedexpected_updated_at(edit a project): the edit has no version check. Remove the field: the call sets the fields you send to the values you send, and the last write wins (section 3.5.14). - 400
showcase.invite.set_not_publishedorshowcase.invite.set_archived(invite): the Showcase is not switched on, or is archived. Publish it in the Flowmingo dashboard first (section 4.8.1). - A Showcase row says
delivery_in_progressorfailed:delivery_unknown:delivery_in_progressmeans another call is sending that invitation right now, and this call sent no second email. Afterfailed:delivery_unknownthe email service may have sent it, and the API will not send it again: check the invitation in the dashboard, and resend it there if the candidate did not get it (section 4.8.2). - A job post is
activebut its page does not open: readpublic_stateonGET /hiring/job-posts/{id}/publication/v1.awaiting_reviewmeans the AI content check has not approved the post yet (a post made with a project is checked about once a minute, so read again after a minute),rejectedthat it refused it (seereview_rejection_reason), andproject_not_openthat the hiring project is a draft or archived (section 3.7.1). - 409
project_not_open, 409job_post_edited_while_off, 422publish_review_rejected, 429publish_review_limit_reachedor 503job_post_review_unavailable(publish a job post): the hiring project is not active (open it withPOST /hiring/projects/{id}/status/v1); the post was approved, then edited while it was off (or edited while on, before its check finished, and then switched off), so switch it on once in the dashboard; the AI check refused the post (readrejection_reason, change the text in the dashboard, publish again; the edit route of section 3.5.14 does not change the words of a job post that is switched off); the 50 checks of the UTC day are used; or the check could not finish. After a check that fails once the post is switched on, Flowmingo tries to switch it back off. If that fails too, themessagesays the post may still be live: open it in the dashboard and switch it off (section 3.7.2). - 409
company_profile_required, 409job_post_not_activeor 422boost_review_rejected(boost a job post): a boost needs a company logo and description, a post that is switched on, and a post the AI check accepts. Add the logo and description in the company profile, publish the post first, or change the text in the dashboard (section 3.7.4). - 400 with a code beginning
assessment_test_,project_required,project_ambiguousorapi_key_has_no_acting_user(invite): a Skill Test, Technical Test or Showcase request breaks one of its rules: one test per request, one project, an email for every candidate and at most 100, no value inats_candidate_id,metadataorpractice_mode, and a key that belongs to a person (section 4.6.1 and 4.6.2). Nothing was created.project_requiredright after you made the test through the API usually means it was made withoutproject_id, so it is attached to no project: attach it in the Flowmingo dashboard, or make it again withproject_id(section 3.6.4). - 403
recording_requires_scale, 400consent_required, 404set_not_foundor arejectedentry such asupload_not_completed(hand over recordings): the organization is below the Scale plan;consent_confirmedis not the JSON valuetrue; there is no such assessment in your organization; or the file was not uploaded to itssigned_urlbefore step 2. A403with the codekey_out_of_reachon step 2 means the assessment is not one the key's creator may open (section 4.7.1). The codes of whole-call refusals and of single entries are in section 4.7.4. - The transcript and recording links route returned no transcript and no clips (read a transcript): read
availabilityandreason(section 5.10).held: the report is not released to your company yet (the candidate's improvement window is open, or the candidate has not submitted the interview; section 5.5).processing: ask again later, and after a minute forrecently_changed.not_available: thereasonsays whether this kind of result is not read by this route, whether the analysis did not finish, or whether your plan's recording limit could not be confirmed (lock_state_unavailable: ask again). - A clip has no
url(read a transcript): read the clip'sstatusandreason(section 5.10).lockedmeans your plan hides the recording.not_availablewithrecording_not_verifiedorlink_not_servablemeans the file did not pass the system's checks as an answer recording on our storage, so no link was made. The system works this out again from the stored address and its records on each call, so the answer changes only if they change.upload_never_arrivedis the system's own judgment, made from the stored link, that the upload never arrived; the file itself is not looked up. - A clip is
readybut itsurldoes not open (read a transcript):readysays only that a link was produced; the file is not fetched when the link is made, so it may be missing. A link is a secret and is meant to be fetched soon: fetch a new one each time you need it (section 5.10). - 400
technical_test_invite.duplicate_recipient(invite): two requests for the same address reached the same Technical Test at the same moment. This call emailed nobody; invitations created earlier in the same request remain. Repeat the call after a moment (section 4.6.5). - 409
skill_test_invite.already_invited_to_test(invite): a candidate in a Skill Test request already holds an invitation to that test through another hiring project. Nothing was emailed, but candidates and pipeline entries for the request were created and invitations created for the rows before the conflict remain, not emailed by this call. Remove that candidate from the request and repeat it (section 4.6.5). - 429
assessment_test_invite_limit_reachedor 503assessment_test_invite_limit_unavailable(invite): the daily limit for a Skill Test, Technical Test or Showcase is reached, or could not be checked. Nothing was sent. After the 429 try again after 00:00 UTC; after the 503 try again in a moment (section 4.6.6). - A Skill Test or Technical Test row says
reused_existingtrue: the invitation already existed when this row was handled. Readstatusandemail_delivery.queuedmeans this call handed it to the mail service (for example an earlier call created it withsend_invitefalse); nothing needs sending from the dashboard. It does not prove that no earlier copy went out: a mail that went out before a failed status write can be followed by a later call that saysqueuedagain.already_queuedusually means it was emailed earlier; if the invitation was deleted while the call ran, it can also appear although no email was handed over, so do not tell anyone that it proves an earlier send.skipped_already_started,skipped_attempt_in_progressorskipped_not_unstartedmean the candidate has moved on, or the invitation is closed (cancelled, expired, used up or finished): the API never revives a closed invitation, so to give that candidate another go, send it again from the Flowmingo dashboard (section 4.6.5). - A Skill Test or Technical Test row says
delivery_in_progressorfailed:followed by a reason: the answer does not say whether the email went out (the one exception isfailed:skill_test_invite.recipient_unsubscribedorfailed:technical_test_invite.recipient_unsubscribed: the person opted out of email, nothing was sent, and repeating the call will not send it while the person stays opted out). Check the invitation in the Flowmingo dashboard, or ask the candidate, before you repeat the call; if you cannot tell, wait about ten minutes and repeat it (section 4.6.5). - 400
... must be a UUID(invite): an assessment, project or job post id is not a version-4 UUID. Copy the id from the API instead of typing it. - 400 naming a candidate row, for example
candidates[2]: email must be an emailorcandidates[2] must be an object(invite, Skill Test or Technical Test): that candidate row is malformed (see section 4.6.1 for what a row may contain). Nothing was created. Fix the named row and repeat the call. - 400
send_invite must be true or false (a JSON boolean, not text)(invite):send_invitearrived as text, for example"false"from a tool that sends every field as a string. Send a real JSONtrueorfalse, or leave the field out. - 400
rating must be a number (a JSON number, not text), orshortlisted must be true or false (a JSON boolean, not text): the same rule on the rate, note and shortlist routes (section 6). - The invite answered
201but the candidate got no email (invite): readwarningson the candidate's entry. The codeassessment_not_activemeans the AI interview or role-play is a draft, inactive or archived: the invitation exists and becomes eligible for the sender once the assessment is published or active again, but the sender does not send it automatically when that is more than 7 days after the invitation was due (normally the moment of the call); an explicit resend can make it eligible again (section 4.5). Eligible does not guarantee that an email is queued or delivered. Publish the assessment, then check that itsstatusis1inGET /hiring/interview-sets/v1(section 5.6). Also read the message fields of the entry (section 4.5), and check thatsend_invitewas notfalse. When there is nowarningseither, the email may have failed for another reason (an address on the unsubscribe list, a failed address check, a refusal by the mail provider): see theinvitation.status.updateevent and the events feed in section 4.5. - 400
attachment_not_yours(invite): a file named inattachment_sys_file_idswas not uploaded by the person the key acts for, and neither a template that your organization shares nor a private template of that same person carries it. Nothing was created. Leave the field out, or name a file that is available to you (section 4.3). - 400
Rate limit exceeded: maximum ... invitations ...(invite): you reached the daily cap of 5000 invitations for your organization or 1000 for one assessment, where a request that names several assessments counts them together (section 4.3). - Validation errors (invite): Ensure
candidatesis non-empty and each candidate has enough data for your desired flow (invite vs CV-entry queue). A201answer can still carry a message for a single candidate: readresults(section 4.5). - 400
external_id_requires_email,application_external_id_requires_project,application_external_id_ambiguous_project,invitation_external_id_requires_inviteorinvitation_external_id_ambiguous_assessment(invite): the ids of a candidate need something to be kept on.external_id,application_external_idandinvitation_external_idneed anemail(a candidate sent with only a CV link has no record to keep them on).application_external_idneeds exactly one hiring project incom_project_ids; send one request per project.invitation_external_idneeds exactly one assessment incom_interview_set_idsandsend_invitetrue; send one request per assessment. "Exactly one" counts every distinct id the request names, in the list and in the deprecated single field together:com_project_ids: [A]besidecom_project_id: Bnames two projects. A Skill Test or Technical Test request meets two of these codes: a candidate with an id and noemailgetsexternal_id_requires_email(a row with no id and noemailkeepsassessment_test_email_required), andinvitation_external_idwithsend_invitefalsegetsinvitation_external_id_requires_invite. Themessagenames the row and the field, and nothing was created (sections 4.3, 4.6.1 and 8.8). - 400
external_id_not_supported_when_staging(stage an invitation for approval): a candidate row ofPOST /approvals/invite/v1carries anexternal_id,application_external_idorinvitation_external_idthat is notnull. A staged invitation keeps only each recipient's name and email, so it cannot keep your ids. Themessagenames the row and the field, and nothing was staged. Leave the ids out, or send the invitation toPOST /interview/candidate/invite/v1, which stores them (sections 4.3.1 and 8.8). - 400
candidate_field_too_longorcandidate_field_invalid(invite): a value the database cannot keep. Themessagenames the field, for examplecandidates[2].ats_candidate_id, and never repeats the value. Nothing was created: shorten the value, or remove the NUL (zero) character, or the half of a surrogate pair (in anats_candidate_idor anemail, or in thesource,nameorthumb_file_linkof a candidate sent with anats_candidate_id), and send the request again.ats_candidate_id,email,firstname,lastnameandnamecan have 250 characters,thumb_file_link2048 (not checked on a Skill Test or a Technical Test, which do not use a thumbnail), andcv_link1536 on a request for an AI Interview, a Role-play or a CV evaluation (2048 for a Skill Test or a Technical Test). The email is measured in lower case, the way it is kept; on a Skill Test or a Technical Test it is measured after the spaces around it are removed, because those two ignore them.invitation_messagecannot hold a NUL character either (section 4.3). - A result in the invite answer says "This step failed because of an internal error (reference ...)": something failed on our side for that step of that candidate, and the text no longer shows what. Try again later; if it keeps failing, contact Flowmingo support and quote the reference (section 4.5).
- 400
question_mode_required(set): an AI interview with no interview question must say how it gets its questions. Send"question_mode": "realtime"(Let AI decide during interview) or"predefined"(I'll set the questions) together with at least one interview question iniai_questions; see section 3.2. - 400
interview_questions_required(set): apredefinedAI interview needs at least oneiai_questionsentry whosequestion_modeisinterview. Pre-screening entries do not count. - 503
question_generation_failed(set): arealtimerequest could not get its questions prepared just now. Nothing was created; send the same request again. - 409
idempotency_request_in_progress,idempotency_key_reusedorinterview_set_was_deleted, 400idempotency_key_not_supported_with_addons(set): these meet only a request that sends anidempotency_key. In progress means a request with this key is still being processed: wait theRetry-Afterseconds (10) and send the same request again to get the assessment. Reused means the key was used with a different request, or the assessment it made cannot be opened by this API key's creator (the message is the same for both). Deleted means the assessment the key made has been deleted: use a new key. A key cannot be sent withaddons. A request that failed before its assessment was saved does not use its key up (section 3.2.6). - Validation errors (set):
titleis required. Iniai_questions[],question_modeandquestion_typeare optional (omit them and you get aninterviewquestion), but if you DO send one it must be a valid value (pre_screening|interview, andtext|checkbox|radio|file). The set-levelquestion_mode(next totitle) isrealtime|predefined.question_typeonly takes effect on apre_screeningquestion. Also check max-length limits on string fields, thatset_type,interview_duration,number_of_retakesandpriorityare JSON numbers, and thatproject_id(if sent) is a valid UUID owned by your organization. - 400
Your plan allows up to N minutes(set):interview_durationis longer than your plan allows for one interview: 20 minutes on Free, 30 on Starter, 60 on Growth and Scale, and no plan limit on Enterprise. Send a shorter duration, or upgrade your plan. - Evaluation criteria look wrong or missing: check
cfg_importance_idagainst the four IDs in section 3.2.2. A value outside that set is stored as244(Good To Have) rather than rejected. - Duplicate ATS IDs in one payload: Ensure
candidates[].ats_candidate_idvalues are unique per request. - A recording entry is
rejectedwithduplicate_not_accessibleorduplicate_read_failed: the entry'sclient_refalready created a submission. Withduplicate_not_accessiblethe person who created the key can no longer open that submission, or it was deleted since, so nothing was created and its details are not shown: use a newclient_reffor a new recording. Withduplicate_read_failedthe details could not be read just now and nothing was created again: send the same entry again in a moment and it answersduplicate. Neither carries asubmission_idor anexternal_id(section 4.7). - 409
order_needs_reconciliationor 503order_in_progress(partner order routes): the order cannot be confirmed (the invitation may or may not have gone out: a send failed after it was saved, or the request that placed the order has recorded no result for more than two minutes), or another request is still sending it. The 409 comes back on every retry and nothing is sent again: give theorder_idin the body to Flowmingo support, who confirm the order. For the 503, send the same request again in a moment and you get the same order and no second invitation. The status reads (GET /ats/zoho-recruit/orders/{id}andGET /ats/greenhouse/test_status) answer the same two codes instead of a status: the 503 at once, without waiting, while the order's first request is less than two minutes old, and the 409 with theorder_idfor a kept order or one with no result after two minutes. Neither is a failed order: ask again later (sections 8.6 and 9.6). - 400 "Validation failed (uuid is expected)" (
GET /ats/greenhouse/test_status):partner_interview_idis not a UUID. Send the id the send call returned. A missing value is a400too ("partner_interview_idis required"). - A result has no score: see section 5.5 (the result may be held while the candidate can still improve it), and the score table in section 5.3 (a showcase and a questionnaire are never scored).
- A technical test event has no score, or the score I read changed: this is expected. The
assessment.completedevent for a technical test never carries a score, and itsoverall_scorecan change after the attempt ended while code answers are graded (section 5.5). Read the result again later and keep the latest value. - Webhook problems: see the Webhook Issues section of the webhook guide.
12. Changelog
This section lists changes that can affect an integration. Fields are added to responses and events over time without being listed here (section 8.7).
November 2026
- The invite route refuses half of a surrogate pair in an
ats_candidate_idor anemailwith a coded400(section 4.3.2). From 11 November 2026 a candidate whoseats_candidate_idoremailholds half of a surrogate pair (for example the lone code unit U+D800, sent as a JSON unicode escape) is refused with the codecandidate_field_invalidon a request for an AI interview, a role-play or a CV evaluation, and so is anemailon a request for a Skill Test or a Technical Test, where it used to answer500. The message names the field, for examplecandidates[2].email, and nothing is created. Before, such a value on a request for an AI interview, a role-play or a CV evaluation was accepted, and it was saved as a different text from the one you sent. This can break an integration in one case: such a request is now a400for the whole request. - A new route reads counts of what a candidate's browser, camera and microphone recorded during a finished Skill Test or Technical Test, and a new opt-in key scope,
read_integrity, opens it (section 5.13). From 11 November 2026GET /hiring/submissions/{id}/integrity/v1(scopesread_hiring,read_candidatesandread_integrity) returns, for one finished Skill Test or Technical Test, counts of what was recorded and what was being watched. It gives counts and coverage only: no recording, no image, no verdict and no score, and Flowmingo does not judge whether anyone broke a rule. It needs a key whose creator is currently an Owner or an Admin; a key that does not meet this is refused with a403whose body says why. A result of any other kind is normally refused with a400and the codeassessment_type_not_supported. A key that does not holdread_integritygets403.GET /me/v1lists the scope on a key that has it, and the MCP server has no tool for the route. With it the public API has 77 operations (it had 76). Existing keys do not have the new scope, so existing calls are unchanged. - A new webhook event,
approval.status.update, tells you how a decision on an approval went (webhook guide, and sections 9.7 and 7 of this guide). From 11 November 2026 Flowmingo sends it when a person decides a change that one of your API keys or your AI assistant asked for and that waited for approval.data.statusisapproved(the change was carried out),declined(a person cancelled it on the approval page before it expired) orfailed(sent only for the deletion of a hiring project that was closed without being carried out).dataischange_id,action(invite,reject,move_stage,remove_candidatesordelete_project),status,decided_at,approved_at,interruptedandoutcome;interruptedis alwaysfalseandoutcomeis alwaysnullfor now (both are reserved), and nothing names a candidate, a recipient, the person who decided, the key or a project. There is no event for an expiry, for a withdrawal by the key, for a replaced request, for an undo or for the Copilot. Flowmingo makes the event once and does not make it again if it could not be made, so it can be missing: it is a hint, andGET /approvals/{id}/v1is the record. Every active endpoint subscribed to All Events starts receiving the new event type, and the Select specific events list offers it. The same event is inGET /events/v1for every key that can read the feed, not only the key that asked. Existing calls and events are unchanged. A receiver that answers an error to an event type it does not know will see failures until it ignores unknown types and answers2xx(webhook guide, section 2). - Four new routes read your email sequences, and a new opt-in key scope,
read_sequences, opens them (section 5.12). From 11 November 2026 the API has four routes that read your email sequences:GET /sequences/v1(the library),GET /sequences/{id}/v1(one sequence with its emails and stop rules),GET /sequences/{id}/results/v1(the numbers of one sequence) andGET /sequences/{id}/activation-preview/v1(what switching a sequence on would do). These four routes only read: none of them creates or changes a sequence, switches one on or off, or adds anybody to one, and they do not give how many people replied or how many addresses bounced.people,started_assessment_within_7dandunsubscribedin the results count enrolments, not individual people;people_emailedcounts candidacies andsent_per_stepcounts emails. A key that does not holdread_sequencesgets403. Email sequences must be open for your company (otherwise404with the codesequences_not_available), a key whose creator has left the company is refused, normally with403, even during the grace period, and your organisation may spend 30 units a minute by default on these reads (429sequence_reads_rate_limitedpast that).GET /me/v1is unchanged. With the five tracking-link routes of the next entry, the public API now has 76 operations (it had 67). Existing keys do not have the new scope, so existing calls are unchanged. - Five new routes make, change, delete and list tracking links, and a new opt-in key scope,
manage_tracking_links, opens the first three and the source list (section 3.8). From 11 November 2026 the API can manage tracking links. They are the custom share links of Settings: an address of a job post, a set of job posts or your careers page with?tl=<token>.POST /tracking-links/v1,PATCH /tracking-links/{id}/v1andDELETE /tracking-links/{id}/v1needmanage_tracking_links;GET /tracking-links/v1needsread_hiringandread_candidates;GET /tracking-links/sources/v1needsmanage_tracking_linksandread_candidates, because the text people typed into a link and the names of sources can name an applicant or a referrer. Making a link needs the Growth plan, and the key's creator must be an Owner or an Admin to make, change or delete a link. A link credits an application only while it is switched on and its target is open, and itsaddresssays only where it points. Company-wide limits apply: 30 changes a UTC minute and 300 a UTC day, 30 reads a UTC minute, and 200 links per hiring project and 2,000 per company (a switched-off link counts). Existing keys do not have the new scope, so existing calls are unchanged. TheGET /me/v1answer lists the scope on a key that has it. - A route answers a question about your hiring data written in plain words (section 5.11), and
GET /me/v1shows a hint about whether your key can use it.POST /data/ask/v1(scoperead_hiring) takes aquestionand an optionaltimezone, and answers with the rows and a working that says how the answer was reached. It answers as the person who created the key, from what that person can open in Flowmingo, and it follows their access when it changes. Flowmingo switched it on for every company on 3 November 2026, and AI assistants have been using it through the MCP server since; this guide listed neither until now. A refusal, a busy answer and an unavailable one are200s with astatus(never an answer of zero), and a company may ask 20 questions a minute and 1,000 a day (429data_rate_limitedpast that).GET /me/v1gainsfeatures.ask_data, a hint that istruewhen asking is on for your company and the key carriesread_hiring. For assistants: when a session is offered theask_datatool, it is normally offered in place of six read tools (hiring_overview,top_candidates,list_results,list_pipeline,list_upcoming_interviewsandcompare_candidates).list_pipelinestays for a session that can use a tool that moves, rejects or removes applications, andlist_resultsfor one that can use a tool that adds a note, a rating or a shortlist mark, because those tools need the ids the lists carry. Which tools an assistant has is decided when it connects (switching asking off can change them during a session). The MCP guide at flowmingo.ai/integration/mcp lists the tool. Existing calls keep working. A client that rejects fields it does not know will see the newfeaturesfield onGET /me/v1, and an assistant that relies on one of the six tools may no longer be offered it. The public API then had 67 operations. - A call that names more than 20,000 candidates is refused with a code, before anything is created (sections 4.3, 8.4 and 8.5).
POST /interview/candidate/invite/v1answers400with the codetoo_many_candidateswhen a call other than a Skill Test, Technical Test or Showcase one names more than 20,000 candidates, and nothing is created, unless an earlier check, such as your key's request limits, refuses it first. The limit is not new (it has been enforced since 5 November 2026), but until now its refusal had no code and could come after the call had begun to create records (the batch and the records of yourats_candidate_idvalues); nothing was sent. The words of the message are unchanged. The check also comes earlier than before, so some calls over 20,000 candidates that used to be refused for another reason, such as a key's invitation limit (429) or a scheduled day without room (send_day_full), now get this400first, and such a call uses none of your key's invitation allowance. Split a large list into several calls. - A new route reads the transcript and the recording links of a finished interview (section 5.10).
GET /hiring/submissions/{id}/media/v1(scopesread_hiringandread_candidates, both) returns, for one finished AI interview or role-play, the text the system stored for each question and answer (paged withpageandlimit, 50 entries by default and 200 at most) and the interview's recordings as clips (at most 500, in interview order), each with astatus, areasonand, whenready, a signed link that expires an hour after it was made. One word,availability, says whether it isready,processing,held,lockedornot_available, with areason. A link is a secret: its expiry is not a limit on who can open the file. A link is made only for a stored recording address on our own storage that passes the system's checks (the address, the file record, the known lineage of screen recordings and your plan's limits). A clip islockedwhen your plan hides the recording, andnot_availablewith a reason when no link can be made. The route is built to leave out the platform's own screen recording of an interview. A live interview, a recording evaluation, a held report and an interview the candidate has not submitted return no transcript and no link. A result the key cannot open, a practice or trial run, a deleted result and an unknown id answer the same404submission_not_found. A503media_busywith aRetry-Afterheader means too many of these requests were being served, and503media_links_unavailablethat the links could not be made: nothing was changed, so send the same request again. No scope was added: a key that holds both scopes can call it. With it the public API has 66 operations (it had 65). - The stop-reminders route now pauses, and a new route resumes (sections 4.10 and 4.10.1).
POST /interview/candidate/invites/{id}/stop-reminders/v1keeps its name, its path and every field of its answer, but it now means pause: the invitation's automatic reminders are held on every channel that sends them for it, nothing is cancelled and no date moves. The answer gainspausedandpaused_lanes;reminders_stoppedis still alwaystrueand keeps its name (it now means paused). Like before, it never holds back an invitation email that has not been sent yet. The newPOST /interview/candidate/invites/{id}/resume-reminders/v1(scopeinvite_candidates, no body) resumes every paused channel, whoever paused it (a recruiter's Pause in the dashboard included), and answerstal_invite_id,resumed,resumed_lanes,skipped,alreadyandpaused_lanes: a reminder that is not due yet goes on its own date, one that is due today goes at the next pass of the reminder sender, and one whose due day (the UTC calendar day) had already ended while its channel was paused is skipped, with every later reminder of that channel (skippedsays which). The409reminders_managed_by_sequenceis narrower than before: it answers only for an invitation that an email sequence alone sends; when Flowmingo's own email also sends it, the pause call pauses Flowmingo's reminders and answers200. Two new409codes:reminders_cancelled(a resume for an invitation whose invitation email was cancelled in the dashboard: a cancel is final) andreminders_busy(the invitation was being sent or changed at that moment: repeat the call). The entry below that describes the route as one that switches reminders off is the route as first published. A key that already holdsinvite_candidatescan call both routes. With the resume route the public API has 65 operations (it had 64). - New routes read one hiring project, and edit it and its job post (sections 3.5.13 and 3.5.14).
GET /hiring/projects/{id}/v1(scoperead_hiring) returns one project: its name, role, status andexternal_id, its job post (the words it displays, the words as stored before any rewording assent_titleandsent_description,public_state,review,differs_from_sent) and its assessments (never question banks or questionnaires).POST /hiring/projects/{id}/update/v1(scopecreate_project, and the person who created the key must be an Owner or an Admin) sets the project'stitle,positionanddescriptionand the words of its job post (job_post.title,job_post.description), and answers{ id, changed, updated_at, project, warnings }, whereprojectisnullunless the key also holdsread_hiring. The job post's words go through the same automatic content check as an edit in the dashboard, and the check can reword them: the job post then displays the checked wording, anddiffers_from_sent,reviewandwarningssay when. A live job post that has been posted before is not taken down while its new words are checked (one that has never been posted, for example one made in the dashboard and switched on later, is taken offline if the check refuses its new words, and the answer can warn about it withjob_post_may_go_offline_if_rejected), and the call can take up to about a minute because it waits for the check. The two warnings that speak of a page being served (job_post_flagged_still_live,job_post_may_go_offline_if_rejected) are made only while the page of the job post the call edited is open when the answer is built (job_post_may_go_offline_if_rejectedalso needs that job post to have been switched on, never posted and approved before the edit, and is not made when a refusal came back within the wait:job_post_review_rejectedthen carries the verdict), andjob_post_rewritten_by_reviewreports a rewrite of the title or the description that the call changed, not of the other field (which shows indiffers_from_sent).reviewis a best-effort reading of the last check: the edit's answer sayspendingfor the job post it edited, when that is the job post shown, until a check is seen to finish, a later read reports the last check, and to know what the job post shows you comparetitleanddescriptionwithsent_titleandsent_description(what you sent, trimmed); if they are not what you expect, change the words (even by one character; you can change them back afterwards) or change them in the dashboard, because sending or saving the same words again starts no check. The words of a job post that is switched off or has expired cannot be edited through this route: it answers409job_post_is_offand writes nothing. The edit has no version token: it sets the fields you send to the values you send and the last write wins, and the one protection against overwriting a colleague's change is409job_post_changed, which covers the job post's title, description and switch only. It is saved in two steps, the project fields and then the job post, and the errors of the job post step say which project fields were saved (after any other error or a timeout, read the project first and comparejob_post.sent_titleandjob_post.sent_description(of the project's newest job post) with what you sent, after removing leading and trailing blanks from it). An edit that changes the words of a job post that is switched on is checked by AI and counts against a daily allowance of 50 per organization per UTC day: the 51st answers429job_post_edit_budget_reachedwith aRetry-Afterheader, and503job_post_edit_budget_unavailablemeans the allowance could not be read and nothing was changed. The new codes arenothing_to_update,project_has_no_job_post,job_post_is_off,job_post_pay_range_invalid,job_post_not_saved,job_post_outcome_unknown,project_outcome_unknown,edit_saved_answer_unavailable,job_post_edit_budget_reachedandjob_post_edit_budget_unavailable;job_post_changed,project_field_not_supported,job_post_field_not_supported,project_not_foundandkey_out_of_reachalso answer these routes. Neither route can break an integration that does not call it. A key that already holdsread_hiringorcreate_projectcan now call them: check which of your keys carry those scopes. - A new route stops an invitation's automatic reminders (section 4.10).
POST /interview/candidate/invites/{id}/stop-reminders/v1(scopeinvite_candidates) switches off the two automatic reminders of one AI interview or role-play invitation (about 2 and 4 days after the invitation), as the Stop reminders button does in the dashboard. It works for an invitation already sent and for one that is scheduled and not yet sent, answers200withalreadytrueon a repeat only while the invitation can still be stopped (a repeat after the invitation joined a live multi-assessment bundle answers409reminders_managed_by_bundle, and after it was cancelled404invitation_not_found; a repeat after a recruiter has restarted the invitation's reminders in the dashboard, for example with "Send Flowmingo's invitation instead", stops the new reminders and answers200withalreadyfalse, and a further repeattrueagain), and answers409reminders_not_stoppable,reminders_managed_by_sequenceorreminders_managed_by_bundlewhen it cannot (the last, which is checked before the other two, is for an invitation that belongs to a multi-assessment bundle that may still be sending reminders, that is, one that has not expired, is not finished and is within 10 days of its email, whose reminders the bundle sends; one such bundle is enough, even if the invitation also belongs to a bundle that has ended). It does not cancel the invitation, it does not stop a separate email sequence that follows the invitation (the call still answers200for it), and a reminder the sender had already picked up may still arrive. An invitation made without naming a project can be stopped only by the key that made it, when that key's creator is limited to some projects. A key whose creator has left the company (during the grace period) gets404invitation_not_foundfor every invitation.stopped_atis in epoch milliseconds. It cannot break an integration that does not call it. A key that already holdsinvite_candidatescan now call it. With this route and the two routes of the entry above, the public API has 64 operations (it had 61). - The note and the files of the invite route (section 4.3). The route takes up to 2,500 characters of text in the note. An empty string means no note, and within these limits so does a blank note or what an empty editor leaves. A refused note gets a
400, for example withinvitation_note_empty(no text left once unsupported content is removed),invitation_note_too_long(over 2,500 characters of text) orinvitation_note_too_large(over 5,000 characters once unsupported content is removed, formatting and link addresses included); a note over 50,000 characters as sent gets a plain400with no code. The files inattachment_sys_file_idsare checked but not sent, because the route sends from Flowmingo's own address, which carries no company files: an invitation email that is sent goes out without them. The files are dropped when the email is sent, so an invitation that was queued or scheduled before 5 November 2026 and sent after it also goes out without its files. An earlier version of this guide described the note as 10 to 5000 characters, no more than 2000 of them text, and the files as attached to the invitation email: an integration built on that text should re-check both. - Two new routes ask a person to approve a delete, and the MCP server gains two tools (sections 9.7.1 and 9.7.2).
POST /approvals/remove-candidates/v1(scopewrite_pipeline) stages the removal of 1 to 50 applications from their hiring project, as Remove on a candidate's card does: the application is deleted, and the candidate's profile stays in your talent pool and in other projects. It is not a reject.POST /approvals/delete-project/v1(scopecreate_project, and the person who created the key must be an Owner or an Admin) stages the deletion of an active hiring project: the project and its job posts are deleted, its candidate applications are not deleted one by one, and profiles, contacts, assessments and invitation links are kept. Neither deletes anything on the key's say-so: a person approves it at theapproval_urlwithin 15 minutes (an Owner or an Admin, for a project), and neither has an undo. The status read reports them withactionremove_candidatesanddelete_project, and the undo route refuses them. The MCP server toolsremove_application(scopewrite_pipeline) anddelete_project(scopecreate_project) use them. No new scope was added, so a key that already hascreate_projectcan now ask for a project to be deleted (a person must still approve it): check which of your keys carry that scope. The new409codesapproval_changed_sinceandapproval_key_revokedare answered to the person who approves (section 8.4). The public API then had 61 operations. Existing routes are unchanged. This cannot break an existing integration. - New limits: 50 active keys, organization and key ceilings, invitations per key, and an e-mail ceiling (sections 2.2, 8.4 and 8.5). An organization can hold at most 50 active API keys since 4 November 2026 (
409api_key_limit_reached). Since 5 November 2026 a call past 1,500 API calls a minute or 30,000 a day for your organization, or 20,000 a day for one key, answers429withapi_org_rate_limited,api_org_daily_limit_reachedorapi_key_daily_limit_reached, and a call that would take a key past 300 invitations an hour or 1,000 a day answers429withapi_key_hourly_invite_limit_reachedorapi_key_daily_invite_limit_reached. Each429names the limit and its number and says nothing was done. It gives the wait in aRetry-Afterheader and in themessage, except for a single call that would make more than 300 invitations, which tells you to send smaller calls. The invitation, reminder and message e-mails Flowmingo sends for your organization are held, never dropped, past 1,500 recipients an hour or 5,000 a day, so a held invitation can arrive late. For a higher limit, write to contact@flowmingo.ai. - The MCP server gains eight tools that use the routes of sections 3.5, 3.6, 3.7 and 4.9.
get_job_post_publication(scoperead_hiring),publish_job_post,unpublish_job_post,boost_job_post,set_project_stateandcreate_project(scopecreate_project),create_assessment(scopecreate_assessment) andcancel_scheduled_invite(scopeinvite_candidates). They act at once, are not staged for a person's approval, and email no candidate. The key's creator must be an Owner or an Admin for all of them exceptcancel_scheduled_invite.create_projectandcreate_assessmentare opt-in at the MCP sign-in: a client that asks for no scope is not given them, and a connection that did not get a scope does not see its tools. So a key that already holdscreate_projectorcreate_assessmentcan now be used by an assistant for these actions: check which of your keys carry those scopes. The MCP guide at flowmingo.ai/integration/mcp lists the tools. This cannot break an existing integration. - New routes publish, unpublish and boost a job post, and open or close a hiring project (section 3.7).
GET /hiring/job-posts/{id}/publication/v1(scoperead_hiring) says whether a job post is open to the public:statusactivedoes not, so the publication read, and the publish, unpublish and boost answers, carrypublic_state(live,awaiting_review,rejected,flagged,offorproject_not_open),public_url,tier,reasonsandwarnings. The project status answer has its own shape.POST /hiring/job-posts/{id}/publish/v1,unpublish/v1andboost/v1, andPOST /hiring/projects/{id}/status/v1, needcreate_project, and the person who created the key must be an Owner or an Admin (a creator who has left uses the role they left with, during the grace period). A publish runs the dashboard's AI content check first when the post has no checked copy of its wording, and a boost raise runs it too; each is limited to 50 a day for the organization (429publish_review_limit_reachedorboost_limit_reached). Boosting is free. The new codes are in sections 3.7.6 and 8.4. The public API then had 59 operations (the current count is in section 9). Existing routes are unchanged. No new scope was added, so a key that already hascreate_projectcan call the four newPOSTroutes (while its creator is an Owner or an Admin): check which of your keys carry that scope. - The invite route sends Showcases (section 4.8). A request that names a Showcase is no longer refused with
assessment_type_not_invitable_via_api: it emails the candidates the Showcase's invitation, as the Flowmingo dashboard does, by email address. An integration that sent a Showcase id and handled the400will now send real emails. One Showcase per request, an email for every candidate and at most 100 per call, noats_candidate_id,metadata,practice_modeorsend_at, and daily limits of 1,000 for one Showcase and 5,000 for your organization (429assessment_test_invite_limit_reached). Each entry ofresultscarriesassessment_inviteswithassessment_typeshowcase. A custom note (invitation_message) is accepted, andattachment_sys_file_idsis checked but the files are not sent (section 4.3). Sending costs nothing; a Showcase that is "Sponsored by you" debits your organization's wallet when a candidate completes it. The new400codes areshowcase.invite.set_not_published,showcase.invite.set_archivedandshowcase.invite.placeholder_recipient_not_allowed. - The invite route can schedule an invitation, and a new route cancels it (section 4.9).
send_at(a date and time with an offset orZ, from 2 minutes to 90 days ahead) sends an AI interview or role-play invitation later. It is when the invitation is sent, not when the candidate takes the interview. Each scheduled invitation carriesschedule_send_at. It counts against the daily limits on the day you make the request and on the day it is due (400send_day_full). The new400codes aresend_at_invalid,send_at_requires_send_invite,send_at_not_supported_for_assessment,send_at_requires_assessmentandsend_at_requires_email.POST /interview/candidate/invites/{id}/cancel/v1(scopeinvite_candidates) cancels a scheduled invitation while its due time is still in the future, and answers409invitation_not_scheduledotherwise. A request withoutsend_atbehaves as before. - A Showcase made through the API has the dashboard wizard's defaults (section 3.6.9). It has an intro message and the CV upload question, like one made in the dashboard. This cannot break an integration.
- The invite answer warns when an invitation will not be emailed (section 4.5). The entry of a candidate invited to an AI interview or a role-play whose assessment is a draft, inactive or archived now carries
warningswith the codeassessment_not_active, the assessment's id and a sentence. Such an invitation was always created and not emailed while the assessment was not published, and the request still answers201: only the warning is new. It is absent whensend_inviteisfalse. The assessments list now documents all fourstatusvalues (1active,2draft,0inactive,3archived). The guide also lists, which the route already took or returned, the request fieldsattachment_sys_file_idsandmetadataand the result fieldcom_contact_id(on the entries of an AI interview, a role-play and a CV evaluation, never on those of a Skill Test or a Technical Test). This cannot break an integration that ignores fields it does not know. emailonGET /hiring/candidates/v1now matches the address your company holds for the person: the one the row shows inemail(section 5.6). It used to be matched against the address the candidate keeps on their own Flowmingo profile, which you never see and which can differ from the one in your records, so a lookup could miss a person you hold or hit one by an address your company does not have. A person with no address on your company's record is found by the address on their newest result that the key's creator can open; a person who deleted their Flowmingo account is found like anyone else your company holds. The parameter is new (29 October 2026). If a step of yours looked a person up by the address they typed on their own profile and that address differs from the one you hold, send the address you hold: theemailof the row you get back is the one that was matched.- Your own ids:
external_idon twelve kinds of record (section 8.8). A project, a job post, a stage, an assessment, a question, a criterion, a person, an application, an invitation, a recording submission, a note, rating or shortlist mark, and a Zoho Recruit order can carry an id of your own. It is optional plain text of 1 to 128 characters, stored exactly as sent, scoped to your company, not unique and fixed when the record is created. The answers and list rows that show these records gain anexternal_idkey (nullwhen none was set), and the lists of projects, job posts, assessments, candidates and applications filter by?external_id=. A request that does not use it behaves as before; the entries below say what else changed. The new key cannot break an integration that ignores fields it does not know (section 8.7). - Projects, job posts and stages take ids (sections 3.5 and 3.5.11).
POST /hiring/projects/v1takesexternal_id,job_post.external_idandstages(the ids of the four default stages, by name). The answer,GET /hiring/projects/v1,GET /hiring/job-posts/v1,GET /hiring/job-posts/{id}/v1andGET /hiring/projects/{projectId}/stages/v1return them. A filtered project list leaves outlargest_projects. The ids are part of the request, so the sameidempotency_keywith another id, an id added or an id dropped is a409. This can break an integration in one case only: it repeats theidempotency_keyof a project made before this release with a body that now carries an id, which is a409. - A key that
job_postor an entry ofstagesdoes not take is refused even when it is namedconstructor,__proto__orprototype. Before, those three names were dropped silently and the project was created. The check also runs before the rest of the body is validated, so a body with several faults answersjob_post_field_not_supportedorstage_field_not_supportedfirst. This can break an integration whose body carries one of those names: a repeat of such an earlier call is now a400, not a replay. Keys of those names at the top level of the body are still ignored. - A new route reads a create-project call back by its key:
GET /hiring/projects/requests/{idempotency_key}/v1(section 3.5.12). It says whether the project was saved, and gives its id, title andexternal_idas they are now. It needscreate_project. It cannot break an integration. The public API then had 53 operations. - The six assessment routes take
external_id(section 3.6.4). Every answer and the status read return it, and the assessments list returns it and filters by it. It is part of the request, so another id under the sameidempotency_keyis a409; a request you stored earlier and send again unchanged is still a replay. An assessment made inside a create-project call hasexternal_idnull. This cannot break an integration. - The status read of an assessment request no longer says that nothing was created for a project the key can no longer open (section 3.6.3).
404project_not_foundkeeps its code and status, but its message now says that the request exists and an assessment may have been made for it, and sends you to the assessments list. The message of a request that was stopped for the same reason says the same. This can break an integration only if it matches the old wording, which the guide never promised: branch oncode(section 8.4). POST /interview/set/v1takesexternal_idand an optionalidempotency_key(sections 3.2.5 and 3.2.6). The ids can sit on the assessment, on each question and on each criterion, and the answer lists the ids of the questions and criteria it saved (results.iai_questions,results.iai_requirements) besideresults.external_id. With anidempotency_key, a repeat of the same request returns the assessment the first call made (200,idempotent_replaytrue). The new codes areidempotency_key_reused,idempotency_request_in_progress(withRetry-After),interview_set_was_deletedand, for a key sent together withaddons,idempotency_key_not_supported_with_addons. A request without a key behaves as before: two identical requests still make two assessments. Only a request that sends the new key or id can meet the new codes.- The invite route takes three ids for each candidate (sections 4.3 and 4.5).
external_idis the person,application_external_idthe application in the one project the request names, andinvitation_external_idthe invitation to the one assessment it names. Each result returns the ids now stored, with a*_kept_existingflag when the record already held a different id, and now always has the three keys.GET /hiring/candidates/v1,GET /hiring/candidates/{id}/v1,GET /hiring/applications/v1andGET /hiring/interviews/upcoming/v1return them, the candidates and applications lists filter by them, andPOST /hiring/applications/v1takesexternal_id. The new400codes (external_id_invalid,external_id_requires_email,application_external_id_requires_project,application_external_id_ambiguous_project,invitation_external_id_requires_invite,invitation_external_id_ambiguous_assessment) apply only to a request that sends one of the three fields. This cannot break an integration that ignores fields it does not know. - The invite route refuses a value the database cannot keep with a coded
400; it used to end in a500with a half-saved request (section 4.3).candidate_field_too_longandcandidate_field_invalidname the field, such ascandidates[2].ats_candidate_id, and nothing is created. The limits are 250 characters forats_candidate_id,email,firstname,lastnameandname(the email is measured in lower case, the way it is kept), 2048 forthumb_file_link(not checked on a Skill Test or a Technical Test), and forcv_link1536 on a request for an AI Interview, a Role-play or a CV evaluation and 2048 for a Skill Test or a Technical Test; on the first kind the part after the last/of acv_linkcan be 1024 at most. A NUL (zero) character is refused too. This can break an integration in one case: acv_linkof 1537 to 2048 characters on a request for an AI Interview, a Role-play or a CV evaluation used to fail for that candidate only, and is now a400for the whole request. - A failure inside an invite no longer shows database or library text. In
invitation_error_messages,application_error_messages,submission_error_messages,error_messageand the older single-value*_error_messagefields, an internal error now reads "This step failed because of an internal error (reference ...). Try again later; if it keeps failing, contact Flowmingo support and quote this reference." The reference is 8 hexadecimal characters and names a line in our log. Messages the code words itself, such as "Candidate was already in this project" and "Email already invited", are unchanged. The recruiter dashboard shows the same sentence. This can break an integration that read the old text; branch on the field being non-empty, not on its wording. ats_candidate_idis plain text of up to 250 characters, not a UUID, and a request with several projects now keeps its application in every one. The guide used to call it a UUID you choose; any text was already accepted, so nothing changes for a request that worked. With severalcom_project_idsand anats_candidate_id, the later projects used to fail with a raw database message; each project's application is now created. Each result entry that carries anats_candidate_idgainsats_records, one{ com_interview_set_id, record_id }for each record written, and an assessment id named twice writes one record, not two. This cannot break an integration that ignores fields it does not know.- Recording submissions take
external_id(section 4.7.3). Each entry ofPOST /interview/recording/v1can carry one; the result of the entry and the three submission reads (GET /hiring/submissions/v1,/{id}/v1and/{id}/detail/v1) return it.?external_id=on the results list is a400, not an ignored parameter, because that list has no index for it yet. An invalid id rejects only its own entry (invalid_external_id). Two newrejectedcodes can replaceduplicatefor an entry whoseclient_refalready created a submission:duplicate_not_accessible(the key's creator can no longer open that submission, or it was deleted since, with its assessment or alone: neither id is shown, never anexternal_idofnull; use a newclient_ref) andduplicate_read_failed(its details could not be read just now and nothing was created again; send the entry again). This can break an integration that expects onlyduplicatefor a reusedclient_ref. - Notes, ratings and shortlist marks take
external_id(sections 6.4 to 6.6). The answer of the call returns it. It names that event, not the result's current state. This cannot break an integration. - Partner orders: concurrent sends, failed sends and
external_id(section 9.6). Two identical requests at the same moment toPOST /ats/greenhouse/send_test,POST /ats/recruitee/send_candidateorPOST /ats/zoho-recruit/ordersnow send one invitation. A send that failed frees its report id only when nothing was left behind; otherwise the order is kept, and that call and every later request with the same report id answer409order_needs_reconciliationand send nothing. A request that finds an order still being sent can answer503order_in_progress, and one that finds an order whose first request has recorded no result for more than two minutes answers the same409and sends nothing (the next entry). An email, candidate id, application id or report id over 250 characters, or a NUL character (also in the Zoho Recruitcallback_url), is a400instead of a500. The Zoho route takesexternal_id. This can break a script that retried every non-success: do not retry the409. GET /ats/greenhouse/test_statusanswers400for apartner_interview_idthat is not a UUID. It answered500before. A script that retried on that500should stop: the400is final.- A bracketed spelling of the
external_idfilter is refused (sections 8.3 and 8.8).?external_id[]=aand?external_id[x]=a, also with the brackets percent-encoded, answer400on the lists of projects, job posts, assessments, applications and candidates, and on the results list, where every spelling of the filter is refused. They do not return the unfiltered page. This can break an integration only if it already sent a parameter namedexternal_id[...]to one of these lists, which was ignored before. - A query string with more than 1,000 parameters is refused on the lists that take
external_idand on the results list (section 8.3). It answers400with the message "Too many query parameters: only the first 1000 are read, so the external_id filter could not be checked and nothing was filtered. Send fewer parameters, once each." Before, the server dropped everything after the 1,000th parameter without a word, so a bracketed or repeatedexternal_idplaced after it was never seen. A query of exactly 1,000 parameters is read in full, and a trailing&is not counted. This can break an integration only if it sends more than 1,000 query parameters to one of these lists. - The read of a create-project call by its key ignores spaces at either end of the key (section 3.5.12). A call made with
" ats-job-48213 "is found by readingats-job-48213, as the create call already treated the key. This cannot break an integration. - A repeat of
POST /interview/set/v1with anidempotency_keylists the questions and criteria as they are now (sections 3.2.6 and 3.4).results.iai_questionsandresults.iai_requirementshold the live ones in their current order, which differs from the order you sent if someone has since added, removed or reordered some: never pair them with your input by position. Anexternal_idmatches a row only where you sent one that no other entry in the list carries; an entry with no id, an entry with a repeated id and a question Flowmingo prepared for arealtimeinterview cannot be matched. An assessment made by a request that carries anidempotency_keycounts as made by the creator of the API key, so a repeat from that key is not refused for that reason. This can break an integration only if it pairs the rows of a repeat with its input by position, or by anexternal_idthat is missing or repeated. POST /approvals/invite/v1refuses the three ids (sections 8.8 and 9.7). A candidate row with a non-nullexternal_id,application_external_idorinvitation_external_idanswers400with the codeexternal_id_not_supported_when_staging, and nothing is staged. A staged invitation keeps only each recipient's name and email, and the ids would otherwise have been dropped without a word. This can break an integration only if it already sent a field with one of these names to that route, which was ignored before.GET /hiring/candidates/v1?external_id=also finds a person invited to an assessment without a hiring project (section 5.6). Those people come after the matches in your pipelines, andtotalcounts both.qandemailnarrow both groups; onlycom_project_idkeeps the lookup on the pipelines. This cannot break an integration.- An address on a candidate row is never taken from an assessment the key's creator cannot open (section 5.6). The
emailofGET /hiring/candidates/v1andGET /hiring/candidates/{id}/v1is the address your company holds for the person or, when it holds none, the one on the person's newest result that carries an address, in an assessment the key's creator can open. For a key whose creator is limited to some projects it could come from any assessment of the company; it isnullnow when only an assessment outside the creator's reach has an address. This can break an integration only if a key limited to some projects read such an address. - The invite rules for the three ids reach every kind of invitation (sections 4.3.1, 4.3.2 and 4.6.1). A request that names several projects or assessments is counted over the list and the deprecated single field together. On a Skill Test or a Technical Test, a candidate with an id and no
emailgetsexternal_id_requires_email(a row with neither keepsassessment_test_email_required), andinvitation_external_idwithsend_invitefalsegetsinvitation_external_id_requires_invite. A test does not measurethumb_file_link, which it does not use, and measures theemailafter the spaces around it are removed. This can break an integration only if it sends one of the three ids with the combinations above. - An invitation that is reactivated keeps an id another request stored (section 4.3.1). A request changes only the fields it owns, so two requests that meet cannot erase an id. This cannot break an integration.
- A partner order whose first request has recorded no result for more than two minutes is kept for a person (sections 8.6 and 9.6). A request with its report id answers
409order_needs_reconciliationand sends nothing, however many times it is repeated; the order is not released for a new send. The409body carries theorder_idto give to Flowmingo support. A retry of an order that already exists gets that order back before its lengths and NUL characters are checked, but its body must still pass request validation (a Zoho Recruitexternal_idof 129 characters is a400on a retry too), and a NUL character in the Zoho Recruitcallback_urlis a400with the codecandidate_field_invalidbefore anything is reserved. As with the entry above, do not retry the409. - Reading a partner order that is not settled answers
409or503(sections 8.6 and 9.6).GET /ats/zoho-recruit/orders/{id}andGET /ats/greenhouse/test_statusanswer503order_in_progressat once, without waiting, for an order whose first request is less than two minutes old, and409order_needs_reconciliationwith theorder_idfor an order kept for Flowmingo to check or one with no result after two minutes. They never report such an order asfailedorsent. Before, a read answered200with the status of any order it found. This can break a script that treats every answer other than200as the end of the order: treat the503and the409as "not settled yet" and ask again later. A new order for an assessment the key's creator may not send to still gets the invite route's403key_out_of_reach, now before anything is reserved, so no order exists and nothing is kept.
October 2026
- Refused keys say why, and a busy permission check says how long to wait. A key turned off because its creator left now answers
403with the codekey_creator_leftwhen the secret is right (it was401). A503from the check of which projects a key may use carries the codekey_reach_unavailableand aRetry-Afterheader. The refusals about a creator's reach carry codes (section 8.4). An error'spathno longer repeats the query string. A list field sent as one text value in an invite is refused with400(it was500). With a wrong secret an expired key now answers401like any other key (it was403). - A key acts with the permissions of the person who created it. Before, a key reached the whole organization. Now a key has the reach of its creator (section 2.9): a creator limited to some projects gives the key that limit, lists, counts and averages leave out what it cannot reach, and a route that names something outside it answers
404or403as its own section says.GET /events/v1needs a key whose creator has access to all hiring projects.create_set,create_projectandcreate_assessmentalso need their creator to be an Owner or an Admin. The recording routes accept only an assessment the creator can open (section 4.7.1). A key whose creator is still in the company but can open no hiring project answers403on every route (section 11).GET /hiring/projects/v1and theproject_idsof the assessments list follow the same reach. - A key stops after its creator leaves. After a grace period (seven days by default) the key of a person who left the company answers
403(401if it was scheduled for deletion and has been deleted) unless an Owner or an Admin took it over in Settings (section 2.10). If no other Owner is left in the company when the creator leaves, the key is turned off at once (section 2.10). During the grace period, sending a Skill Test or a Technical Test (section 4.6) also stops at once and answers404Project ... not found, because those two go out as the person and read their access today. A key whose creator had already left before this release keeps working for seven days from the release, and the company's Owners and Admins are emailed so that one of them can take it over; where the company has no Owner left, that key was turned off at the release. The exceptions are a key that has no creator on record and a key whose creator held no hiring project when they left: those stopped at the release (sections 2.10 and 11). Before, a key kept working. - A change to a key applies from the next request. A key you turn off, narrow or delete stops working, or loses the scope, from its next request. Before, a change took effect after a short delay.
- Practice runs and trial runs answer
404by id, and no read, count or average includes them (section 5.1). Names and CV links come from your company's own records;cv_linkis returned only for a CV your company was given, otherwise it isnull. assessment_typeacceptsinterview(AI interviews and role-plays together). The results list now refuses a value that is not one of the nine with400; before, it returned an empty page (section 5.2).- The applications list reads only the filters of section 5.6.
candidateNameandprojectName, which were never documented, are ignored now; before, they filtered the list. - New:
assessments, a breakdown by assessment type, onGET /hiring/overview/v1(section 5.8); anundofield on the note, rate, shortlist and collection routes (taking a rating or a shortlist mark back sendssubmission.review.updateagain, see the webhook guide); and the eight approval routes for AI assistants (section 9.7). The public API then had 52 operations. - Webhooks and the events feed carry no practice score. A recorded practice attempt or try-out raises no score event, and the events feed leaves out stored events that name one (section 7). See the webhook guide.
POST /hiring/projects/v1takes thePreferheader.Prefer: respond-asyncorPrefer: wait=<seconds>shortens the wait for the project's assessments (not the job post's draft), an answer that contains assessments carriesPreference-Appliedwhen the preference was applied, and a call that shortens the wait normally reports a failed assessment instead of starting it again (section 3.5.5). Without the header nothing changes.project_idson the assessments list no longer names deleted projects. Until 30 October 2026 it could list the id of a project that had been deleted; an assessment whose only project was deleted now shows an empty list (section 5.7).- Each API key is limited to 600 requests a minute. The 601st request of a UTC minute is refused with
429and the codeapi_key_rate_limited, aRetry-Afterheader and the wait in themessage; answers to requests we counted carryX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset. Before, a key could make any number of requests a minute; only the volume caps of section 8.5 applied. An integration that sends more than 600 requests a minute on one key must now slow down or spread its work over more minutes. - The OpenAPI document is a static file now. It is at
https://flowmingo.ai/openapi.json(OpenAPI 3.0; at that time the guide had 44 operations). The address this guide used to give,https://apis.flowmingo.ai/company/doc-json, and the Swagger page next to it no longer answer. - Two routes hand over recordings of interviews you ran yourself.
POST /interview/recording/uploads/v1gives an upload URL for each file, andPOST /interview/recording/v1turns the uploaded files into submissions of a recording evaluation, which are transcribed and scored like an AI Interview (section 4.7). They need the new scopesubmit_recordings, which a key does not gain automatically and which is never implied byinvite_candidatesorcreate_set: tick Submit interview recordings on a key to use it (section 2.3). Evaluating recordings is part of the Scale plan and above. The public API then had 44 operations and eight scopes. Existing routes and keys are unchanged. - Six routes create assessments with the AI.
POST /assessments/cv-evaluation/v1,ai-interview,role-play,skill-test,showcaseandtechnical-testeach make one assessment from a job title, published and ready to use, andGET /assessments/requests/{idempotency_key}/v1reads where a request has got to (section 3.6). They need the new scopecreate_assessment, which a key does not gain automatically and which is never implied bycreate_setorcreate_project: tick Create assessments on a key to use it. Each call takes a requiredidempotency_keyand acts as the person who created the key (an Owner or Admin). It answers201when the assessment is ready within about 45 seconds and202withRetry-Afterotherwise (the same key then returns it), and a repeat of a request that is already ready answers200withidempotent_replaytrue. Each assessment costs AI generation, against a budget of 60 units a minute and 1,500 a day per organization (section 3.6.11). The public API then had 42 operations. Existing keys do not gain the new scope automatically, and the changes to the existing create-project route are described in the next entry. - The create-project route can make the project's assessments and set its job post by name, and its answer changed. The new
assessmentsfield names the assessments to make with the project, and the newjob_postblock sets job functions, seniority levels, employment types, workplace types, places and pay by name (sections 3.5.5 and 3.5.6). The answer for a project created now carriesmissing_details, the details the saved job post still lacks (section 3.5.7). A call that omitsassessmentsnow also makes a CV Evaluation and an AI Interview, waits up to about 45 seconds for them, and answers202while one is still being made; sendassessments: []to create the project alone, as before. An answer with assessments addsassessmentsandpartial_failure, and a repeat of a call that made assessments answers200instead of201. The AI's own age, gender, marital status and nationality filters are no longer saved on the job post. A project made before 22 October 2026 keeps replaying exactly as it was made (section 3.5.5). The project limits (10 a minute, 300 a day) are unchanged, and the call needs only thecreate_projectscope. - A new route creates hiring projects.
POST /hiring/projects/v1needs the new scopecreate_project, which no existing key carries: tick Create hiring projects on a key to use it (section 3.5). It creates an active project with its four default stages and an AI-drafted job post, all together or not at all (if the AI cannot draft the post, nothing is created and the call answers503job_post_generation_failed; send it again), takes a requiredidempotency_key, acts as the person who created the key (an Owner or Admin), and is limited to 10 projects a minute and 300 a day per organization. The public API then had 35 operations. Existing routes and keys are unchanged. - The
assessment.completedevent for a Technical Test never carries a score.scoreandscore_percentare alwaysnullfor a Technical Test (before, they carried the score unless a code answer was still being graded). The event says the attempt ended, not that the score is ready: read the score fromGET /hiring/submissions/{id}/v1orGET /hiring/submissions/{id}/detail/v1, where a read made right after the attempt ended can still show a provisional number, and nothing marks the point at which it stops changing (section 5.5). Every other field of the event, the events of the other assessment types, and the results routes are unchanged. See the webhook guide. - The invite route sends Skill Tests and Technical Tests. A request that names one is no longer refused with
assessment_type_not_invitable_via_api: it emails the candidates the test's invitation, as the Flowmingo dashboard does, by email address. An integration that sent one of these ids and handled the400will now send real emails. The rules are in section 4.6: one test per request, one hiring project (or the test's single one), an email for every candidate and at most 100 per call, no value inats_candidate_idormetadata, and nopractice_mode. Each entry ofresultscarriesassessment_invites(withemail_delivery,reused_existingandstatus) instead of the interview fields, and never the candidate's invitation link. Daily limits are 1000 invitations to one test and 5000 for your organization, answering429(assessment_test_invite_limit_reached) or503(assessment_test_invite_limit_unavailable). The API never revives a cancelled, used-up or expired invitation (it gives it no new link and no new attempts). Repeating a call is built not to email a candidate twice, but that protection is temporary (section 4.6.5). Sending costs nothing; a sponsored test can use your organization's wallet when a candidate starts it, as it does for an invitation sent from the dashboard. Theinvite_candidatestool of the MCP server can invite to them too, but it sends nothing itself: it stages the invitation, and nobody is emailed until a signed-in person approves it at a link (section 9.7). - The invite route refuses assessments it cannot send. A request naming a Skill Test, Showcase, Technical Test, Recording evaluation, Questionnaire or question bank was refused with
400and the codeassessment_type_not_invitable_via_api, before anything was created (since the entry above, a Skill Test and a Technical Test are sent instead). Before, such a request answered2xx, sent nothing, and still created the candidate and an empty "needs interview" entry. - Booleans and numbers must be real JSON values on the invite, create-set, note, rate and shortlist routes. Before, the text
"false"was read astrue(sosend_invite: "false"sent the invitation emails andshortlisted: "false"shortlisted the candidate) andrating: ""was read as0. They are now refused with400. - Every project and job post id in an invite request is checked against your organization, as assessment ids already were. An id that is not yours answers
404and nothing is created. - All eight assessment types are labelled.
role_play,technical_test,recordingandquestionnaireused to readunknown. The results list and the assessments list acceptassessment_typeas a filter, and the assessments list returnsassessment_typebesideset_type. Questionnaires are left out of the assessments list unless you ask forassessment_type=questionnaire, and the helper question sets that other assessments keep for their own after-questions are never listed. - Practice runs, trial runs and archived attempts are no longer returned by the results list, counted in its
total, or counted in the overview. - The invite sample ids are valid. The guide's earlier sample ids were not version-4 UUIDs, so the sample was refused by the invite route once a real key was used.
- Last used on a key now reflects every request made with it, brought up to date about hourly.
- The OpenAPI document describes the public operations (34 at the time, 35 with the project route above), with summaries, scopes, parameters, request bodies, response shapes and the
x-api-keyscheme. - Webhooks: the new
assessment.completedevent, sent for skill tests, technical tests, showcases, questionnaires, recording evaluations and CV evaluations (a recording evaluation and a CV evaluation send it as well asinterview.evaluation.update, on purpose; AI interviews and role-plays send only their existing events);application.createdis now also sent for applications that come from an applicant applying, from a candidate starting an AI interview, a role-play or a Showcase from a link, from an invitation that creates an application, and from recruiters adding candidates in bulk; turning a switched-off endpoint back on restarts its failure count at zero (before, the count was kept, so the first failed attempt after you turned an endpoint back on could switch it off again); webhook addresses are resolved when an event is delivered and refused if any address they lead to is private. See the webhook guide.
Earlier in 2026
- April: the invite response gained the arrays
invitations,applicationsandsubmissionsand their*_error_messagesmaps. The older single-value fields were restored beside them, so an integration written against the first response keeps working. - September: candidate names and email addresses in applications, results and events require the
read_candidatesscope. A key withread_hiringalone no longer receives them. - September: an invite that names candidates by
ats_candidate_idmust also name an assessment. - September:
POST /hiring/applications/{id}/reject/v1moves the application into the project's Rejected stage. It used to remove the candidate from the hiring project. - October:
question_typeon a create-set question only takes effect on apre_screeningquestion.