Flowmingo has a REST API for your own systems. With an API key you can create hiring projects and assessments, invite candidates, read scores and AI evaluations, update your hiring pipeline, and poll the same events that webhooks deliver. This guide covers all 35 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). - Machine-readable description: the OpenAPI document is at
https://apis.flowmingo.ai/company/doc-json, and a browsable version (Swagger UI) is athttps://apis.flowmingo.ai/company/doc. The 35 operations of this guide are the ones whose path starts with/integration/, each with its summary, the scope it needs, its parameters, its request body and its response shape. The same file also lists routes that our own web app uses. Those are not part of the public API and are not described here.
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. 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: the simplest kinds to send through the invite route. A Skill Test or a Technical Test can be sent through it too, with a few extra rules (sections 4.2 and 4.6). 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 sends a real invitation email. 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.
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.****).
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 six:
| Scope | Label in the dashboard | What it opens |
|---|---|---|
read_hiring |
Read hiring data | The read routes for job posts, assessments, projects and stages, applications, upcoming invitations, results with their AI evaluations, and the overview counts (section 5), and the events feed (section 7). Candidate names and email addresses are left out unless the key also has read_candidates. |
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. |
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). |
invite_candidates |
Invite candidates | POST /interview/candidate/invite/v1 (section 4). |
create_set |
Create interview sets | POST /interview/set/v1 (section 3). |
create_project |
Create hiring projects | POST /hiring/projects/v1 (section 3.5). |
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, so a key made with nothing ticked looks valid and does nothing. 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. - 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,create_setto attach an assessment to it, andinvite_candidatesto invite into it. - An AI assistant (MCP):
read_hiringandread_candidates, pluswrite_pipelineandinvite_candidatesif it may act. The assistant does not usecreate_setorcreate_project.
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 takes effect after a short delay.
There is no rotate action. To rotate a key, create a new one, switch your integration to it, then delete the old one. Deleting a key revokes it after a short delay, not at the instant you click, so do not rely on it being instant. Once the change has taken effect, a disabled or deleted key answers 401 Invalid API key.
Keys created in the dashboard do not expire (expires_at is null). Revoke a key you no longer need by deleting it.
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 for your whole organization. For reading, it is not narrowed to the hiring projects of the person who created it, so create a key only for an integration you trust with all of your organization's data. Notes, ratings, shortlists and stage moves made with a key are recorded under the name of the person who created it. The actions that act as that person also check their access to the hiring project involved: adding an application to a project, moving a stage and rejecting answer 403 when they cannot open the project (section 6), and sending a Skill Test or a Technical Test goes out on behalf of a person and can only use the hiring projects they can open (section 4.6.2).
2.6 Authenticate your requests
Send the key in the X-API-Key header:
X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj
Two other headers carry the same key, 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.
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"],
"expires_at": null
}
A 200 proves the key is accepted and shows which organization and scopes it carries. 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.
3. Create interview sets through the API
Use this endpoint 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).
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.
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. |
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. 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. |
iai_questions |
array | Optional | Interview questions (see 3.2.1). 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. |
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). |
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. |
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. |
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.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,
"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,
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,
"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,
'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,
"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,
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.4 Sample response
The endpoint returns a results envelope.
Treat the response as an acknowledgement, not a confirmation.
resultsdescribes the set itself. It does not return the questions, criteria or access entries you sent (so none of their ids), 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. 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
}
}
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).
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, then attach assessments to it with project_id (section 3.2) and invite candidates into it (section 4).
| 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 or has left the organization stops working here at once: it answers 403 with the code project_create_requires_admin. If the role cannot be confirmed just now the answer is 503 with project_create_role_unconfirmed, and nothing is created. A key that has no user and no creator on record answers 400 api_key_has_no_acting_user.
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). |
Text is trimmed before it is checked. Only these four 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 property that is not in the table is ignored without an error (a misspelt setting is not reported). 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. - Its four default stages, in order: Unassigned, Shortlisted, In Progress, Rejected. The project and its stages are saved together: if the stages cannot be saved, the project is not saved either, and the call answers an error.
- 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. No location is guessed from where your server is.
It starts with no assessments. Create one with POST /interview/set/v1 and the new id as project_id (section 3), then invite candidates (section 4).
The job post can fail without failing the call. It is built after the project is saved, by an AI model, and a model call sometimes fails. The call still answers 201 with the project, job_post is null and job_post_error says why:
job_post_error |
Meaning |
|---|---|
job_post_generation_failed |
The AI could not draft the post. |
job_post_save_failed |
A storage step failed: the post could not be looked up or saved. This is a problem on our side, not with the AI. |
job_post_interrupted |
A repeat of the call (same idempotency_key) found, more than five minutes later, that the first call never finished building the post, and no post exists. |
Add the job post in the dashboard. Repeating the call with the same key does not retry the post; it returns the same project. A new key would create a second project. A project without a job post is complete for a Skill Test or a Technical Test, which never use one. For an AI interview or another assessment that is not a test, the invite route answers 404 No job post found for project ... when you name the project with its older single com_project_id field and no com_job_post_id, until a post exists; the newer com_project_ids list does not look for one (section 4.3).
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,positionanddescription: you get the project the first call created, withidempotent_replaytrue(and the same201). Nothing new is created, and the repeat does not count against the limits below. The answer is read fresh, so it shows the project as it is now, including any edit made in the dashboard. - Same key, a different
title,positionordescription:409with the codeidempotency_key_reused. Nothing is created. Use a new key for a different project. - 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 the first attempt stopped while it was building the job post, the repeat answers409idempotency_request_in_progressfor up to five minutes, and after that returns the project withjob_post_errorjob_post_interrupted. - Two calls with the same key at the same moment: one project is created; the other call is given that project, or
409idempotency_request_in_progresswhile the job post is still being built. Wait a few seconds and send the same request again. If your organization is at its limit (section 3.5.4), the second call can instead be answered429before the first has finished: send it again with the same key.
3.5.4 Limits
Each organization can create 10 projects per minute and 200 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). 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.
3.5.5 Sample request
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": "Senior Backend Engineer - Q4 hiring",
"position": "Senior Backend Engineer",
"description": "We are hiring a senior backend engineer to own our payments API. You will work in Go and Postgres from our Berlin office...",
"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: 'Senior Backend Engineer - Q4 hiring',
position: 'Senior Backend Engineer',
description: 'We are hiring a senior backend engineer to own our payments API...',
idempotency_key: 'ats-job-48213'
})
})
const project = await res.json() // 201; project.id is the project_id for POST /interview/set/v1
3.5.6 Sample response
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"title": "Senior Backend Engineer - Q4 hiring",
"position": "Senior Backend Engineer",
"status": "active",
"source": "api",
"created_at": "2026-10-21T09:00:00.000Z",
"stages": [
{ "id": "b0b0b0b0-abcd-4ef0-8123-456789abcdef", "name": "Unassigned" },
{ "id": "1d2c3b4a-5f6e-4d7c-9b8a-0a1b2c3d4e5f", "name": "Shortlisted" },
{ "id": "2e3d4c5b-6a7f-4e8d-8c9b-1b2c3d4e5f60", "name": "In Progress" },
{ "id": "3f4e5d6c-7b8a-4f9e-a0dc-2c3d4e5f6071", "name": "Rejected" }
],
"job_post": { "id": "c0ffee00-dead-4bee-8f00-0123456789ab", "status": "active" },
"job_post_error": null,
"idempotent_replay": 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.
3.5.7 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. 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. Create the key from a signed-in account. |
403 |
(no code) | The key lacks the create_project scope, or has expired. |
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 or description. |
409 |
idempotency_request_in_progress |
A call with this key is still building the job post. Wait a few seconds, then send the same request again. |
429 |
project_create_limit_reached |
10 a minute or 200 a day reached. Try again in a minute, or after 00:00 UTC; the message says which. |
503 |
project_create_limit_unavailable, project_create_role_unconfirmed |
A check could not be completed. Nothing was created. Try again in a moment. |
4. Invite candidates through the API
This route adds candidates to your account and, unless you switch it off, emails each one an invitation to take an AI interview, a role-play, a Skill Test or a Technical Test. 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 or a Technical Test is sent exactly as the Flowmingo dashboard sends it, by email address, and has its own rules, answer and limits (section 4.6). Required API key scope: invite_candidates.
4.1 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: read the result for each candidate (see 4.5).
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. |
role_play |
Yes. |
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). |
showcase, recording, questionnaire |
No. The request is refused with a 400 and the code assessment_type_not_invitable_via_api, and nothing is created. Send these 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 (Showcase). Only AI interviews, role-plays, CV evaluations, Skill Tests and Technical Tests 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. With neither, the request is refused with a 400. A Skill Test or a Technical Test must be the only id in the request (section 4.6). |
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). |
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. |
candidates[].ats_candidate_id |
UUID | Optional | Your own id for the candidate, returned in the result. 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. A Skill Test or a Technical Test refuses a value here; leave it out or send null (section 4.6). |
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. |
invitation_message |
string | Optional | A note added to the invitation email: 10 to 5000 characters, and no more than 2000 characters of visible text. 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 queues the invitation email for an AI interview or a role-play. Large requests are paced: after the first 50 candidates, the emails go out in later batches about five minutes apart. 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 is emailed during your request, not in later batches (section 4.6). Must be a JSON boolean (see below). |
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). |
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.
Four 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. - 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. - 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 or a Technical Test has limits of the same size (1000 a day for one test, 5000 for your organization) that answer differently, with429and a code (section 4.6.6).
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",
"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.
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. Check these fields as well as the status code.
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 candidate receives the test's invitation email by email address, and 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]), and more than 100 candidates 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; 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. - 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).send_invitedefaults totrue;falsecreates the invitations without emailing anyone, and a later call withsend_invitetrueemails them. - 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 go out during your request, one candidate after another, not in later batches as they do for an interview. 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 goes out 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, and their access to hiring projects applies. A project that is archived, or that this person cannot open, 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). A key that has neither a user nor a creator is refused with 400 and the code api_key_has_no_acting_user, and nothing is created: create the key from a signed-in account, or ask Flowmingo support to attach a user to it.
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).
{
"results": [
{
"email_address": "alex@example.com",
"tal_candidate_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"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",
"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 address the invitation went to, and the candidate. |
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 emailed 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 |
The email for this invitation was sent earlier, by an earlier call or from the dashboard. Nothing was sent now. |
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. |
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 the candidate is invited once. |
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 already went out, the row reads
already_queued. If the invitation was created earlier withsend_invitefalse, this call emails it. - 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 the ones that are still open and not emailed yet are emailed (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.
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/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/projects/v1, GET /hiring/projects/{projectId}/stages/v1 |
read_hiring |
Projects and their pipeline stages. |
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. |
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 /events/v1 |
read_hiring |
Your webhook events, cursor-paginated. Poll these if you cannot host an endpoint (see section 7). |
Every read is scoped to the organization that owns the key: no endpoint can return another organization's
data. The id-addressed routes (job-posts/{id}, submissions/{id}, submissions/{id}/detail,
candidates/{id}) answer 404 for an id you do not own; the list routes simply return no matching rows.
Practice, trial and archived runs are not returned. A practice run by a candidate, a trial run by one of your
recruiters 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 and out of the overview counts, as they are in the dashboard. Reading one by its
id still works.
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).
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: ai_interview, role_play, cv_evaluation, skill_test, technical_test, showcase, recording or questionnaire (see section 5.3). A value of up to 40 characters that is none of these returns an empty page; a longer value is refused with 400. |
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". |
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",
"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,
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).
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",
"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, and the assessments list returns them 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.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.
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": [...] }.
Assessments: GET /hiring/interview-sets/v1. Parameters: page, limit, assessment_type (one of the eight names in section 5.3). Each row: id, title, position, status (1 is active), 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.
Hiring projects and their stages: GET /hiring/projects/v1 (parameters page, limit) returns each project's id, title, created_at and candidates (the number of candidates in its pipeline). GET /hiring/projects/{projectId}/stages/v1 returns the project's stages in pipeline order, each with id, name, position, stage_type and is_rejection. A project that is not yours 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) returns id, title, status, com_project_id and created_at. GET /hiring/job-posts/{id}/v1 returns id, title, description, com_project_id, com_interview_set_id (the assessment candidates take for the role) and created_at. It does not return status: read the status of a job post from the list.
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. |
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. |
Each row has id, tal_candidate_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; with read_candidates it 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, status and scheduled_at, earliest scheduled first. 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, while an assessment narrows only the result counts and leaves the stage and application counts for the whole organization) 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 results that are released to you, including those the AI could not rate (score_status cannot_evaluate, overall_score null), 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.
Candidates: GET /hiring/candidates/v1 (needs read_candidates; parameters page, limit, q, com_project_id) returns the candidates in your pipelines with id, firstname, lastname, email, cv_link and created_at. To look someone up by email address or name, 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, and adds phone and thumb_cv_link; a field your organization hides from API keys, such as the phone number, is left out. A candidate you have no relationship with answers 404.
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. The three pipeline routes (add, move, reject) also check that this person can still access the hiring project involved, and refuse with 403 when they cannot. The ids in these samples are placeholders; take real ones from the read routes in section 5 (applications gives application ids, stages gives stage ids, the results list gives result ids).
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. |
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. |
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"
}'
{
"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",
"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 or project that is not yours answers 404. The call sends the application.created event.
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). 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 replaces the earlier rating of this result by the person who created the 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 }'
{
"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"
}
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.
6.5 Rate a result
POST /hiring/submissions/{id}/rate/v1 with { "rating": 8 }. rating is required: a JSON number from 0 to 10 in steps of 0.5. It replaces the earlier rating given by the person who created the key. 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"
}
The answer is 201. The call sends the submission.review.update event.
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. 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
}
The answer is 201. The call sends the submission.review.update event.
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"]
}
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).
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, a stage that does not belong to the project, a project with no Rejected stage). |
403 |
The key lacks the write_pipeline scope (API key missing required scope: write_pipeline), or, for add, move and reject, the person who created the key has no access to that hiring project. |
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. 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: it can be ignored (you get the first page) or refused with an error. 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
}
This page holds fewer events than limit, so there is no later page and next_cursor is null. When more events exist, next_cursor is a long piece of text for you to pass back as cursor.
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.
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. - 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?email=jane@example.com returns your newest candidates, not Jane: the filter is q. 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.
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"
}
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 Showcase, 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 (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). |
api_key_has_no_acting_user |
400 | The key has no user and no creator to send a test, or create a project, as (sections 4.6.2 and 3.5). |
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). |
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). |
idempotency_key_reused |
409 | The idempotency_key of a create-project call was already used with a different body. Nothing was created (section 3.5.3). |
idempotency_request_in_progress |
409 | A create-project call with this key is still building the job post. Wait a few seconds and send the same request again (section 3.5.3). |
project_create_limit_reached |
429 | 10 projects a minute or 200 a day reached for the organization. Nothing was created (section 3.5.4). |
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). |
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). |
user_banned |
403 | The person who created the key has been blocked from Flowmingo. Contact support. |
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 or Technical Test request that breaks its rules (section 4.6). message says which. |
401 |
The key was not accepted: it is missing, malformed, unknown, deleted or disabled (section 11). |
403 |
The key is valid but expired, or it lacks a scope the route needs. |
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), or a create-project call reuses an idempotency_key (section 3.5.3). |
429 |
The daily limit for a Skill Test or Technical Test (section 4.6.6), or the limit on creating projects (section 3.5.4), is reached. These are volume caps, not a general request rate limit. |
503 with a code |
The daily limit for a Skill Test or Technical Test, or for creating projects, or the role of the key creator, could not be checked. Nothing was sent or created. Retry shortly (sections 4.6.6 and 3.5). |
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 (section 4.5).
8.5 Limits
- There is no general request rate limit on the public API today. Responses carry no rate-limit headers. (Creating projects has its own volume caps, below.) That is a fact about today, not a promise about tomorrow: write your client so that it slows down and retries later when it receives a
429or a5xx, and space out bulk imports. - 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 and Technical Tests have their own daily caps of the same size: 1000 a day for one test 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 (section 4.6.6). - Creating projects has its own caps: 10 a minute and 200 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). - Page size is capped: 100 rows on the list routes, 200 events on the events feed.
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 only the create-project route takes an idempotency key (section 3.5.3):
| 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. |
| 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. |
| Add to a hiring project | Refused with 400 (already in the project), so no duplicate. |
| 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 | The rating ends up the same, because the new rating replaces the earlier one. |
| Shortlist | The state ends up the same. Each call also adds an entry to the result's activity. |
| Add a note | Not safe. Each call adds another note. |
| Create a hiring project | Safe with the same idempotency_key and the same body: you get the original project back, with idempotent_replay true (section 3.5.3). A different key creates a second project. |
| Create an interview set | Not safe. Each call creates another set. |
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.
9. All 35 operations
Every path is relative to https://apis.flowmingo.ai/company/integration. The OpenAPI document describes the same 35 operations, each with its summary, scope, parameters 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 and invite candidates
| 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. | 3.5 |
POST /interview/set/v1 |
create_set |
Creates an assessment (an AI interview or a CV evaluation) with its questions and criteria. | 3 |
POST /interview/candidate/invite/v1 |
invite_candidates |
Adds candidates and emails each an invitation to an AI interview, a role-play, a Skill Test or a Technical Test, or orders a CV evaluation from a cv_link. |
4, 4.6 |
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. | 5.6 |
GET /hiring/job-posts/{id}/v1 |
read_hiring |
One job post with its description. | 5.6 |
GET /hiring/interviews/upcoming/v1 |
read_hiring |
Invitations candidates have not completed yet. | 5.6 |
GET /hiring/interview-sets/v1 |
read_hiring |
Lists your assessments with their assessment_type. |
5.6 |
GET /hiring/projects/v1 |
read_hiring |
Lists your hiring projects. | 5.6 |
GET /hiring/projects/{projectId}/stages/v1 |
read_hiring |
The pipeline stages of one project. | 5.6 |
GET /hiring/applications/v1 |
read_hiring |
Lists the candidates in your hiring projects, with stage and scores. | 5.6 |
GET /hiring/submissions/v1 |
read_hiring |
Lists assessment results of every kind. | 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/candidates/v1 |
read_candidates |
Lists candidates with name, email and CV link; search with q. |
5.6 |
GET /hiring/candidates/{id}/v1 |
read_candidates |
One candidate. | 5.6 |
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. | 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. | 6.4 |
POST /hiring/submissions/{id}/rate/v1 |
write_pipeline |
Rates a result from 0 to 10. | 6.5 |
POST /hiring/submissions/{id}/shortlist/v1 |
write_pipeline |
Shortlists or un-shortlists a result. | 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. | 7 |
9.6 Partner endpoints for ATS integrations
These ten operations are called by the Recruitee, Greenhouse and Zoho Recruit integrations with your API key. Their request and response formats follow what those partners define, and they 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: lists the assessments it can send. |
POST /ats/greenhouse/send_test |
invite_candidates |
Greenhouse: sends an assessment to a candidate. |
GET /ats/greenhouse/test_status |
read_hiring |
Greenhouse: reads the status of an order. |
POST /ats/greenhouse/request_errors |
none | Greenhouse: 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. |
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). 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 emails that person an invitation to an AI interview or a role-play. (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.
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. This recipe sends an interview. A Skill Test or a Technical Test can be sent the same way with the extra rules in section 4.6 (one test per request, an email for every candidate), and a Showcase, 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 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 sends the email. 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, and an invitation email in your inbox. The answer has your invitation under results, in invitations (section 4.5).
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.****). |
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 Showcase, 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 invitation_message must be at least 10 characters |
The message is too short, or empty. | Make it at least 10 characters, or remove the field. |
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. | Open the answer and read results: error_message or invitation_error_messages say why (for example, Email already invited). Check the spam folder too. |
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, unexpired, then correctly scoped. The first failure is the one reported, so an expired key reports expiry even if something else is also wrong:
| 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, or it was deleted or disabled. 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. |
There is no separate "enable API access" setting on an account: a key that exists, is active, has not expired and carries the right scope works.
- 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 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. - 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. - 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 building the job post; the limit of 10 a minute or 200 a day is reached; or a check could not be made. None of them creates a project. After the409idempotency_request_in_progressthe project from the first call already exists, so wait and send the same request again to get it (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. - 400
assessment_type_not_invitable_via_api(invite): the request names a Showcase, Recording evaluation, Questionnaire or question bank. Only AI interviews, role-plays, Skill Tests and Technical Tests can be invited, and CV evaluations ordered, through the API (section 4.2). Nothing was created. - 400 with a code beginning
assessment_test_,project_required,project_ambiguousorapi_key_has_no_acting_user(invite): a Skill Test or Technical Test 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. - 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 or Technical Test 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_queuedmeans it was emailed earlier.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). - 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). - Validation errors (set):
titleis required.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).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. 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 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).
October 2026
- 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, 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 200 a day per organization. The public API now has 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 sends them too. - 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.