Flowmingo exposes a REST API so your ATS, CRM, or internal tooling can drive the recruiting pipeline programmatically: invite candidates to interviews, read your applicants and their results, and move people through your hiring stages. This guide takes a developer from zero to a working integration.
Base URL (production): https://apis.flowmingo.ai/company
Auth: every request sends your secret key in the X-API-Key header.
Format: JSON in, JSON out. All field names are snake_case.
The API operates on the hiring configuration you create in the Flowmingo UI — it references your interview sets, job posts, and projects by id. You author that configuration in the product; the API invites against it, reads it, and moves candidates through it. It does not create interview sets, job posts, or projects.
Each endpoint below lists its method and path relative to that base URL — for example
GET /integration/me/v1 means GET https://apis.flowmingo.ai/company/integration/me/v1. Prepend the
base URL to every path.
1. Prerequisites
Before you write any code you need three things:
- Workspace access — a Flowmingo company account with admin rights, so you can create API keys (see Create an API key).
- The ids you'll target — at minimum an interview set id (
com_interview_set_id) to invite candidates to. You don't have to copy ids by hand: list them with the read endpoints in Read your pipeline (scope read_hiring). A project id (com_project_id) and job post id (com_job_post_id) are optional. - CV hosting (only if you send CVs) — a CV is passed as a public HTTPS URL (
cv_link) that Flowmingo fetches, so host it on publicly reachable storage or a signed URL.
2. Create an API key
2.1 Open the panel
- Sign in at
https://team.flowmingo.ai. - Open the avatar menu (top right) → Account Settings. This opens the Settings dialog.
- In the dialog's left sidebar, under INTEGRATIONS, click API Keys.
2.2 Create the key
- Click Create New Secret Key.
- Enter a Name and Description.
- Select the scopes the key needs (see the table in Scopes). A key only works for the operations its scopes allow.
- Click Generate.
You will see the full secret once, in the form <prefix>.<secret> — for example
fl_live_abcd12345.n8G3zPNw9gWkK1Kj. Copy it immediately and store it in a secret manager. After you
close the dialog it is masked (fl_live_abcd12345.****) and cannot be retrieved again.
Key prefixes and environments. The prefix tells you which environment a key belongs to, and each environment has its own base URL. Use the matching pair:
| Prefix | Environment | Base URL |
|---|---|---|
fl_live_… |
Production | https://apis.flowmingo.ai/company |
fl_pre_… |
Staging (PRE) | https://pre-apis.flowmingo.ai/company |
A fl_pre_ key will not authenticate against the production host and vice versa. The rest of this
guide shows production URLs; on staging, swap the host for pre-apis.flowmingo.ai.
2.3 Scopes
| Scope | Grants |
|---|---|
invite_candidates |
invite candidates to interviews (Invite candidates) |
read_hiring |
read job posts, interview sets, applications, stages, and submission results (Read your pipeline (scope read_hiring)) |
write_pipeline |
add candidates to a pipeline, move stages, reject (Operate the pipeline (scope write_pipeline)) |
A key may hold any combination. A key created with no scopes is rejected on every scoped endpoint
(403). The introspection endpoint in Verify a key works with any valid key, regardless of scopes.
Introspection may also report additional scopes (for example create_set) that are reserved for features
outside this guide — ignore any scope not listed in the table above.
2.4 Verify a key
GET /integration/me/v1
Confirm a key is accepted and see which org and scopes it carries, in one request. This needs no
scope, so even an invite_candidates-only key (which cannot call the read endpoints) can check itself.
curl https://apis.flowmingo.ai/company/integration/me/v1 -H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj'
{ "api_key_id": "019c-…", "organization_id": 123, "scopes": ["invite_candidates","read_hiring"], "expires_at": null }
A valid key returns 200; a missing or bad key returns 401. This is the fastest way to isolate a
credential problem before wiring real calls.
2.5 Rotate / revoke
To rotate, create a new key, update your integration, then delete the old one. Deleting a key revokes it immediately. Never embed keys in front-end code, logs, or version control.
3. Invite candidates
POST /integration/interview/candidate/invite/v1 — scope invite_candidates
Invites one or more candidates to an interview set (and/or adds them to a project's pipeline).
3.1 Request body
| Field | Type | Required | Notes |
|---|---|---|---|
com_interview_set_id |
UUID | conditional | Required unless com_project_id is given. Omitting both returns 400. |
com_project_id |
UUID | conditional | Required unless com_interview_set_id is given; uses the project's latest job post unless com_job_post_id is set. |
com_job_post_id |
UUID | optional | Pin a specific job post within the project. |
candidates |
array | yes | At least one (empty array → 400). Each candidate object uses the fields below. |
candidates[].email |
string | optional* | The candidate's email. *Required to send an invitation email; if omitted but cv_link is present, the candidate is queued for CV processing instead. |
candidates[].firstname |
string | optional | Candidate's first name. |
candidates[].lastname |
string | optional | Candidate's last name. |
candidates[].cv_link |
string (https URL) | optional | Public CV URL Flowmingo fetches for evaluation. |
candidates[].ats_candidate_id |
string | optional | Your own candidate id; echoed back in the response so you can correlate. |
invitation_message |
string | optional | Custom HTML message, 10–5000 characters (a 1–9 char message → 400). Supports {{candidate_name}}, {{position_name}}, {{company_name}}, {{interview_link}}. |
send_invite |
boolean | set explicitly | true creates an invitation record and sends the invitation email; false creates only the submission record (no email, no invitation). Pass it explicitly — if you omit it you currently get the false behavior (no email/invitation), so do not rely on a default. |
3.2 Example request
curl -X POST https://apis.flowmingo.ai/company/integration/interview/candidate/invite/v1 \
-H 'X-API-Key: fl_live_abcd12345.n8G3zPNw9gWkK1Kj' \
-H 'Content-Type: application/json' \
-d '{
"com_interview_set_id": "019c5ea9-a792-7f0d-b671-4c3c1602094e",
"candidates": [
{ "firstname": "Alex", "lastname": "Doe", "email": "alex@example.com",
"cv_link": "https://storage.googleapis.com/acme/cv/alex.pdf",
"ats_candidate_id": "ats-1001" }
],
"invitation_message": "<p>Dear {{candidate_name}}, please complete your interview: {{interview_link}}</p>",
"send_invite": true
}'
3.3 Response
The endpoint returns 201 Created with { "results": [ ... ] }, one entry per candidate. (All write
endpoints in this API — invite and the pipeline writes in Operate the pipeline (scope write_pipeline) — return 201 on success, not 200. Assert
2xx, or === 201, not === 200.)
{
"results": [
{
"email_address": "alex@example.com",
"ats_candidate_id": "ats-1001",
"tal_candidate_id": "019c-cand",
"record_id": "019c-rec",
"submissions": [ {
"id": "019c-sub",
"status_text": "not_started",
"com_interview_set_id": "019c5ea9-…",
"tal_candidate_id": "019c-cand",
"email": "alex@example.com",
"firstname": "Alex",
"lastname": "Doe",
"code": "…",
"created_at": 1782531392849
} ],
"submission": { "id": "019c-sub", "status_text": "not_started", "…": "…" },
"invitations": [ { "id": "019c-inv", "email": "alex@example.com", "status_text": "pending" } ],
"invitation": { "id": "019c-inv", "email": "alex@example.com", "status_text": "pending" }
}
]
}
Which fields you get back depends on what the invite created:
submissions/submissionare always present when you invite to an interview set.submissionis simplysubmissions[0], kept for backward compatibility. This is the field to key your integration off — a submission record always exists. Usesubmission.idto read results later (Get a submission's results).invitations/invitationare conditional. An invitation record is created only whensend_inviteis explicitlytrue(which also sends the email). Withsend_invite:false— or if you omitsend_invite(see Request body: the default is not applied) — bothinvitationsandinvitationare absent (the keys are not present at all — not an empty array, so don't writeresult.invitations.lengthunguarded). The interview set's status does not affect this. Always readsubmission; readinvitationonly when you sentsend_invite:true.
Timestamp note: timestamps inside the invite response (e.g.
submission.created_at) are epoch milliseconds (a number like1782531392849). The read endpoints in Read your pipeline (scope read_hiring) instead return ISO-8601 strings (e.g."2026-03-04T01:00:00.000Z"). Parse each accordingly.
The submission object here reports state via status_text (a string such as "not_started") — it
has no numeric status key (that appears on the read endpoint in Get a submission's results). It also includes operational
fields such as is_try, practice_mode, unlock_expires_at, unlocked_at, retake_expires_at,
retaked_at, and result_email_scheduled_at. Treat the field set as open — read the fields you need by
name rather than asserting an exact shape.
Partial / per-candidate errors. When one candidate in a batch fails (the rest still succeed), the
failing result carries per-target maps keyed by set/project id: invitation_error_messages,
submission_error_messages, application_error_messages, plus the legacy singular
invitation_error_message / submission_error_message. A whole-request failure (bad auth, malformed
body, missing ids) instead returns a top-level error envelope — see Troubleshooting.
4. Read your pipeline (scope read_hiring)
List endpoints return { "data": [ ... ], "total", "page", "limit" }; detail endpoints return the object
directly. All reads are scoped to your organization and never include candidate phone numbers, raw
transcripts, or raw answers. page defaults to 1; limit defaults to 20 (max 100). Timestamps are
ISO-8601 strings.
Nullable fields. Examples below show populated values, but
stage,score,overall_score,status,com_job_post_id, andsubmission_atmay benulldepending on pipeline state. Code defensively.
4.1 List job posts
GET /integration/hiring/job-posts/v1 · Query: com_project_id (optional), page, limit.
{ "data": [ { "id": "…", "title": "Senior Engineer", "status": 1, "com_project_id": "…", "created_at": "2026-03-04T01:00:00.000Z" } ], "total": 12, "page": 1, "limit": 20 }
4.2 List interview sets
GET /integration/hiring/interview-sets/v1 · Query: page, limit.
{ "data": [ { "id": "…", "title": "Sales Assessment", "status": 1, "set_type": 1, "created_at": "…" } ], "total": 7, "page": 1, "limit": 20 }
Use a returned id as com_interview_set_id when inviting (Invite candidates). status and set_type are internal
integers (see the integer-status note in Get a submission's results) — you generally only need the id. Whether an invite
produces an invitation record depends on send_invite:true, not on the set's status (see Response).
4.2.1 List hiring projects
GET /integration/hiring/projects/v1 · Query: page, limit. Returns your hiring projects so a
com_project_id is discoverable directly (you don't have to find it via a job post or application that
happens to reference it).
{ "data": [ { "id": "…", "title": "Backend Engineer", "created_at": "…" } ], "total": 9, "page": 1, "limit": 20 }
4.3 List applications (pipeline)
GET /integration/hiring/applications/v1 · Query: com_project_id, com_job_post_id, com_stage_id
(all optional), page, limit.
{ "data": [ {
"id": "…", "tal_candidate_id": "…", "com_project_id": "…", "com_job_post_id": "…",
"stage": { "id": "…", "name": "Screening" }, "score": 7.4, "status": 1, "created_at": "…"
} ], "total": 53, "page": 1, "limit": 20 }
stage is null until the candidate is in a stage; score is null until scored. To enumerate all
stages of a project (including empty ones), use List a project's stages — don't infer the stage list from this endpoint.
4.4 Get a submission's results
GET /integration/hiring/submissions/{id}/v1
{ "id": "…", "status": 1, "com_interview_set_id": "…", "tal_candidate_id": "…",
"overall_score": "8.50", "submission_at": "…", "created_at": "…" }
overall_score is the AI evaluation score out of 10, returned as a numeric string (e.g. "8.50") or
null until evaluated — parse it with parseFloat. status is an internal integer (see the note below).
A submission id from another organization returns 404.
Where do submission ids come from? Three sources: the invite response (submission.id, Response), the
submissions list below (List submissions), and webhook payloads (the submission_url on
interview.evaluation.update events — the submission id is the last path segment of that URL).
Integer
statusfields.statuson submissions, job posts, and applications is an internal enum (values like0,1,2,5) that is not part of the public contract. Build on the meaningful fields instead:overall_scorefor submissions andstagefor applications. (The submission read endpoints return only the numericstatus; the human-readablestatus_textappears on the invite-response submission, not on these reads.)
4.5 List submissions
GET /integration/hiring/submissions/v1 · Query: com_interview_set_id, tal_candidate_id (both
optional filters), page, limit. Returns the same per-submission shape as Get a submission's results. Use this to discover
submission ids for an interview set or candidate.
{ "data": [ { "id": "…", "status": 1, "com_interview_set_id": "…", "tal_candidate_id": "…",
"overall_score": "8.50", "submission_at": "…", "created_at": "…" } ], "total": 4, "page": 1, "limit": 20 }
4.6 List a project's stages
GET /integration/hiring/projects/{com_project_id}/stages/v1
Returns every pipeline stage of the project, in pipeline order. These are the valid com_stage_id values
for move-stage (Operate the pipeline (scope write_pipeline)).
{ "data": [
{ "id": "…", "name": "Applied", "priority": 1, "com_project_id": "…" },
{ "id": "…", "name": "Screening", "priority": 2, "com_project_id": "…" },
{ "id": "…", "name": "Final", "priority": 3, "com_project_id": "…" }
] }
A project id outside your organization (or one that doesn't exist) returns 200 with an empty data
array — this list endpoint is the one exception to the general "unknown id → 404" rule in Troubleshooting.
5. Operate the pipeline (scope write_pipeline)
Paths are relative to the base URL (§intro). All three return 201.
| Operation | Method & path | Body |
|---|---|---|
| Add a candidate to a pipeline | POST /integration/hiring/applications/v1 |
{ tal_candidate_id, com_project_id, com_job_post_id?, com_stage_id? } |
| Move to another stage | POST /integration/hiring/applications/{id}/move-stage/v1 |
{ com_stage_id } |
| Reject | POST /integration/hiring/applications/{id}/reject/v1 |
— |
All three return 201 Created with a minimal confirmation: add → { id, com_project_id, com_job_post_id, com_stage_id, status }, move → { id, com_stage_id }, reject → { id, status: "rejected" }. Operating on an id outside your organization returns 404 (e.g. "Application not found",
"Hiring project not found", "Candidate not found") — including reject on an unknown application. A
com_stage_id must belong to the target project (for move-stage, the application's project); a
non-existent or cross-project stage returns 400. List valid stages with List a project's stages. Likewise, a
com_job_post_id passed to add must belong to com_project_id — a mismatched one returns 404
("Job post not found or does not belong to this project").
Adding a candidate who already has an active application in that project returns 400
("Candidate has already applied to this project") — treat that as an expected "already exists" case in
idempotent sync, not a malformed request. (A candidate you previously rejected can be re-added.)
6. End-to-end example
KEY='fl_live_abcd12345.n8G3zPNw9gWkK1Kj'; API='https://apis.flowmingo.ai/company'
# 0. confirm the key works and see its scopes
curl -s "$API/integration/me/v1" -H "X-API-Key: $KEY" | jq '{org: .organization_id, scopes}'
# 1. find an interview set
SET=$(curl -s "$API/integration/hiring/interview-sets/v1" -H "X-API-Key: $KEY" | jq -r '.data[0].id')
# 2. invite a candidate to it — read submission.id (always present), not invitation.id (conditional)
curl -s -X POST "$API/integration/interview/candidate/invite/v1" -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d "{\"com_interview_set_id\":\"$SET\",\"send_invite\":true,\"candidates\":[{\"firstname\":\"Alex\",\"email\":\"alex@example.com\"}]}" \
| jq '.results[0].submission.id'
# 3. read the pipeline
curl -s "$API/integration/hiring/applications/v1?limit=20" -H "X-API-Key: $KEY" | jq '.total'
7. Troubleshooting
Endpoint-level errors return a JSON envelope: { "statusCode": 404, "message": "Submission not found", "timestamp": "…", "path": "…" }. (This is distinct from the per-candidate *_error_messages maps in a
partially-successful invite — see Response.)
| Status | Meaning |
|---|---|
400 |
Validation error — e.g. neither com_interview_set_id nor com_project_id given, empty candidates, or invitation_message outside 10–5000 chars. Returns a clean JSON message. |
401 |
Missing/malformed/deleted/inactive/expired key, or X-API-Key header absent. |
403 |
The key lacks the scope the endpoint requires (e.g. a read_hiring-only key calling a pipeline write). Check scopes via Verify a key. |
404 |
The id is not in your organization, or doesn't exist — e.g. a well-formed but unknown com_interview_set_id / com_project_id on invite, or an unknown application on a pipeline write. |
Want to be notified when things happen (emails delivered, interviews completed, evaluations scored)? See the Webhook Integration guide.