Contents

v1

Partner 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 key

Best 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

1

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.

2

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.

3

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.

4

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.

5

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 mode field. Every Role Fit call runs in the background.
  • Idempotency-Key is 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

Send your API key in the 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.

<strong>Send the CV text whenever you have it.</strong> Role Fit compares the CV with the job, so the CV makes the biggest difference to the report. Without it, the comparison relies on the name, company, job title, location and public information, and metadata.cv_provided is false so your interface can say the report rests on limited information.
POST/api/v1/role-fits
{
  "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).

POST/api/v1/role-fits
{
  "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 402 and 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 queued until they start.
  • items in the response are in the same order as candidates.

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-progress with Retry-After.
  • Keys are kept for 24 hours. Responses with status 429 or 5xx are 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

202
{
  "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)

202
{
  "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

GET/api/v1/role-fits/{id}
{
  "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.match is always null.
  • 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.decision values 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 completed as soon as it is created, with credits.charged: 0 and metadata.cached: true.
  • Every request still gets its own id, with your candidate_id and job_id.
  • force_refresh: true always 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.

DELETE/api/v1/role-fits/{id}
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.

POST/api/v1/role-fits/{id}/feedback
{
  "client": {
    "id": "client-42",
    "first_name": "Anna",
    "last_name": "Meier",
    "company": "Acme Recruiting"
  },
  "rating": 4,
  "feedback": "Spot on for seniority.",
  "tags": []
}
200
{
  "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, canceled
report.match.levelstrong_fit, good_fit, partial_fit, weak_fit, unclear
report.recommendation.decision"Proceed with confidence", "Go with validation", "Requires Validation (Signals flagged)", "Requires Validation (Insufficient data)"
metadata.job_sourcedescription, url
plan.codefree, starter, growth, custom

Errors

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.

Add a webhook endpoint for Role Fit in your workspace under Developers → Webhooks.

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=&lt;unix&gt;,v1=&lt;hmac-sha256&gt; 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_validation is 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.