Contents
v1Partner API
Role Fit API Integration
Compare candidates with a specific job
Send a job and one or more candidates. Role Fit compares each candidate's CV with the job's requirements and returns a structured report your recruiters can act on.
Get your API keyBest fit for
Applicant Tracking Systems scoring applicants against an open requisition
Recruitment CRMs matching a talent pool to a new job
Staffing agencies checking a candidate against several client roles
HR tech vendors adding job-specific screening support to their product
Teams that hold the CV text and the job description and want a consistent first read
Integration flow
Create a workspace and an API key
Sign up for a TieTalent workspace, then create an X-API-Key under Developers → API keys. The key is shown once, so store it securely.
Send the job and the candidates
POST to /api/v1/role-fits with a job and a candidate, or up to 50 candidates. Add an Idempotency-Key header so a retry never creates a second run.
Receive 202 Accepted
Each candidate gets its own id. Store it with your candidate_id and job_id. A report that already exists for the same CV and job comes back as completed straight away, at no charge.
Poll or wait for a webhook
GET /api/v1/role-fits/{id} until status is completed, failed or canceled. Webhooks (role_fit_ready / role_fit_failed) avoid polling when configured.
Show the report in your product
Map the report object into your candidate view, or open the hosted report through metadata.pdf_download_url. The report supports the recruiter's decision; it never makes it.
vs Candidate Intelligence
Role Fit shares authentication, the client object, language, eu_compliant, credits, rate limits, webhooks and feedback with Candidate Intelligence. What differs:
- Every candidate is compared with a
job. Send one job for the whole request, or a job on each candidate. - No
report_type: Role Fit returns the same report on every channel. - Batches of up to 50 candidates work directly, with no
modefield. Every Role Fit call runs in the background. Idempotency-Keyis supported on create.- The report is snake_case and describes how the candidate's evidence covers the job's requirements.
- A run costs 2 credits per candidate; a reused report costs nothing.
Authentication
X-API-Key header on every request, identical to Candidate Intelligence.Request headers
X-API-Key: ats_your_api_key_here Content-Type: application/json Idempotency-Key: 7b1c9e0a-5d2f-4c8e-9a61-0f3b2d8c4e17 (optional, recommended)
Create a role fit
Creates one asynchronous role fit per candidate. The response returns straight away; the report is ready in about a minute.
metadata.cv_provided is false so your interface can say the report rests on limited information.{
"language": "en",
"eu_compliant": null,
"force_refresh": false,
"client": {
"id": "client-42",
"first_name": "Anna",
"last_name": "Meier",
"company": "Acme Recruiting"
},
"job": {
"id": "job-481",
"title": "Senior Backend Engineer",
"description": "We are hiring a Senior Backend Engineer to own our payments platform: settlement, reconciliation and payouts. 5+ years of Go and PostgreSQL, Kafka in production, payments experience."
},
"candidate": {
"id": "cand-1001",
"first_name": "Ada",
"last_name": "Lovelace",
"cv": "Ada Lovelace. Senior Backend Engineer, Globex AG (2019 to present): built the card settlement service in Go and PostgreSQL...",
"company": "Globex AG",
"role": "Senior Backend Engineer",
"location": "Zurich, Switzerland"
}
}Request fields
Top-level fields
languageRequired. Report language: en, fr or de.clientRequired. The recruiter or client user the report is for: id, first_name, last_name, company. Same object as Candidate Intelligence.eu_compliantOptional. true for an EU-compliant report, false for Standard, or null / omitted to use your workspace default. See EU-compliant vs Standard.force_refreshOptional, default false. When true, always generate a new report instead of reusing an existing one. See Report reuse.jobThe job to compare with. Required unless every candidate carries its own job.candidate / candidatesSend exactly one: a single candidate object, or a candidates array of 1 to 50.job
Every job needs description or url. When both are sent, description is used.
descriptionThe job text, 50 to 30,000 characters. Preferred over url.urlA public https link to the job posting. We read the page once per request. Pages behind a login cannot be read and return 422.titleOptional. The role's name, up to 200 characters. Shown in report.target_role when the job text does not name the role.idOptional. Your job or requisition reference, up to 200 characters. Echoed back as job_id; never interpreted by TieTalent.candidate
id, first_name and last_name are required. Everything else is optional; cv matters most.
idYour identifier for the candidate. Echoed back as candidate_id.first_name, last_nameThe candidate's name.cvThe CV as plain text, up to 50,000 characters. Not a file or a link. Strongly recommended.company, role, locationCurrent or latest employer, the candidate's own job title, and city or country. They help confirm the right person when public information is checked.jobOptional. A job for this candidate only, with the same fields as the top-level job. It replaces the top-level job for this candidate.Batches and job per candidate
Send candidates instead of candidate to compare up to 50 candidates in one request. The top-level job applies to every candidate; a candidate with its own job uses that one instead. This covers all applicants for one requisition, mixed jobs, and one candidate against several jobs (repeat the candidate with a different job).
{
"language": "en",
"client": {
"id": "client-42",
"first_name": "Anna",
"last_name": "Meier",
"company": "Acme Recruiting"
},
"job": {
"id": "job-481",
"title": "Senior Backend Engineer",
"description": "We are hiring a Senior Backend Engineer to own our payments platform..."
},
"candidates": [
{
"id": "cand-1001",
"first_name": "Ada",
"last_name": "Lovelace",
"cv": "Ada Lovelace. Senior Backend Engineer, Globex AG..."
},
{
"id": "cand-1002",
"first_name": "Alan",
"last_name": "Turing",
"cv": "Alan Turing. Staff Engineer, Hut 8...",
"job": {
"id": "job-502",
"url": "https://careers.acme.example/jobs/502"
}
}
]
}Batch rules
- The whole request is validated before anything starts. If one candidate has no job, or a job page cannot be read, the request fails with a pointer to each problem and nothing is created or charged.
- Credits for every new run are reserved up front. If your balance is too low for the whole batch, the request returns
402and nothing is created. - Each candidate spends one unit of your per-minute rate limit.
- Runs are processed a few at a time per workspace. Items wait as
queueduntil they start. itemsin the response are in the same order ascandidates.
Idempotent retries
Send an Idempotency-Key header (any unique string up to 255 characters, such as a UUID) to retry a create safely after a timeout or a network error.
- A retry with the same key and the same body returns the original response with the header
Idempotent-Replayed: true. No new run is created and nothing is charged again. - The same key with a different body returns
422 idempotency-key-reused. - A retry while the first request is still being processed returns
409 idempotency-key-in-progresswithRetry-After. - Keys are kept for 24 hours. Responses with status
429or5xxare not stored, so you can retry those with the same key.
Create response
A successful create returns 202 Accepted. A single candidate returns one object and a Location header; a batch returns items with one object per candidate. Each object carries its own credits; the top-level credits and plan describe the whole request.
Response headers
HTTP/1.1 202 Accepted Location: https://intelligence.tietalent.com/api/v1/role-fits/cmv9r1f2a0000xk0k8m3c2q71 Retry-After: 10 RateLimit-Limit: 300 RateLimit-Remaining: 299 RateLimit-Reset: 60
Single candidate
{
"id": "cmv9r1f2a0000xk0k8m3c2q71",
"status": "queued",
"candidate_id": "cand-1001",
"job_id": "job-481",
"created_at": "2026-11-03T10:00:00.000Z",
"credits": {
"balance": 412,
"plan_credits": 400,
"rollover_credits": 12,
"charged": 2,
"cached": false
},
"plan": {
"code": "growth",
"cycle": "monthly",
"credits_per_cycle": 5000,
"requests_per_minute": 300,
"period_ends_at": "2026-11-30T23:59:59.000Z"
}
}Batch (the second candidate reused an existing report)
{
"items": [
{
"id": "cmv9r1f2a0000xk0k8m3c2q71",
"status": "queued",
"candidate_id": "cand-1001",
"job_id": "job-481",
"created_at": "2026-11-03T10:00:00.000Z",
"credits": { "balance": 412, "plan_credits": 400, "rollover_credits": 12, "charged": 2, "cached": false },
"plan": { "code": "growth", "cycle": "monthly", "credits_per_cycle": 5000, "requests_per_minute": 300, "period_ends_at": "2026-11-30T23:59:59.000Z" }
},
{
"id": "cmv9r1f2b0001xk0k4n7d9p02",
"status": "completed",
"candidate_id": "cand-1002",
"job_id": "job-502",
"created_at": "2026-11-03T10:00:00.000Z",
"credits": { "balance": 412, "plan_credits": 400, "rollover_credits": 12, "charged": 0, "cached": true },
"plan": { "code": "growth", "cycle": "monthly", "credits_per_cycle": 5000, "requests_per_minute": 300, "period_ends_at": "2026-11-30T23:59:59.000Z" }
}
],
"credits": {
"balance": 412,
"plan_credits": 400,
"rollover_credits": 12,
"charged": 2,
"cached": false
},
"plan": {
"code": "growth",
"cycle": "monthly",
"credits_per_cycle": 5000,
"requests_per_minute": 300,
"period_ends_at": "2026-11-30T23:59:59.000Z"
}
}credits.balance is your remaining balance; charged is what this run reserved or charged; credits.cached is true when a report was reused at no charge. plan describes your current plan. Both objects are additive and can be ignored.
Get a role fit
Call GET /api/v1/role-fits/{id} to check progress and read the report. While the run is queued or processing, the response carries a Retry-After header.
status values
queuedAccepted and waiting to start.processingThe comparison is running. Poll again after Retry-After.completedThe report is ready in report, with metadata.failedThe run could not finish. error explains it and the credits are refunded. Retry the request.canceledCanceled through DELETE. The credits are refunded.While the run is in progress
{
"id": "cmv9r1f2a0000xk0k8m3c2q71",
"status": "processing",
"candidate_id": "cand-1001",
"job_id": "job-481",
"created_at": "2026-11-03T10:00:00.000Z",
"updated_at": "2026-11-03T10:00:14.000Z",
"completed_at": null,
"report": null,
"metadata": null,
"error": null,
"credits": {
"balance": 412,
"plan_credits": 400,
"rollover_credits": 12,
"charged": 2,
"cached": false
},
"plan": {
"code": "growth",
"cycle": "monthly",
"credits_per_cycle": 5000,
"requests_per_minute": 300,
"period_ends_at": "2026-11-30T23:59:59.000Z"
}
}When the run is completed (Standard report)
{
"id": "cmv9r1f2a0000xk0k8m3c2q71",
"status": "completed",
"candidate_id": "cand-1001",
"job_id": "job-481",
"created_at": "2026-11-03T10:00:00.000Z",
"updated_at": "2026-11-03T10:00:52.000Z",
"completed_at": "2026-11-03T10:00:52.000Z",
"report": {
"candidate_name": "Ada Lovelace",
"target_role": "Senior Backend Engineer",
"match": { "score": 81, "level": "good_fit" },
"recommendation": {
"decision": "Go with validation",
"reasoning": "Five years of payments backend work in Go covers the core requirements; Kafka in production is not shown in the CV."
},
"summary": "Strong overlap on Go, PostgreSQL and the payments domain. Kafka experience and team leadership could not be verified.",
"alignment": {
"strong_matches": ["5 years building payment services in Go and PostgreSQL"],
"partial_matches": ["Event streaming with RabbitMQ; the role uses Kafka"],
"gaps": ["No team leadership shown; the role leads three engineers"]
},
"seniority": {
"candidate_level": "Senior",
"role_level": "Senior",
"gap_analysis": "Scope and ownership match the senior level the role asks for."
},
"experience": {
"relevant": ["PCI-DSS compliant service design"],
"not_shown_in_cv": ["Kafka in production"],
"transferable": ["RabbitMQ event-driven design"]
},
"points_to_validate": ["Depth of on-call ownership for the settlement service"],
"additional_strengths": ["Led a card-tokenisation migration that reduced PCI scope"],
"open_questions": ["Notice period and current team size"],
"external_validation": {
"verified_insights": [],
"no_data_flags": ["No public code repository found"],
"impact_on_fit": "No external data changes the picture from the CV."
},
"interview_focus": ["Walk us through how you scaled the settlement service."]
},
"metadata": {
"language": "en",
"eu_compliant": false,
"cv_provided": true,
"job_source": "description",
"generated_at": "2026-11-03T10:00:52.000Z",
"cached": false,
"cached_at": null,
"pdf_download_url": "https://intelligence.tietalent.com/api/ats/reports/cmv9r1f2a0000xk0k8m3c2q71/pdf?t=atsl_...",
"pdf_download_url_expires_at": "2026-11-04T10:00:52.000Z"
},
"error": null,
"credits": {
"balance": 412,
"plan_credits": 400,
"rollover_credits": 12,
"charged": 2,
"cached": false
},
"plan": {
"code": "growth",
"cycle": "monthly",
"credits_per_cycle": 5000,
"requests_per_minute": 300,
"period_ends_at": "2026-11-30T23:59:59.000Z"
}
}When the run failed
{
"id": "cmv9r1f2a0000xk0k8m3c2q71",
"status": "failed",
"candidate_id": "cand-1001",
"job_id": "job-481",
"report": null,
"metadata": null,
"error": {
"type": "https://intelligence.tietalent.com/errors/internal-error",
"title": "Role fit failed",
"status": 500,
"detail": "The role fit could not be completed. No credits were charged."
},
...
}metadata.pdf_download_url opens the hosted report, where the PDF can be downloaded. Anyone holding the link can open the report until it expires, 24 hours after it is issued (pdf_download_url_expires_at). Do not store the link: store the role fit id and call GET when a recruiter opens the report to get a current one.
Response fields
Every field is always present. Lists can be empty; report and metadata are null until the run is completed.
report
candidate_name, target_roleThe candidate's name and the role they were compared with (null when the job does not name it).matchStandard reports only: score (0 to 100) and level. Always null on EU-compliant reports. The score says how completely the evidence covers this job's requirements. It is not a grade of the person and not a ranking against other candidates.recommendationdecision (one of four values, see Enumerations) and reasoning, which names the evidence the decision rests on.summaryTwo or three sentences: the decisive matches, the decisive gaps and what could not be verified.alignmentstrong_matches, partial_matches and gaps between the candidate's evidence and the job's requirements.senioritycandidate_level, role_level and gap_analysis.experiencerelevant, not_shown_in_cv and transferable experience for this job.points_to_validatePoints the evidence does not settle. Check them with the candidate; they are not reasons to exclude anyone.additional_strengthsEvidenced strengths the job description would not surface.open_questionsFacts the CV and public sources could not establish.external_validationverified_insights confirmed by public sources, no_data_flags for what could not be checked, and impact_on_fit.interview_focusQuestions the recruiter can ask, each tied to a gap or an unverified point.metadata
language, eu_compliantThe language and compliance mode the report was produced in.cv_providedfalse when no usable CV text was sent. Use it to show that the report rests on limited information.job_sourcedescription or url: where the job text came from.generated_at, cached, cached_atWhen the report was produced, and whether an existing report was reused.pdf_download_url, pdf_download_url_expires_atThe hosted report link and when it expires.EU-compliant vs Standard
Role Fit evaluates candidates for a job, which the EU AI Act treats as a high-risk use that needs human oversight. Both modes are decision support only: the recruiter decides. EU-compliant reports go further to avoid presenting the output as a judgement of the person:
- No score and no fit level:
report.matchis alwaysnull. - The text describes evidence against the job's stated requirements. It never calls the candidate weak, risky or unsuitable, never ranks candidates, and never infers personality or motivation.
- Protected characteristics such as health, religion or origin are never mentioned, even when the CV mentions them.
- The four
recommendation.decisionvalues are the same in both modes so your mapping stays stable. Show the EU display labels below for EU-compliant reports.
EU-compliant report (abridged)
"report": {
"candidate_name": "Ada Lovelace",
"target_role": "Senior Backend Engineer",
"match": null,
"recommendation": {
"decision": "Go with validation",
"reasoning": "The CV evidences the Go, PostgreSQL and payments requirements; Kafka in production is not evidenced."
},
...
},
"metadata": {
"eu_compliant": true,
...
}Suggested display labels for recommendation.decision on EU-compliant reports:
EU display labels
// Suggested display labels for EU-compliant reports
const EU_LABELS = {
"Proceed with confidence": "Requirements well evidenced",
"Go with validation": "Some points to validate",
"Requires Validation (Signals flagged)": "Conflicting evidence to review",
"Requires Validation (Insufficient data)": "Not enough information",
};Report reuse
A report is reused for free when your workspace already has one for the same inputs.
- The match is on the CV text (or, without a CV, the candidate details), the job text, the compliance mode and the language. Reports are reused for six months.
- A reused run is
completedas soon as it is created, withcredits.charged: 0andmetadata.cached: true. - Every request still gets its own
id, with yourcandidate_idandjob_id. force_refresh: truealways generates a new report.
Cancel a role fit
DELETE /api/v1/role-fits/{id} stops a queued or processing run and refunds its credits. It returns the role fit with status: canceled. A run that already finished is returned unchanged.
200 OK: the role fit resource, with "status": "canceled"
Feedback
Send a recruiter's rating of a completed report (1 to 5, with optional text and tags). Sending feedback again for the same client.id and role fit updates it. Same contract as Candidate Intelligence feedback.
{
"client": {
"id": "client-42",
"first_name": "Anna",
"last_name": "Meier",
"company": "Acme Recruiting"
},
"rating": 4,
"feedback": "Spot on for seniority.",
"tags": []
}{
"role_fit_id": "cmv9r1f2a0000xk0k8m3c2q71",
"rating": 4,
"feedback": "Spot on for seniority.",
"tags": [],
"updated_at": "2026-11-03T10:05:00.000Z"
}Enumerations
Every enum may gain new members. Treat an unrecognised value as its safest neighbour, not as an error.
statusqueued, processing, completed, failed, canceledreport.match.levelstrong_fit, good_fit, partial_fit, weak_fit, unclearreport.recommendation.decision"Proceed with confidence", "Go with validation", "Requires Validation (Signals flagged)", "Requires Validation (Insufficient data)"metadata.job_sourcedescription, urlplan.codefree, starter, growth, customErrors
Non-2xx responses use application/problem+json (RFC 9457). Branch on status and the last part of type. Validation errors add an errors array with a JSON Pointer to each problem.
Status
When
400
The body does not match the contract (invalid-body), or the Idempotency-Key is too long.
401
Missing, invalid or deactivated API key, or a suspended workspace.
402
Not enough credits for the whole request (insufficient-credits). Nothing was created.
404
Unknown role fit id, or one belonging to another workspace.
409
A request with the same Idempotency-Key is still being processed. Retry after Retry-After.
422
A job page could not be read (invalid-job), or an Idempotency-Key was reused with a different body.
429
Rate limit exceeded (rate-limit-exceeded). Respect Retry-After, then retry.
500
The request could not be processed. Safe to retry with the same Idempotency-Key.
Example: a candidate without a job
{
"type": "https://intelligence.tietalent.com/errors/invalid-body",
"title": "Invalid request body",
"status": 400,
"detail": "Provide job, or a job on this candidate.",
"errors": [
{ "pointer": "/candidates/1/job", "detail": "Provide job, or a job on this candidate." }
]
}Example: a job page that cannot be read
{
"type": "https://intelligence.tietalent.com/errors/invalid-job",
"title": "Invalid job",
"status": 422,
"detail": "A job page could not be read. Send the job description as text, or a public https link that does not need a login.",
"errors": [
{ "pointer": "/candidates/1/job/url", "code": "job_unreadable" }
]
}Rate limits & versioning
Rate limits
Your plan sets how fast you may call the API, in requests per minute: Free 10, Starter 60, Growth 300; Custom plans are agreed with you. A batch spends one unit per candidate, and every TieTalent API shares the same allowance. Responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; a 429 adds Retry-After in seconds. Credits are separate: they meter how much you use, not how fast.
Retry-After: 42
Versioning
The version is in the path (/v1). New response fields and new enum values may appear without a version bump.
Webhooks
Instead of polling, receive a signed notification when a role fit finishes. Delivery is at least once: deduplicate on the event id. Keep GET /api/v1/role-fits/{id} as the source of truth.
Event types
role_fit_readyThe report is ready, including a reused report.role_fit_failedThe run failed. Includes error_code in data. Canceled runs send no event.Signature verification
Every delivery carries an X-CI-Signature header: t=<unix>,v1=<hmac-sha256> over "{t}.{raw_body}", signed with your whsec_... secret. Verify it and reject timestamps older than five minutes.
X-CI-Signature: t=<unix>,v1=<hmac-sha256>
# Signed over "{t}.{raw_body}". Reject timestamps older than five minutes.The payload is a pointer only: role_fit_id, candidate_id, job_id and result_url. No report content or personal data travels in the webhook; fetch the report with an authenticated GET.
role_fit_ready
{
"id": "evt_01JDABC",
"type": "role_fit_ready",
"created_at": "2026-11-03T10:00:52.000Z",
"api_environment": "Production",
"data": {
"candidate_id": "cand-1001",
"status": "completed",
"result_url": "https://intelligence.tietalent.com/api/v1/role-fits/cmv9r1f2a0000xk0k8m3c2q71",
"role_fit_id": "cmv9r1f2a0000xk0k8m3c2q71",
"job_id": "job-481"
}
}role_fit_failed
{
"id": "evt_01JDABD",
"type": "role_fit_failed",
"created_at": "2026-11-03T10:01:30.000Z",
"api_environment": "Production",
"data": {
"candidate_id": "cand-1001",
"status": "failed",
"result_url": "https://intelligence.tietalent.com/api/v1/role-fits/cmv9r1f2a0000xk0k8m3c2q71",
"role_fit_id": "cmv9r1f2a0000xk0k8m3c2q71",
"job_id": "job-481",
"error_code": "internal_error"
}
}Known limits
Publishing these openly is what makes the rest of the documentation trustworthy.
- The report is only as good as the inputs. Without a CV, it compares the job with the name, company, title, location and public information, and is often thin.
- A job URL must be a public page. Pages that need a login, or that build their text in the browser, may not be readable: send the description as text instead.
- Public information is attributed to the candidate only when a specific detail such as the same employer confirms it. A shared name is never enough, so
external_validationis often empty. - The score and decision support the recruiter's judgement. They are not a hiring decision and not a ranking.
- Maximum 50 candidates per request, 50,000 characters of CV text and 30,000 characters of job description.
Partner Program
Start integrating Role Fit
Create a workspace and generate an API key in minutes. Need a Custom plan? Contact partnerships.