Integrations

API Integration Guide

Authenticate with Flowmingo, request API keys, and trigger async interview invitations directly from your ATS or backend workflows.

Need help integrating?

Stuck on a step or need technical support? Our team is happy to help — reach out any time and we'll get you connected.

API Key Integration

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 as GET /me/v1 in this guide means GET 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 at https://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

  1. Sign in at https://team.flowmingo.ai, click your email at the bottom-left of the sidebar, then Settings.
  2. Open Integrations -> API Keys and click Create New Key.
  3. Type a Name. The name is required.
  4. 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.
  5. Click Create secret key.
  6. 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

  1. Sign in at https://team.flowmingo.ai.
  2. Click your email at the bottom-left of the sidebar, then Settings.
  3. 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

  1. Click Create New Key.
  2. In the Create New API Key window, type a Name. The name is required. The Description is optional.
  3. Tick the permissions (scopes) the key needs. Tick at least one: see section 2.3.
  4. 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, plus read_candidates if the report needs names.
  • Starting assessments from a form or an ATS: invite_candidates alone.
  • Syncing stages with an ATS: read_hiring and write_pipeline.
  • Opening a role from your own system: create_project to create the hiring project, create_set to attach an assessment to it, and invite_candidates to invite into it.
  • An AI assistant (MCP): read_hiring and read_candidates, plus write_pipeline and invite_candidates if it may act. The assistant does not use create_set or create_project.

Candidate names and email addresses require read_candidates. read_hiring alone returns ids, scores and evaluations. Without read_candidates, candidate_name, firstname, lastname and email are left out of the applications, results and result-detail responses, and candidate_email and source_detail are removed from events. GET /me/v1 shows 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.n8G3zPNw9gWkK1Kj
  • Authorization: Bearer fl_live_abcd12345.n8G3zPNw9gWkK1Kj. This is accepted when the value starts with fl_, which every Flowmingo key does. Any other bearer value, such as a session token, is not an API key and answers 401 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" }
    ]
  }'

3.4 Sample response

The endpoint returns a results envelope.

Treat the response as an acknowledgement, not a confirmation. results describes the set itself. It does not return the questions, criteria or access entries you sent (so none of their ids), and it does not repeat project_id. It also does not show when a value was stored differently from how you sent it: a cfg_importance_id outside the four valid IDs, for example, is stored as 244. To confirm that the set was linked to your project, find it in GET /hiring/interview-sets/v1 and read its project_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 description when you send one (it also reads the location, level and employment type out of the text), or from the position alone when you do not, which gives a weaker post. The post is saved active with a public link, titled with the position, 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, position and description: you get the project the first call created, with idempotent_replay true (and the same 201). 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, position or description: 409 with the code idempotency_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 deleted if someone has since deleted it) and with an edited body is always a 409. 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 as ats-job-48213-r2.
  • A call that fails with a 5xx or 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 answers 409 idempotency_request_in_progress for up to five minutes, and after that returns the project with job_post_error job_post_interrupted.
  • Two calls with the same key at the same moment: one project is created; the other call is given that project, or 409 idempotency_request_in_progress while 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 answered 429 before 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"
  }'

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 400 with each 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 example Interview 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": false works. "send_invite": "false" is refused with 400 (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 leave send_invite out: it defaults to true.
  • Daily limits. While send_invite is true, 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 with 400 and a message that begins Rate limit exceeded. The day is the calendar day on our server clock, not in your time zone. The limits are not checked when send_invite is false. 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, with 429 and 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
  }'

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: "; a candidate who already has an application in that project keeps it, and keeps its source. Nothing is charged when you send (section 4.6.7). The request is the one in section 4.3 with the rules below, and the answer has a different shape (sections 4.6.4 and 4.6.5).

