Outcome
Create a candidate with source attribution, optionally adding them to a role’s Applied stage.
Email is unique within the workspace. Source defaults to manual; use sourceDetail for a referrer or other attribution. Hiring managers must supply one of their own roles.
If the destination stage has connected Forms or Survey steps, Recruit may request notification emails with the candidate’s personal links. Inspect the returned steps result; a successful recruitment operation or a reported email status is not a mailbox delivery receipt.
Before you start
- Verify identity and the pinned organization context, then discover the exact action and a granted Recruit workspace.
- The required application permission is workspace:write. Hiring-team roles and record visibility are enforced separately; a workspace grant alone does not grant every Recruit action.
- Organization and workspace identity come from verified context. Pass the workspace as resourceType/resourceId, separate from action input; request-body organizationId/workspaceId values cannot select an authorized scope.
- The first authorized action can initialize an identity-only workspace mirror and invoke the configured workspace-created callback. It does not create an Auth workspace or grant new access.
Recruiters can perform the full hiring workflow. Hiring managers see their managed roles and candidates; interviewers see assigned interviews and their candidates. Workspace owners and organization administrators act as recruiters. Use team.me to read the caller's effective role and allowed actions.
Discover the target
topolo whoami --json
topolo actions get app_topolo_recruit.candidates.create --detail --json
topolo resources topolo-recruit --json
topolo workspaces list topolo-recruit --json
topolo actions get app_topolo_recruit.team.me --detail --json
topolo actions get app_topolo_recruit.candidates.list --detail --json
topolo actions get app_topolo_recruit.roles.list --detail --json
Request
POST /api/candidates
Save input as payload.json. CLI and MCP action input includes path identifiers. The reviewed plan separates path/query fields from the HTTP body. Set RECRUIT_WORKSPACE_ID from granted resource discovery.
Inspect and test this action in the signed action console. Use its discovered schema and plan; a test response still requires the verification described below.
Input fields
| Field | Required | Type | Constraints |
|---|---|---|---|
firstName | Required | string | {"minLength":1,"maxLength":80} |
lastName | Required | string | {"minLength":1,"maxLength":80} |
email | Required | string | {"maxLength":254,"pattern":"^(?:[A-Za-z0-9_'+\-]+\.)[A-Za-z0-9_'+\-][A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$"} |
phone | Optional | Union | {} |
location | Optional | Union | {} |
profileUrl | Optional | Union | {} |
tags | Optional | array | {"maxItems":20} |
source | Optional | string | {"enum":["manual","sourced","referral","agency"]} |
sourceDetail | Optional | Union | {} |
roleId | Optional | string | Role id from roles.list. Hiring managers must select a role they manage when creating a candidate. {"minLength":1,"maxLength":64} |
Exact input schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"lastName": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"email": {
"type": "string",
"maxLength": 254,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"phone": {
"anyOf": [
{
"type": "string",
"maxLength": 40
},
{
"type": "null"
}
]
},
"location": {
"anyOf": [
{
"type": "string",
"maxLength": 160
},
{
"type": "null"
}
]
},
"profileUrl": {
"anyOf": [
{
"type": "string",
"maxLength": 500,
"pattern": "^https?:\\/\\/"
},
{
"type": "null"
}
]
},
"tags": {
"maxItems": 20,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 40
}
},
"source": {
"type": "string",
"enum": [
"manual",
"sourced",
"referral",
"agency"
]
},
"sourceDetail": {
"anyOf": [
{
"type": "string",
"maxLength": 200
},
{
"type": "null"
}
]
},
"roleId": {
"type": "string",
"minLength": 1,
"maxLength": 64
}
},
"required": [
"firstName",
"lastName",
"email"
],
"additionalProperties": false
}
Effects
- Creates one candidate, and one application when roleId is given.
Interpret the response
Call candidates.get with $.candidate.id and confirm the fields and application.
The response identifier path is $.candidate.id for candidate. Lists may be empty; an empty list is not a discovered target.
Exact success schema:
{
"type": "object",
"required": [
"candidate"
],
"properties": {
"candidate": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"email": {
"type": "string"
},
"phone": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"profileUrl": {
"type": [
"string",
"null"
]
},
"source": {
"enum": [
"job_board",
"manual",
"sourced",
"referral",
"imported",
"agency"
]
},
"sourceDetail": {
"type": [
"string",
"null"
]
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"enum": [
"active",
"archived"
]
},
"consentAt": {
"type": [
"string",
"null"
]
},
"retainUntil": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"duplicateOf": {
"type": [
"string",
"null"
],
"description": "Set on a job-board record whose email belongs to this existing candidate id; merge it with candidates.merge once a recruiter has checked it is the same person."
},
"locale": {
"type": [
"string",
"null"
],
"description": "The language the candidate applied in on the job board (en, ar, es, fr, de, pt-BR, ja or hi); Recruit's default emails to them use it. Null when unknown: those emails follow the sender's language."
}
}
},
"application": {
"anyOf": [
{
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"candidateId": {
"type": "string"
},
"roleId": {
"type": "string"
},
"stageId": {
"type": "string"
},
"status": {
"enum": [
"active",
"rejected",
"hired",
"withdrawn"
]
},
"source": {
"type": "string"
},
"answers": {
"type": "array",
"items": {
"type": "object"
}
},
"coverLetter": {
"type": [
"string",
"null"
]
},
"appliedAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"roleTitle": {
"type": "string",
"description": "Present on candidates.list and candidates.get."
},
"stageName": {
"type": "string",
"description": "Present on candidates.list and candidates.get."
}
}
},
{
"type": "null"
}
]
},
"steps": {
"type": "object",
"description": "The stage's steps from other apps (roles.steps.list) sent to the candidate on reaching it: sent counts them; email is the email with their personal links, or null when none were sent.",
"properties": {
"sent": {
"type": "integer"
},
"email": {
"type": [
"object",
"null"
],
"properties": {
"status": {
"enum": [
"sent",
"failed",
"skipped"
]
},
"emailId": {
"type": "string"
},
"error": {
"type": [
"string",
"null"
]
}
}
}
}
}
}
}
Inspect optional steps.sent and steps.email separately from the recruitment result. sent counts stage steps; steps.email.status is Recruit’s reported notification result, not proof of mailbox delivery. Use the returned emailId, when present, for separately authorized delivery verification.
Worked examples
Actual dispatcher and migration-created SQLite or Worker handlers; Auth/workspace identity, object storage, mail and Notify dependencies are simulated. These synthetic fixtures establish no live authorization or delivery. A synthetic fixture identifier grants no access. Validate and inspect the planned request before executing. Commands with --confirm or MCP confirm:true assume the intended effect or data export has already been authorized.
Successful request
Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.
Input · payload.json:
{
"firstName": "Ada",
"lastName": "L",
"email": "ada@example.com",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112"
}
CLI:
topolo actions validate app_topolo_recruit.candidates.create --data @payload.json --json
topolo actions plan app_topolo_recruit.candidates.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.candidates.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_recruit.candidates.create",
"input": {
"firstName": "Ada",
"lastName": "L",
"email": "ada@example.com",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112"
},
"resourceType": "workspace",
"resourceId": "<identifier returned by resource discovery>",
"confirm": true
}
}
Captured local response · HTTP 201:
{
"candidate": {
"id": "cand_62ee38ce9eb54716bd78a59ab686fb10",
"firstName": "Ada",
"lastName": "L",
"email": "ada@example.com",
"phone": null,
"location": null,
"profileUrl": null,
"source": "manual",
"sourceDetail": null,
"tags": [],
"status": "active",
"consentAt": null,
"consentNotice": null,
"retainUntil": "2027-10-05T09:00:00.000Z",
"createdBy": "u_admin",
"createdAt": "2026-10-05T09:00:00.000Z",
"updatedAt": "2026-10-05T09:00:00.000Z",
"duplicateOf": null
},
"application": {
"id": "apl_287c01e390f943f88c581827f34d40fd",
"candidateId": "cand_62ee38ce9eb54716bd78a59ab686fb10",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112",
"stageId": "stg_f66b90dbda904e35be8b334f27c423f7",
"status": "active",
"source": "manual",
"answers": [],
"coverLetter": null,
"appliedAt": "2026-10-05T09:00:00.000Z",
"updatedAt": "2026-10-05T09:00:00.000Z"
}
}
HTTP 201. Call candidates.get with $.candidate.id and confirm the fields and application.
Another supported request
Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.
Input · payload.json:
{
"firstName": "Ada",
"lastName": "L",
"email": "ada@example.com"
}
CLI:
topolo actions validate app_topolo_recruit.candidates.create --data @payload.json --json
topolo actions plan app_topolo_recruit.candidates.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.candidates.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_recruit.candidates.create",
"input": {
"firstName": "Ada",
"lastName": "L",
"email": "ada@example.com"
},
"resourceType": "workspace",
"resourceId": "<identifier returned by resource discovery>",
"confirm": true
}
}
Captured local response · HTTP 201:
{
"candidate": {
"id": "cand_0b8b180eeca54b459f31f91504db056c",
"firstName": "Ada",
"lastName": "L",
"email": "ada@example.com",
"phone": null,
"location": null,
"profileUrl": null,
"source": "manual",
"sourceDetail": null,
"tags": [],
"status": "active",
"consentAt": null,
"consentNotice": null,
"retainUntil": "2027-10-05T09:00:00.000Z",
"createdBy": "u_admin",
"createdAt": "2026-10-05T09:00:00.000Z",
"updatedAt": "2026-10-05T09:00:00.000Z",
"duplicateOf": null
},
"application": null
}
HTTP 201. Call candidates.get with $.candidate.id and confirm the fields and application.
Refused request · candidate_exists
Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.
Input · payload.json:
{
"firstName": "Ada",
"lastName": "L",
"email": "ada@example.com"
}
CLI:
topolo actions validate app_topolo_recruit.candidates.create --data @payload.json --json
topolo actions plan app_topolo_recruit.candidates.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
Captured local response · HTTP 409:
{
"error": {
"code": "candidate_exists",
"message": "A candidate with this email already exists in this workspace.",
"details": {
"candidateId": "cand_0b8b180eeca54b459f31f91504db056c"
}
}
}
HTTP 409. Preserve the exact response code and details. Do not claim success.
Refused request · hiring_team_role_required
Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.
Input · payload.json:
{
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"source": "referral",
"sourceDetail": "Referred by Grace",
"roleId": "role_example"
}
CLI:
topolo actions validate app_topolo_recruit.candidates.create --data @payload.json --json
topolo actions plan app_topolo_recruit.candidates.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
Captured local response · HTTP 403:
{
"error": {
"code": "hiring_team_role_required",
"message": "Your hiring-team role in this workspace does not allow this action.",
"details": {
"teamRole": "interviewer",
"allowedRoles": [
"recruiter",
"hiring_manager"
]
}
}
}
HTTP 403. Preserve the exact response code and details. Do not claim success.
Verify the result
- Call candidates.get with $.candidate.id and confirm the fields and application.
Errors and recovery
organization_context_required(HTTP 403): Select the organization authorized by the credential; do not send an organization id in the action input.permission_denied(HTTP 403): Inspect the required Recruit permission and the credential’s granted workspace access. Use an already authorized credential/scope or request the missing access from its owner. Change credentials or grants only under separate explicit authorization; do not expand permissions automatically to clear this refusal.hiring_team_role_required(HTTP 403): Ask a recruiter to give the caller a hiring-team role that allows this action (team.members.set); error.details lists the allowed roles.workspace_required(HTTP 409): Create a Recruit workspace, or pass the workspace resource context.invalid_input(HTTP 422): Correct the fields named in error.details using inputSchema, then retry.candidate_exists(HTTP 409): Use error.details.candidateId; add them to a role with applications.create.role_required(HTTP 422): Give roleId of a role you manage.not_found(HTTP 404): Use roles.list to find a role you manage.role_closed(HTTP 409): Reopen the role with roles.open first.invalid_json(HTTP 400): Send a valid JSON object. The request is rejected before workspace resolution or record creation.api_key_workspace_binding_required(HTTP 403): Use a key explicitly bound to the intended granted workspace. Inspect Auth api_keys.options before creating or changing a key, and obtain approval for any credential or access change; do not widen bindings automatically.workspace_required(HTTP 400): Send the workspace the key is bound to with --resource-type workspace --resource-id <workspace id> (or the X-Topolo-Resource-Type and X-Topolo-Resource-Id headers).workspace_forbidden(HTTP 403): Use a workspace the key is bound to; read the key's bindings in API key management.workspace_access_insufficient(HTTP 403): The required workspace permission is workspace:read for reads or workspace:write for changes, including deletion. Inspect the authorized key options and resource access; request any missing access from its owner rather than granting it automatically.api_key_scope_required(HTTP 403): Read requiredScope in the refusal and inspect Auth api_keys.options for the authorized operation. Request any needed access from its owner; create or update a key only with explicit approval. Do not retry an unchanged 403 or widen scopes automatically.
Authentication or gateway failures may occur before the Recruit handler. Preserve the exact discovered service error, request id and credential context. The local fixtures do not establish live credential authorization.
Recovery boundary
Archive the candidate added in error.
Related actions
- Upload a candidate document: Attach a CV, cover letter or other file to a candidate (optionally to one application). Send the file as base64 in contentBase64. Allowed: PDF, Word (.docx, .doc), OpenDocument (.odt), RTF and plain text, up to 10 MB; the bytes must match the type (the declared contentType is not trusted).
- Archive a candidate: Archive a candidate: they leave active lists but their record, applications and documents are kept until retention or erasure. Adding them to a role again (applications.create, or candidates.import with roleId) reactivates them; a job-board application with their email never does, it becomes a separate record flagged duplicateOf them. Recruiters only.
Evidence
Actual dispatcher and migration-created SQLite or Worker handlers; Auth/workspace identity, object storage, mail and Notify dependencies are simulated. These synthetic fixtures establish no live authorization or delivery. Test capture provenance is retained with each fixture. API, database and integration outcomes require separate verification in the target environment.
Documentation history
2026-10-18
- Updated candidate/application responses and separated recruitment success from stage-step notification delivery.
- Updated this guide from source revision
63171ed7c08aa066adb44cc01e64011e465302e1; retained 4 source-captured request/response examples and their simulated-dependency limitations. - Clarified authorized API-key/resource recovery without automatic permission expansion. This documentation update does not establish deployment, live hiring operations or notification delivery.