4.6.1 The request

  • One test per request. com_interview_set_ids (or the deprecated com_interview_set_id) holds that one id and nothing else. A test beside any other assessment, or two tests, is refused with 400 and the code assessment_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 400 and the code skill_test.variants.published_required or technical_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 deprecated com_project_id. If you name none, the test's single active project is used. A test that is attached to no active project answers 400 with the code project_required, and one that is attached to several answers 400 with project_ambiguous: send the id of the project you mean. Naming two projects answers 400 with assessment_test_requires_single_project. A test needs no job post, so com_job_post_ids is not used (an id that is not yours is still a 404).
  • Every candidate needs an email, at most 100 candidates per call. A candidate without an email is refused with 400 and the code assessment_test_email_required (the message names the row, for example candidates[2]), and more than 100 candidates with assessment_test_too_many_candidates. A CV link alone does not invite anyone to a test. name, firstname, lastname and cv_link are used. source and thumb_file_link are not. An address that ends in @placeholder.local, or a preview address, is refused with 400 and the code skill_test_invite.placeholder_recipient_not_allowed or technical_test_invite.placeholder_recipient_not_allowed.
  • A candidate row must be well formed. Each entry of candidates must be a JSON object. An email, when given, must be a valid address (spaces around it are ignored). Send name, firstname, lastname, cv_link, thumb_file_link and source as JSON strings, and keep < and > out of a name. A row that breaks a rule is refused with 400, the message names the row and the first problem (for example candidates[2]: email must be an email or candidates[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 blank email is still refused with assessment_test_email_required; leave out optional fields you do not use. A list of more than 100 candidates is refused with assessment_test_too_many_candidates before the rows are checked.
  • Fields a test does not take. A value in ats_candidate_id (on any candidate), a value in metadata and a practice_mode other than null are refused with 400 and the code assessment_test_field_not_supported, and nothing is created. A null in 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_mode is the field for a practice invitation to an AI interview, and it does not apply here.
  • Fields a test does take. invitation_message is 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_ids are files available to you (otherwise 400 with the code attachment_not_yours). send_invite defaults to true; false creates the invitations without emailing anyone, and a later call with send_invite true emails 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 with send_invite false, this call emails it.
  • If the candidate has started or finished the test, nothing is sent (skipped_already_started, skipped_attempt_in_progress or skipped_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; status shows how it stands. A Technical Test invitation that was never emailed and still reads sent or opened can be emailed even after its expires_at has passed, so check expires_at before 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_progress or already_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_existing true does 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_progress and 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 after failed:timeout_possibly_delivered, after any other failed: value and after a delivery_in_progress that 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 429 with the code assessment_test_invite_limit_reached, and nothing is sent. This 429 is 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 503 with the code assessment_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_score is out of 10, and so is every competencies[].rating. It is null when there is no usable number: score_status says which: scored, cannot_evaluate (the AI ran and could not rate the attempt) or not_scored (no score yet). A cannot_evaluate is not a zero. A scored status 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).

  • status is a number: 0 inactive, 1 active (complete), 2 pending, 3 archived, 4 incomplete, 5 needs interview (invited, not started), 6 retaking.

  • assessment_type says what kind of assessment the result is from. Read it before you compare scores, because the scales differ:

    assessment_type Assessment overall_score
    ai_interview AI interview (an older interview with no recorded type reads as this) The AI's rating, 0-10.
    role_play Role-play The AI's rating, 0-10.
    cv_evaluation CV evaluation The AI's rating of the CV, 0-10.
    recording Recording evaluation The AI's rating, 0-10.
    skill_test Skill test The test percentage divided by 10, with score_percent (0-100) beside it once the report is released.
    technical_test Technical 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).
    showcase Showcase null: a showcase is never scored by the AI.
    questionnaire Questionnaire null: a questionnaire is never scored by the AI.
    unknown The 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 in assessment_type beside the raw set_type number.

  • evaluation holds 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_cv is the separate CV evaluation attached to an interview set. Either can be null. 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: competencies is omitted entirely when there are no per-criterion rows, so test if (evaluation?.competencies) rather than comparing with []. Both objects may also carry rating_status: "cannot_evaluate", meaning the AI ran but could not rate the attempt.

  • competencies[].name is the title of each iai_requirements[] item you sent. Name them whatever you like; they come back under those names. A requirement you sent with an empty title is 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) and overall_score are out of 10. The rubric is out of 5: every rubric row repeats score_max so 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_cursor of one answer as cursor of the next to read older events, and stop when next_cursor is null. There is no "since" parameter: to read only what is new, remember the event_id values 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_email and source_detail are taken out of data. 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_id values 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/json with a body. Use the exact field names documented for each request and response. Business data uses snake_case; error responses use statusCode (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_id and 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 answers 400 instead 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 with 400 on 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 429 or a 5xx, 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_invite is true. 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 429 with the code assessment_test_invite_limit_reached and 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 answers 429 with the code project_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-Key with 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 header Content-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 on localhost) 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 to candidate_name, Email to email, Assessment to assessment_name, Type to assessment_type, Score to overall_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, URL https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1. Put the JSON below in Data, and add the headers Content-Type with the value application/json and X-API-Key with your key.
  • Make: HTTP -> Make a request. Method POST, the same URL, headers X-API-Key and Content-Type, Body type Raw, Content type JSON (application/json), and the JSON below as the request content.
  • n8n: an HTTP Request node. Method POST, the same URL, Send Headers with X-API-Key, Send Body on, Body Content Type JSON, Specify Body Using 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 expects true, false or a number is refused. That is why the steps above use "Custom Request", "Raw" and "Using JSON".
  • Leave send_invite out. It defaults to true, which sends the email. If you do include it, write true or false without 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, name and email included. 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 q to find a candidate, and check the rows before you act on them.
  • 404 Interview set ... not found, Project ... not found or Job 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, 409 idempotency_key_reused or idempotency_request_in_progress, 429 project_create_limit_reached, 503 project_create_limit_unavailable (create a project): the person who created the key is not an Owner or Admin; the idempotency_key was 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 the 409 idempotency_request_in_progress the 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_post is null (create a project): read job_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. With job_post_error null, 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_ambiguous or api_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 in ats_candidate_id, metadata or practice_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_reached or 503 assessment_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_existing true: the invitation already existed when this row was handled. Read status and email_delivery. queued means this call handed it to the mail service (for example an earlier call created it with send_invite false); 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 says queued again. already_queued means it was emailed earlier. skipped_already_started, skipped_attempt_in_progress or skipped_not_unstarted mean 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_progress or failed: followed by a reason: the answer does not say whether the email went out (the one exception is failed:skill_test_invite.recipient_unsubscribed or failed: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 email or candidates[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_invite arrived as text, for example "false" from a tool that sends every field as a string. Send a real JSON true or false, or leave the field out.
  • 400 rating must be a number (a JSON number, not text), or shortlisted 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 candidates is non-empty and each candidate has enough data for your desired flow (invite vs CV-entry queue). A 201 answer can still carry a message for a single candidate: read results (section 4.5).
  • Validation errors (set): title is required. question_mode and question_type are optional (omit them and you get an interview question), but if you DO send one it must be a valid value (pre_screening | interview, and text | checkbox | radio | file). question_type only takes effect on a pre_screening question. Also check max-length limits on string fields, that set_type, interview_duration, number_of_retakes and priority are JSON numbers, and that project_id (if sent) is a valid UUID owned by your organization.
  • 400 Your plan allows up to N minutes (set): interview_duration is 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_id against the four IDs in section 3.2.2. A value outside that set is stored as 244 (Good To Have) rather than rejected.
  • Duplicate ATS IDs in one payload: Ensure candidates[].ats_candidate_id values 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.completed event for a technical test never carries a score, and its overall_score can 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/v1 needs the new scope create_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 required idempotency_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.completed event for a Technical Test never carries a score. score and score_percent are always null for 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 from GET /hiring/submissions/{id}/v1 or GET /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 the 400 will 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 in ats_candidate_id or metadata, and no practice_mode. Each entry of results carries assessment_invites (with email_delivery, reused_existing and status) 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, answering 429 (assessment_test_invite_limit_reached) or 503 (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. The invite_candidates tool 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 400 and the code assessment_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 answered 2xx, 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 as true (so send_invite: "false" sent the invitation emails and shortlisted: "false" shortlisted the candidate) and rating: "" was read as 0. They are now refused with 400.
  • 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 404 and nothing is created.
  • All eight assessment types are labelled. role_play, technical_test, recording and questionnaire used to read unknown. The results list and the assessments list accept assessment_type as a filter, and the assessments list returns assessment_type beside set_type. Questionnaires are left out of the assessments list unless you ask for assessment_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-key scheme.
  • Webhooks: the new assessment.completed event, sent for skill tests, technical tests, showcases, questionnaires, recording evaluations and CV evaluations (a recording evaluation and a CV evaluation send it as well as interview.evaluation.update, on purpose; AI interviews and role-plays send only their existing events); application.created is 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, applications and submissions and their *_error_messages maps. 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_candidates scope. A key with read_hiring alone no longer receives them.
  • September: an invite that names candidates by ats_candidate_id must also name an assessment.
  • September: POST /hiring/applications/{id}/reject/v1 moves the application into the project's Rejected stage. It used to remove the candidate from the hiring project.
  • October: question_type on a create-set question only takes effect on a pre_screening question.