{"openapi":"3.0.3","info":{"title":"Madita Public API","version":"1.0.0","description":"Programmatic access to your Madita organization: manage jobs and candidates, create and send interview invites, and read interview state and evaluations.\n\n**New here?** Step-by-step integration guides (quick start, authentication, polling, PII handling) live at [/guides](/guides). This reference is the exhaustive, always-current endpoint and field contract.\n\n**Authentication.** Every request needs a per-organization API key, sent as the `x-api-key` header (or `Authorization: Bearer <key>`). Keys are issued manually - email support@madita.ai to request one.\n\n**Rate limiting.** Each key has its own rate limit; exceeding it returns `429` with a `Retry-After` header.\n\n**Pagination.** List endpoints are keyset-paginated: pass `limit` and the `meta.nextCursor` from the previous response as `cursor`.\n\n**Webhooks.** Rather than polling, you can register an endpoint and have Madita POST signed events to it. The delivery body is the `WebhookEvent` schema below; setup, signature verification and retries are covered in the [webhooks guide](/guides/webhooks)."},"servers":[{"url":"https://api.madita.ai","description":"Production"}],"tags":[{"name":"Jobs"},{"name":"Candidates"},{"name":"Interviews"},{"name":"Evaluations"}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Per-organization API key. Email support@madita.ai to request one - keys are issued manually and scoped to a single organization."},"BearerAuth":{"type":"http","scheme":"bearer","description":"Alternatively pass the same API key as `Authorization: Bearer <key>`."}},"schemas":{"ValidationIssue":{"type":"object","properties":{"path":{"type":"string","description":"Dotted path to the offending field, for example \"competencies.0.name\". Empty for a whole-body issue."},"message":{"type":"string","description":"What is wrong with that field."}},"required":["path","message"],"description":"One field that failed validation."},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","forbidden","rate_limited","not_found","internal_error","invalid_body","invalid_cursor","invalid_session","validation_failed","description_too_short","generation_in_progress","interview_in_progress","candidate_not_eligible","candidate_unreachable","duplicate_external_reference_id","external_reference_id_ats_owned","invite_already_sent","invite_not_sent","delivery_not_owned","not_deliverable","job_not_active","job_config_incomplete","cv_parse_in_progress","file_too_large","unsupported_media_type","storage_unavailable","evaluation_not_ready","candidate_erased"],"description":"Stable, machine-readable error identifier - branch on this rather than on `message`, which is prose and may be reworded. The full set is enumerated here; new codes may be added within v1, so treat an unrecognised code as a generic failure of its HTTP status."},"message":{"type":"string","description":"Human-readable explanation. For display and logs, not for branching."},"details":{"type":"object","properties":{"issues":{"type":"array","items":{"$ref":"#/components/schemas/ValidationIssue"},"description":"One entry per field that failed validation."}},"required":["issues"],"description":"Present on `validation_failed` (and `invalid_body`), listing each field that failed validation."}},"required":["code","message"],"description":"What went wrong. Always present on an error response."},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["error","meta"],"description":"The error envelope every 4xx and 5xx response uses."},"ResponseMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Identifier for this request. Quote it when reporting a problem - it is how support finds the request in our logs."},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page: pass it back as the `cursor` query parameter. Null on the last page, and absent on responses that are not a list."}},"required":["requestId"],"description":"Metadata carried on every response: the `requestId` to quote when reporting a problem, plus `nextCursor` on a list."},"Job":{"type":"object","properties":{"id":{"type":"string","description":"Madita's ID for this job. Use it in later requests (candidates, invites) to reference this job."},"status":{"type":"string","enum":["draft","active","paused","closed"],"description":"Where the job is in its lifecycle: `draft` (not yet interviewing), `active` (live, accepting interviews), `paused`, or `closed`."},"title":{"type":"string","description":"The job title, shown to candidates and used by the interviewer. Required on create."},"description":{"type":"string","nullable":true,"description":"The job description text, used to generate suggested competencies and questions when the job is created in the dashboard."},"location":{"type":"string","nullable":true,"description":"Where the role is based, free text (for example a city, or \"Remote\")."},"interviewLanguage":{"type":"string","enum":["de","en"],"description":"Language the interview is conducted in."},"analysisLanguage":{"type":"string","nullable":true,"enum":["de","en",null],"description":"Language the evaluation output is written in."},"formality":{"type":"string","nullable":true,"enum":["du","sie",null],"description":"German formality register (`du` or `sie`)."},"tone":{"type":"string","nullable":true,"enum":["formal","neutral","casual",null],"description":"The interviewer's tone of voice during the interview."},"interviewStyle":{"type":"string","nullable":true,"enum":["concise","balanced","deep",null],"description":"Controls interview depth and length."},"externalReferenceId":{"type":"string","nullable":true,"description":"Your own job identifier (free text). Set it on create/update and use it to match a Madita job back to your system. Stored and returned, but not currently usable as a query filter."},"responsibleRecruiter":{"type":"object","nullable":true,"properties":{"id":{"type":"string","description":"Madita's user id for the recruiter."},"name":{"type":"string","description":"The recruiter's display name."},"email":{"type":"string","format":"email","description":"The recruiter's email address, the same value you send as `responsibleRecruiterEmail`."}},"required":["id","name","email"],"description":"The Madita user who owns this job and receives its completed-interview notifications. Set it with `responsibleRecruiterEmail` on create or update. Null when the job has nobody Madita would notify, usually an organization with no account owner to fall back to, or a recruiter who has since been banned; a job with no recruiter notifies everyone in the organization instead."},"competencies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Madita's id for this competency, stable across updates that do not replace the list."},"name":{"type":"string","description":"Short name of the competency."},"description":{"type":"string","description":"What good looks like for it, used to probe and to score."},"tier":{"type":"string","nullable":true,"enum":["essential","important","optional",null],"description":"How much it weighs in the overall assessment: `essential`, `important` or `optional`."},"covered":{"type":"boolean","nullable":true,"description":"Whether the interview asks about it. Null means it has never been set either way."}},"required":["id","name","description","tier","covered"]},"description":"What the interview scores against. Each item has `name`, `description`, `tier` (`essential`/`important`/`optional`), and `covered`. Sent verbatim on create; the API does not auto-generate competencies from the description."},"hardFacts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Madita's id for this hard fact."},"label":{"type":"string","description":"The pass/fail criterion itself."},"description":{"type":"string","nullable":true,"description":"Extra detail on what counts as meeting it."},"covered":{"type":"boolean","description":"Whether the interview asks about it."}},"required":["id","label","description","covered"]},"description":"Pass/fail criteria (for example work permit, driver's license)."},"knowledgeItems":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Madita's id for this knowledge item."},"title":{"type":"string","description":"Short label for the background it carries."},"body":{"type":"string","description":"The information the interviewer can draw on."}},"required":["id","title","body"]},"description":"Background information the interviewer can reference."},"forbiddenTopics":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Madita's id for this forbidden topic."},"title":{"type":"string","description":"Short label for the topic to avoid."},"body":{"type":"string","description":"What specifically must not be said or promised."}},"required":["id","title","body"]},"description":"Topics the interviewer must not raise."},"expectedDuration":{"type":"object","properties":{"goalMinutes":{"type":"number","description":"What the interview is paced to take, in minutes."},"minMinutes":{"type":"number","description":"Lower end of the expected range."},"maxMinutes":{"type":"number","description":"Upper end of the expected range."}},"required":["goalMinutes","minMinutes","maxMinutes"],"description":"How long an interview for this job is expected to take. Derived from the job's `interviewStyle` and how many competencies and hard facts it covers, so it changes whenever the job's setup changes - read it rather than caching a number. Capped at the 45-minute maximum an interview can run."},"deadlineHours":{"type":"number","description":"How long an interview link minted for this job is valid, in hours - the configured policy (the job's override, else your organization's setting, else 96). This is the policy, not a date: a link that already exists keeps the deadline it was minted with, which is the interview's `expiresAt`."},"generationStatus":{"type":"string","enum":["idle","generating","done","error"],"description":"State of the LLM config generation (see `generateFromDescription`, available on both create and update). `idle` when no extraction has run; `generating` while it runs; `done` when it has filled the job; `error` if it failed. Read it on every create - a job created with a usable description and no config of your own starts `generating` without you asking - and after any request that sets `generateFromDescription: true`."},"generationStage":{"type":"string","nullable":true,"enum":["preparing","fetching_url","loading_prompt","calling_llm","parsing","saving","done","error",null],"description":"Fine-grained progress within `generating`: `preparing`, `fetching_url`, `loading_prompt`, `calling_llm`, `parsing`, `saving`, `done`, or `error`. Null when no generation has run."},"createdAt":{"type":"string","format":"date-time","description":"When the job was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the job was last updated."},"activatedAt":{"type":"string","nullable":true,"format":"date-time","description":"When the job moved to `active` status. Null until then."}},"required":["id","status","title","description","location","interviewLanguage","analysisLanguage","formality","tone","interviewStyle","externalReferenceId","responsibleRecruiter","competencies","hardFacts","knowledgeItems","forbiddenTopics","expectedDuration","deadlineHours","generationStatus","generationStage","createdAt","updatedAt","activatedAt"],"description":"A role Madita interviews for, and the interview configuration behind it."},"Candidate":{"type":"object","properties":{"id":{"type":"string","description":"Madita's ID for this candidate. Use it in later requests (invite, decision)."},"jobId":{"type":"string","description":"The job this candidate is applying to."},"firstName":{"type":"string","description":"The candidate's first name. Required on create."},"lastName":{"type":"string","nullable":true,"description":"The candidate's last name, if provided."},"email":{"type":"string","nullable":true,"format":"email","description":"The candidate's email address."},"phone":{"type":"string","nullable":true,"description":"The candidate's phone number."},"preferredLocale":{"type":"string","nullable":true,"enum":["de","en",null],"description":"Language the candidate prefers for their interview and communications."},"status":{"type":"string","enum":["new","link_generated","declined","descoped","interview_started","interview_completed","reviewed","decided","anonymized"],"description":"Where the candidate is in the Madita pipeline. `new` (created, no link yet), `link_generated` (interview invite minted), `declined` (candidate opted out of the AI interview), `descoped` (withdrawn from the Madita process without declining), `interview_started`, `interview_completed`, `reviewed` (a recruiter has looked at the result), `decided` (a hiring decision was recorded), `anonymized` (PII removed for data retention)."},"overallScore":{"type":"integer","nullable":true,"deprecated":true,"description":"Deprecated. Always null. Madita does not compute a single overall score. Use the per-competency scores on the evaluation."},"currentJobTitle":{"type":"string","nullable":true,"description":"The candidate's current role, if known."},"currentLocation":{"type":"string","nullable":true,"description":"The candidate's current location, if known."},"externalReferenceId":{"type":"string","nullable":true,"description":"Your own candidate/application identifier (free text, unique per organization). Set it on create or update and use it to match a Madita candidate back to your system. For a candidate Madita imported from a connected ATS this is that ATS's application id, and updating it is rejected - the ATS owns it, because it is the key decisions and evaluations are written back on. Stored and returned, but not currently usable as a query filter."},"cvParsingStatus":{"type":"string","nullable":true,"enum":["pending","completed","failed",null],"description":"Progress of parsing the uploaded CV: `pending`, `completed`, or `failed`."},"declineReason":{"type":"string","nullable":true,"enum":["prefer_human","withdraw_application",null],"description":"Why the candidate opted out of the interview: `prefer_human` (they want a human recruiter) or `withdraw_application` (they withdrew entirely). Null unless they declined."},"declinedAt":{"type":"string","nullable":true,"format":"date-time","description":"When the candidate declined the interview, if they did."},"cvFullMarkdown":{"type":"string","nullable":true,"description":"The full parsed CV text. Returned ONLY on `GET /v1/candidates/{candidateId}`, never in list responses, because it is sensitive. Treat it as PII and do not log full candidate responses."},"createdAt":{"type":"string","format":"date-time","description":"When the candidate was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the candidate was last updated."}},"required":["id","jobId","firstName","lastName","email","phone","preferredLocale","status","overallScore","currentJobTitle","currentLocation","externalReferenceId","cvParsingStatus","declineReason","declinedAt","createdAt","updatedAt"],"description":"A person applying to a job, and where they are in the Madita pipeline."},"Interview":{"type":"object","properties":{"id":{"type":"string","description":"Madita's ID for this interview session."},"candidateId":{"type":"string","description":"The candidate this interview belongs to."},"jobId":{"type":"string","description":"The job this interview belongs to."},"state":{"type":"string","enum":["pending","starting","active","paused","reconnecting","ending","ended","postprocess_enqueued","postprocess_processing","postprocess_completed","postprocess_failed"],"description":"Where the interview is in its lifecycle: `pending`, `starting`, `active`, `paused`, `reconnecting`, `ending`, `ended`, then `postprocess_enqueued`, `postprocess_processing`, `postprocess_completed`, `postprocess_failed`. Terminal (the interview is over) means `ended` or any `postprocess_*` value; when polling whether the interview is done, treat those as terminal, then poll the evaluation."},"isActive":{"type":"boolean","description":"Whether the interview is currently in progress."},"kind":{"type":"string","enum":["real"],"description":"Always `real` for the public API. Internal test interviews are not exposed here."},"languageCode":{"type":"string","nullable":true,"description":"The language the interview was conducted in."},"startedAt":{"type":"string","nullable":true,"format":"date-time","description":"When the interview started. Null until it starts."},"endedAt":{"type":"string","nullable":true,"format":"date-time","description":"When the interview ended. Null until it ends."},"outcome":{"type":"string","nullable":true,"enum":["completed","ended_early","abandoned","no_response","minimal_response","technical_failure",null],"description":"How the interview ended, classified once at session end (null until then). `completed`, `ended_early`, `abandoned`, `no_response`, `minimal_response`, `technical_failure`. Use `state` (not `outcome`) to decide whether the interview is done; `outcome` answers how it ended."},"durationSeconds":{"type":"number","nullable":true,"description":"How long the interview actually ran, in seconds. Excludes time the session sat paused - a candidate who leaves and rejoins inside the resume window does not inflate this. It is therefore normally shorter than `endedAt` minus `startedAt`, and never longer. Null until the interview ends."},"expiresAt":{"type":"string","nullable":true,"format":"date-time","description":"When this interview's link stops working - the deadline to give the candidate. It is stamped when the link is minted, from your organization's (or the job's) configured invite deadline at that moment, and snapped to the end of that day in Europe/Berlin. Changing the configured deadline later does not move a link that is already out with a candidate; it applies to links minted after the change. Null once there is no live link left to expire - the candidate has opened it, or it was replaced by a `regenerate` invite, declined, descoped, or revoked."},"createdAt":{"type":"string","format":"date-time","description":"When this interview record was created (may be earlier than startedAt)."}},"required":["id","candidateId","jobId","state","isActive","kind","languageCode","startedAt","endedAt","outcome","durationSeconds","expiresAt","createdAt"],"description":"One voice interview session for a candidate."},"Transcript":{"type":"object","properties":{"interviewId":{"type":"string","description":"The interview this transcript belongs to."},"source":{"type":"string","enum":["live","post_hoc"],"description":"Whether this is the live-session transcript or the post-processed (re-aligned) one. Madita prefers `post_hoc` once it is available, otherwise returns `live`."},"turns":{"type":"array","items":{"type":"object","properties":{"speaker":{"type":"string","enum":["candidate","agent"],"description":"Who said this turn."},"turnIndex":{"type":"number","description":"Order of this turn within the transcript, starting at 0."},"text":{"type":"string","description":"What was said."},"startMs":{"type":"number","nullable":true,"description":"Turn start position in the audio, in milliseconds from the start of the interview."},"endMs":{"type":"number","nullable":true,"description":"Turn end position in the audio, in milliseconds from the start of the interview."}},"required":["speaker","turnIndex","text","startMs","endMs"]},"description":"The ordered list of transcript turns."}},"required":["interviewId","source","turns"],"description":"The turn-by-turn transcript of an interview."},"EvidenceStatement":{"type":"object","properties":{"text":{"type":"string","description":"The observation, as a sentence grounded in what was said in the interview."},"timestamp":{"type":"string","description":"Position in the interview audio as \"MM:SS\"."},"relatedCompetency":{"type":"string","description":"Name of the competency this observation supports, when it maps to one."},"hasDetailLink":{"type":"string","enum":["language","integrity"],"description":"Present when the full reasoning lives in another payload: read `language` or `integrity` on the evaluation for it."},"confidence":{"type":"string","enum":["high","limited","thin"],"description":"How confident the evaluator is in this particular observation."},"confidenceNote":{"type":"string","description":"Why confidence is less than high, when it is."}},"required":["text"],"description":"One observation from the interview, with the evidence and confidence behind it."},"EvidenceRef":{"type":"object","properties":{"label":{"type":"string","description":"Short label for the moment being pointed at."},"timestamp":{"type":"string","description":"Position in the interview audio as \"MM:SS\"."}},"required":["label","timestamp"],"description":"A pointer to a specific moment in the interview audio."},"CompetencyAssessment":{"type":"object","properties":{"id":{"type":"string","description":"Id of the job competency this scores, matching `competencies[].id` on the job."},"name":{"type":"string","description":"Name of the competency, as configured on the job."},"weight":{"type":"string","enum":["essential","important","optional"],"description":"How much this competency weighed in the overall assessment."},"score":{"type":"integer","nullable":true,"minimum":1,"maximum":5,"description":"1-5, or null if the competency was not covered in the interview."},"confidence":{"type":"string","enum":["high","limited","thin"],"description":"How confident the evaluator is in this score."},"confidenceNote":{"type":"string","description":"Why confidence is less than high, when it is."},"summary":{"type":"string","description":"One-line verdict on this competency."},"detail":{"type":"string","description":"The longer reasoning behind the score."},"evidence":{"type":"array","items":{"type":"object","properties":{"quote":{"type":"string","description":"What the candidate actually said, quoted from the transcript."},"timestamp":{"type":"string","description":"Position in the interview audio as \"MM:SS\"."}},"required":["quote","timestamp"]},"description":"Quotes from the interview backing the score, each with the point in the audio it came from."}},"required":["id","name","weight","score","confidence","summary","detail","evidence"],"description":"How the candidate scored on one of the job's competencies, with the evidence."},"QualificationFact":{"type":"object","properties":{"id":{"type":"string","description":"Id of the hard fact this checks, matching `hardFacts[].id` on the job."},"fact":{"type":"string","description":"What the candidate stated about this criterion."},"requirement":{"type":"string","description":"What the job required, when a requirement was configured."},"status":{"type":"string","enum":["fits","warning","fails"],"description":"Whether the candidate meets the criterion: `fits`, `warning` (partial or unclear) or `fails`. A `fails` on a knockout criterion is what sets `hasFailingKnockout`."},"statements":{"type":"array","items":{"$ref":"#/components/schemas/EvidenceStatement"},"description":"The interview evidence behind this verdict."}},"required":["id","fact","status","statements"],"description":"Whether the candidate meets one of the job's hard facts."},"TalentFlag":{"type":"object","properties":{"id":{"type":"string","description":"Madita's id for this finding."},"category":{"type":"string","enum":["strength","concern","observation","unknown"],"description":"Which bucket the finding belongs to, matching the key it appears under in `findings`."},"headline":{"type":"string","description":"The finding in one line."},"statements":{"type":"array","items":{"$ref":"#/components/schemas/EvidenceStatement"},"description":"The interview evidence behind it."}},"required":["id","category","headline","statements"],"description":"One finding about the candidate, bucketed as a strength, concern, observation or unknown."},"CandidateQuestion":{"type":"object","properties":{"id":{"type":"string","description":"Madita's id for this question."},"question":{"type":"string","description":"What the candidate asked."},"timestamp":{"type":"string","description":"Position in the interview audio as \"MM:SS\"."},"coverage":{"type":"string","enum":["full","partial","none"],"description":"How completely the question was answered during the interview: `full`, `partial` or `none`. Anything but `full` is worth following up with the candidate."},"answer":{"type":"string","description":"The answer the candidate was given."},"answerNote":{"type":"string","description":"What was left unanswered or needs a human follow-up."}},"required":["id","question","timestamp","coverage","answer"],"description":"A question the candidate asked during the interview, and how well it was answered."},"AudioMarker":{"type":"object","properties":{"timestamp":{"type":"string","description":"Position in the interview audio as \"MM:SS\"."},"label":{"type":"string","description":"Why this moment is worth hearing."},"kind":{"type":"string","enum":["highlight","warning"],"description":"`highlight` for a moment that reflects well, `warning` for one to check before deciding."}},"required":["timestamp","label","kind"],"description":"A moment in the interview audio worth listening to directly."},"Evaluation":{"type":"object","properties":{"id":{"type":"string","description":"Madita's ID for this evaluation."},"sessionId":{"type":"string","description":"The interview session this evaluation is for."},"candidateId":{"type":"string","description":"The candidate this evaluation belongs to."},"jobId":{"type":"string","description":"The job this evaluation belongs to."},"status":{"type":"string","enum":["pending_enqueue","queued","running","complete","partial","retryable","dead_lettered","failed"],"description":"Where the evaluation is in processing: `pending_enqueue`, `queued`, `running`, `complete`, `partial` (finished, but some sections are missing), `retryable`, `dead_lettered`, or `failed`. Poll until `complete` (or `partial`); `failed` and `dead_lettered` are terminal error states, so stop polling on either."},"outputLanguage":{"type":"string","enum":["de","en"],"description":"The language the evaluation output (summary, etc.) is written in."},"summaryMarkdown":{"type":"string","nullable":true,"description":"Human-readable summary of the evaluation. Null until the evaluation is complete."},"languageLevel":{"type":"string","nullable":true,"enum":["A1","A2","B1","B2","C1","C2",null],"description":"CEFR estimate of the candidate's spoken language level (A1-C2). Null until the evaluation is complete."},"assistanceRisk":{"type":"string","nullable":true,"enum":["low","medium","high",null],"description":"Likelihood that the candidate used unauthorized assistance (for example, reading answers from an AI tool) during the interview. Read the scale carefully: it measures assistance risk, so it runs opposite to integrity. `low` = low likelihood of assistance, so interview integrity is high (no meaningful concerns); `medium` = some indicators of possible assistance, so integrity is uncertain; `high` = strong indicators of assistance, so integrity is low (review before trusting the result). Null until the evaluation is complete."},"hasFailingKnockout":{"type":"boolean","nullable":true,"description":"Whether the candidate failed a hard-fact/knockout criterion. Null until the evaluation is complete."},"overallConfidence":{"type":"string","nullable":true,"enum":["high","limited","thin",null],"description":"How much the interview gave the model to judge on: `high`, `limited`, or `thin`. Null until the evaluation is complete."},"assessment":{"type":"object","nullable":true,"properties":{"qualificationFacts":{"type":"array","items":{"$ref":"#/components/schemas/QualificationFact"},"description":"Pass/fail on each hard-fact/knockout criterion."},"findings":{"type":"object","properties":{"strengths":{"type":"array","items":{"$ref":"#/components/schemas/TalentFlag"},"description":"What the candidate did well."},"concerns":{"type":"array","items":{"$ref":"#/components/schemas/TalentFlag"},"description":"What counts against them, worth probing before a decision."},"observations":{"type":"array","items":{"$ref":"#/components/schemas/TalentFlag"},"description":"Neutral findings that are neither for nor against."},"unknowns":{"type":"array","items":{"$ref":"#/components/schemas/TalentFlag"},"description":"What the interview could not establish, so a human still has to."}},"required":["strengths","concerns","observations","unknowns"],"description":"Talent findings, bucketed by category."},"competencies":{"type":"array","items":{"$ref":"#/components/schemas/CompetencyAssessment"},"description":"Per-competency score + evidence - what the interview scored against."},"candidateQuestions":{"type":"array","items":{"$ref":"#/components/schemas/CandidateQuestion"},"description":"Questions the candidate asked, and how the interviewer answered."},"audioMarkers":{"type":"array","items":{"$ref":"#/components/schemas/AudioMarker"},"description":"Timestamps worth listening to directly."},"summaryMarkdown":{"type":"string","description":"Human-readable overall summary."}},"required":["qualificationFacts","findings","competencies","candidateQuestions","audioMarkers","summaryMarkdown"],"description":"Scored assessment (Call 1). Shape owned by the evaluation service; may gain optional fields within v1. Null until the evaluation completes."},"language":{"type":"object","nullable":true,"properties":{"level":{"type":"string","enum":["A1","A2","B1","B2","C1","C2"],"description":"Assessed CEFR level."},"required":{"type":"string","nullable":true,"enum":["A1","A2","B1","B2","C1","C2",null],"description":"The job's required level, if one was set."},"confidence":{"type":"string","enum":["high","limited","thin"],"description":"How confident the assessment is."},"confidenceNote":{"type":"string","description":"Why confidence is limited, if applicable."},"summary":{"type":"string","description":"Overall language assessment."},"methodology":{"type":"string","description":"How the assessment was derived."},"findings":{"type":"array","items":{"type":"string"},"description":"Specific observations."},"samples":{"type":"array","items":{"$ref":"#/components/schemas/EvidenceRef"},"description":"Supporting quotes: `{label, timestamp}`."}},"required":["level","required","confidence","summary","methodology","findings","samples"],"description":"Language-proficiency assessment (Call 2). Null until the evaluation completes."},"integrity":{"type":"object","nullable":true,"properties":{"status":{"type":"string","enum":["low","medium","high"],"description":"The same value as the top-level `assistanceRisk`: `low` = few concerns (good), `high` = red flag."},"label":{"type":"string","description":"Short human-readable label."},"confidence":{"type":"string","enum":["high","limited","thin"],"description":"How confident the assessment is."},"confidenceNote":{"type":"string","description":"Why confidence is limited, if applicable."},"summary":{"type":"string","description":"Overall integrity assessment."},"analyzed":{"type":"array","items":{"type":"string"},"description":"What was checked."},"findings":{"type":"array","items":{"type":"string"},"description":"What was found, if anything."},"flaggedMoments":{"type":"array","items":{"$ref":"#/components/schemas/EvidenceRef"},"description":"Specific moments of concern: `{label, timestamp}`."}},"required":["status","label","confidence","summary","analyzed","findings","flaggedMoments"],"description":"Assistance-detection assessment (Call 3). Null until the evaluation completes."},"createdAt":{"type":"string","format":"date-time","description":"When this evaluation record was created."},"completedAt":{"type":"string","nullable":true,"format":"date-time","description":"When the evaluation finished processing. Null until then."}},"required":["id","sessionId","candidateId","jobId","status","outputLanguage","summaryMarkdown","languageLevel","assistanceRisk","hasFailingKnockout","overallConfidence","assessment","language","integrity","createdAt","completedAt"],"description":"The scored result of an interview: summary, per-competency assessment, language level and integrity check."},"InviteResult":{"type":"object","properties":{"candidateId":{"type":"string","description":"The candidate the invite link is for."},"interviewId":{"type":"string","description":"The interview the link points at. Use it to poll `GET /v1/interviews/{interviewId}`."},"interview":{"$ref":"#/components/schemas/Interview"},"interviewUrl":{"type":"string","description":"The interview link. Deliver it to the candidate yourself (email/SMS); this endpoint does not send it."},"token":{"type":"string","description":"The one-time interview token embedded in `interviewUrl`."},"reused":{"type":"boolean","description":"Whether an existing usable link was reused (`true`) rather than a fresh one minted (`false`)."}},"required":["candidateId","interviewId","interview","interviewUrl","token","reused"],"description":"A minted interview link and the interview it points at."},"SendInviteResult":{"type":"object","properties":{"candidateId":{"type":"string","description":"The candidate the invite was sent to."},"interviewId":{"type":"string","description":"The interview the link points at. Use it to poll `GET /v1/interviews/{interviewId}`."},"interview":{"$ref":"#/components/schemas/Interview"},"interviewUrl":{"type":"string","description":"The interview link that was delivered."},"token":{"type":"string","description":"The one-time interview token embedded in `interviewUrl`."},"reused":{"type":"boolean","description":"Whether an existing usable link was reused (`true`) rather than a fresh one minted (`false`)."},"channels":{"type":"array","items":{"type":"string","enum":["email","whatsapp","rcs","sms"]},"description":"The channels the invite was queued on: `email`, `whatsapp`, `rcs` or `sms`."},"heldUntil":{"type":"string","nullable":true,"format":"date-time","description":"When the send is held until (quiet-hours window), or null to send immediately."}},"required":["candidateId","interviewId","interview","interviewUrl","token","reused","channels","heldUntil"],"description":"A delivered invite: the link, the channels it went out on, and the interview it points at."},"DecisionResult":{"type":"object","properties":{"id":{"type":"string","description":"Madita's ID for this recorded decision."},"candidateId":{"type":"string","description":"The candidate the decision is for."},"decision":{"type":"string","enum":["forwarded","rejected","on-hold"],"description":"The recruiter's hiring decision: `forwarded`, `rejected`, or `on-hold`."},"score":{"type":"number","nullable":true,"description":"The 1-4 suitability rating recorded with the decision (1 = strong no, 4 = strong yes). Null if not provided."},"createdAt":{"type":"string","format":"date-time","description":"When the decision was recorded."}},"required":["id","candidateId","decision","score","createdAt"],"description":"A hiring decision recorded against a candidate."},"WebhookEvent":{"type":"object","properties":{"id":{"type":"string","description":"Unique id for this delivery. Use it to make your handler idempotent - a retry reuses it."},"type":{"type":"string","enum":["job.created","job.updated","job.closed","job.generation_completed","job.generation_failed","candidate.created","candidate.updated","candidate.decision_changed","candidate.declined","interview.invited","interview.started","interview.completed","interview.failed","evaluation.completed"],"description":"Which event fired. Branch on this. New types may be added within v1, so ignore any type you do not recognise rather than failing the delivery."},"createdAt":{"type":"string","format":"date-time","description":"When the event was generated."},"organizationId":{"type":"string","description":"Your organization's id."},"data":{"anyOf":[{"$ref":"#/components/schemas/Job"},{"$ref":"#/components/schemas/Candidate"},{"$ref":"#/components/schemas/Interview"},{"$ref":"#/components/schemas/Evaluation"},{"$ref":"#/components/schemas/DecisionResult"}],"description":"The resource the event is about, in exactly the shape the matching `/v1` endpoint returns it: a Job for `job.*`, a Candidate for `candidate.created`/`candidate.updated`/`candidate.declined`, a DecisionResult for `candidate.decision_changed`, an Interview for `interview.*`, and an Evaluation for `evaluation.completed`."}},"required":["id","type","createdAt","organizationId","data"],"description":"The body Madita POSTs to your registered webhook endpoint. See the [webhooks guide](/guides/webhooks) for signature verification and retry behaviour. This schema documents the delivery; it is not a request you make."},"JobResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Job"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"One job, wrapped in the standard `data` / `meta` envelope."},"CandidateResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Candidate"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"One candidate, wrapped in the standard `data` / `meta` envelope."},"InterviewResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Interview"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"One interview, wrapped in the standard `data` / `meta` envelope."},"TranscriptResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Transcript"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"One interview transcript, wrapped in the standard `data` / `meta` envelope."},"EvaluationResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Evaluation"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"One evaluation, wrapped in the standard `data` / `meta` envelope."},"InviteResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/InviteResult"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"A minted interview link, wrapped in the standard `data` / `meta` envelope."},"SendInviteResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SendInviteResult"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"The result of sending an invite, wrapped in the standard `data` / `meta` envelope."},"DecisionResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DecisionResult"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"A recorded recruiter decision, wrapped in the standard `data` / `meta` envelope."},"JobListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Job"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"A page of jobs. `meta.nextCursor` carries the cursor for the next page."},"CandidateListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Candidate"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"A page of candidates. `meta.nextCursor` carries the cursor for the next page."},"InterviewListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Interview"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"A page of interviews. `meta.nextCursor` carries the cursor for the next page."},"EvaluationListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Evaluation"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["data","meta"],"description":"A page of evaluations. `meta.nextCursor` carries the cursor for the next page."}},"parameters":{}},"paths":{"/v1/jobs":{"get":{"summary":"List jobs","tags":["Jobs"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size (max 100, default 25)."},"required":false,"description":"Page size (max 100, default 25).","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque keyset cursor from a prior response's meta.nextCursor."},"required":false,"description":"Opaque keyset cursor from a prior response's meta.nextCursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"A page of jobs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobListResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a job","tags":["Jobs"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":300,"description":"The job title, shown to candidates and used by the interviewer. The only field required to create a job."},"description":{"type":"string","maxLength":100000,"description":"The job ad text. Source for the LLM extraction that fills the interview config, which needs at least 100 characters to run. See `generateFromDescription`."},"location":{"type":"string","maxLength":300,"description":"Where the role is based, free text (a city, or \"Remote\")."},"status":{"type":"string","enum":["draft","active","paused","closed"],"description":"Lifecycle state: `draft` (not interviewing), `active` (live), `paused` or `closed`. Moving a never-live job to `active` needs at least one competency or covered hard fact, otherwise the call is rejected with `422 job_config_incomplete`. New jobs default to `draft`."},"interviewLanguage":{"type":"string","enum":["de","en"],"description":"Language the interview is conducted in."},"analysisLanguage":{"type":"string","enum":["de","en"],"description":"Language the evaluation output is written in. Independent of the interview language, so you can interview in German and read the assessment in English."},"formality":{"type":"string","enum":["du","sie"],"description":"German formality register the interviewer addresses the candidate in: `du` or `sie`."},"tone":{"type":"string","enum":["formal","neutral","casual"],"description":"The interviewer's tone of voice: `formal`, `neutral` or `casual`."},"interviewStyle":{"type":"string","enum":["concise","balanced","deep"],"description":"How deep and long the interview runs. It feeds `expectedDuration` on the job, together with how many competencies and hard facts you configure."},"externalReferenceId":{"type":"string","maxLength":255,"description":"Your own job identifier (free text). Stored and returned so you can match a Madita job back to your system. Not usable as a query filter, so key your records by Madita's `id` rather than looking jobs up by this later."},"responsibleRecruiterEmail":{"type":"string","format":"email","description":"Email of the Madita user who should own this job: they get the completed-interview notifications instead of everyone in your organization. It must match a member of your organization; Madita does not create users from this field. If it matches nobody, or you omit it on create, the job falls back to your organization's account owner, so a job is never left unassigned. On update an unmatched email never replaces a recruiter the job already has. Read `responsibleRecruiter` on the response to see who ended up assigned."},"competencies":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Short name of the competency, for example \"Reliability\"."},"description":{"type":"string","default":"","description":"What good looks like for this competency. The interviewer uses it to probe and the evaluator to score."},"tier":{"type":"string","enum":["essential","important","optional"],"description":"How much this competency weighs in the overall assessment: `essential`, `important` or `optional`. Defaults to the `important` weighting."},"covered":{"type":"boolean","description":"Whether the interview should actually ask about this competency. Send `false` to keep it on the job without interviewing for it."}},"required":["name"]},"description":"What the interview scores against. Replaced whole on update, not merged item by item, so sending your own copy discards any refinements a recruiter made in the dashboard. Leave it out entirely if your system is not the source of truth for the interview config."},"hardFacts":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"description":"The pass/fail criterion, for example \"Valid EU work permit\"."},"description":{"type":"string","description":"Extra detail on what counts as meeting the criterion."},"covered":{"type":"boolean","default":true,"description":"Whether the interview asks about this hard fact. Defaults to `true`."}},"required":["label"]},"description":"Pass/fail criteria such as a work permit or driver's license. Replaced whole on update, with the same caveat as `competencies`."},"knowledgeItems":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","minLength":1,"description":"Short label for this piece of background, for example \"Shift pattern\"."},"body":{"type":"string","minLength":1,"description":"The information itself. The interviewer may use it to answer a candidate's question."}},"required":["title","body"]},"description":"Background about the role the interviewer can draw on when a candidate asks. Replaced whole on update."},"forbiddenTopics":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","minLength":1,"description":"Short label for the topic the interviewer must avoid, for example \"Salary negotiation\"."},"body":{"type":"string","minLength":1,"description":"What specifically must not be said or promised on this topic."}},"required":["title","body"]},"description":"Topics the interviewer must not raise or commit to. Replaced whole on update."},"generateFromDescription":{"type":"boolean","description":"Fill the interview config from `description` via LLM extraction. Only fills generatable fields you omitted (competencies, hardFacts, knowledgeItems, forbiddenTopics, interviewLanguage, formality, tone, interviewStyle); values you send are kept. Omit it and extraction runs anyway whenever it can help - a `description` of 100+ characters and no `competencies` or `hardFacts` of your own (an empty list counts as none) - so a job never goes live without a config. Send `false` to opt out, or `true` to require it (a `description` under 100 characters is then a `400` validation error rather than a silent skip). Whenever it runs the job returns immediately with `generationStatus: \"generating\"` - poll `GET /v1/jobs/{jobId}` until `done` or `error`."}},"required":["title"]}}}},"responses":{"201":{"description":"Created job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/jobs/{jobId}":{"get":{"summary":"Get a job","tags":["Jobs"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"job_9f2a1c"},"required":true,"description":"The job's id.","name":"jobId","in":"path"}],"responses":{"200":{"description":"A job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a job","tags":["Jobs"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"job_9f2a1c"},"required":true,"description":"The job's id.","name":"jobId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":300,"description":"The job title, shown to candidates and used by the interviewer. The only field required to create a job."},"description":{"type":"string","maxLength":100000,"description":"The job ad text. Source for the LLM extraction that fills the interview config, which needs at least 100 characters to run. See `generateFromDescription`."},"location":{"type":"string","maxLength":300,"description":"Where the role is based, free text (a city, or \"Remote\")."},"status":{"type":"string","enum":["draft","active","paused","closed"],"description":"Lifecycle state: `draft` (not interviewing), `active` (live), `paused` or `closed`. Moving a never-live job to `active` needs at least one competency or covered hard fact, otherwise the call is rejected with `422 job_config_incomplete`. New jobs default to `draft`."},"interviewLanguage":{"type":"string","enum":["de","en"],"description":"Language the interview is conducted in."},"analysisLanguage":{"type":"string","enum":["de","en"],"description":"Language the evaluation output is written in. Independent of the interview language, so you can interview in German and read the assessment in English."},"formality":{"type":"string","enum":["du","sie"],"description":"German formality register the interviewer addresses the candidate in: `du` or `sie`."},"tone":{"type":"string","enum":["formal","neutral","casual"],"description":"The interviewer's tone of voice: `formal`, `neutral` or `casual`."},"interviewStyle":{"type":"string","enum":["concise","balanced","deep"],"description":"How deep and long the interview runs. It feeds `expectedDuration` on the job, together with how many competencies and hard facts you configure."},"externalReferenceId":{"type":"string","maxLength":255,"description":"Your own job identifier (free text). Stored and returned so you can match a Madita job back to your system. Not usable as a query filter, so key your records by Madita's `id` rather than looking jobs up by this later."},"responsibleRecruiterEmail":{"type":"string","format":"email","description":"Email of the Madita user who should own this job: they get the completed-interview notifications instead of everyone in your organization. It must match a member of your organization; Madita does not create users from this field. If it matches nobody, or you omit it on create, the job falls back to your organization's account owner, so a job is never left unassigned. On update an unmatched email never replaces a recruiter the job already has. Read `responsibleRecruiter` on the response to see who ended up assigned."},"competencies":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Short name of the competency, for example \"Reliability\"."},"description":{"type":"string","default":"","description":"What good looks like for this competency. The interviewer uses it to probe and the evaluator to score."},"tier":{"type":"string","enum":["essential","important","optional"],"description":"How much this competency weighs in the overall assessment: `essential`, `important` or `optional`. Defaults to the `important` weighting."},"covered":{"type":"boolean","description":"Whether the interview should actually ask about this competency. Send `false` to keep it on the job without interviewing for it."}},"required":["name"]},"description":"What the interview scores against. Replaced whole on update, not merged item by item, so sending your own copy discards any refinements a recruiter made in the dashboard. Leave it out entirely if your system is not the source of truth for the interview config."},"hardFacts":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"description":"The pass/fail criterion, for example \"Valid EU work permit\"."},"description":{"type":"string","description":"Extra detail on what counts as meeting the criterion."},"covered":{"type":"boolean","default":true,"description":"Whether the interview asks about this hard fact. Defaults to `true`."}},"required":["label"]},"description":"Pass/fail criteria such as a work permit or driver's license. Replaced whole on update, with the same caveat as `competencies`."},"knowledgeItems":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","minLength":1,"description":"Short label for this piece of background, for example \"Shift pattern\"."},"body":{"type":"string","minLength":1,"description":"The information itself. The interviewer may use it to answer a candidate's question."}},"required":["title","body"]},"description":"Background about the role the interviewer can draw on when a candidate asks. Replaced whole on update."},"forbiddenTopics":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","minLength":1,"description":"Short label for the topic the interviewer must avoid, for example \"Salary negotiation\"."},"body":{"type":"string","minLength":1,"description":"What specifically must not be said or promised on this topic."}},"required":["title","body"]},"description":"Topics the interviewer must not raise or commit to. Replaced whole on update."},"generateFromDescription":{"type":"boolean","description":"Opt-in re-extraction: **replaces** the interview config (competencies, hardFacts, knowledgeItems, forbiddenTopics, interviewLanguage, formality, tone, interviewStyle), discarding dashboard edits. Values sent in the same request are kept. Omitted, the update never touches the config. Needs a `description` of 100+ characters, from this request or the stored job. Returns immediately with `generationStatus: \"generating\"` - poll `GET /v1/jobs/{jobId}` until `done` or `error`."}}}}}},"responses":{"200":{"description":"Updated job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"generation_in_progress - a config generation is already running for this job, or the job is still generating and cannot be activated yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/candidates":{"get":{"summary":"List candidates","tags":["Candidates"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size (max 100, default 25)."},"required":false,"description":"Page size (max 100, default 25).","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque keyset cursor from a prior response's meta.nextCursor."},"required":false,"description":"Opaque keyset cursor from a prior response's meta.nextCursor.","name":"cursor","in":"query"},{"schema":{"type":"string","description":"Only candidates for this job."},"required":false,"description":"Only candidates for this job.","name":"jobId","in":"query"},{"schema":{"type":"string","enum":["new","link_generated","declined","descoped","interview_started","interview_completed","reviewed","decided","anonymized"],"description":"Only candidates in this pipeline status."},"required":false,"description":"Only candidates in this pipeline status.","name":"status","in":"query"}],"responses":{"200":{"description":"A page of candidates","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateListResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a candidate","tags":["Candidates"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string","minLength":1,"description":"Madita's id of the job this candidate is applying to. The job must belong to your organization."},"firstName":{"type":"string","minLength":1,"maxLength":200,"description":"The candidate's first name. Used to address them in the invite and the interview."},"lastName":{"type":"string","maxLength":200,"description":"The candidate's last name, if you have it."},"email":{"type":"string","format":"email","description":"The candidate's email address. Required for Madita to send the invite itself; without a reachable channel `invite/send` returns `422 candidate_unreachable`."},"phone":{"type":"string","maxLength":40,"description":"The candidate's phone number, in E.164 form. Used for WhatsApp delivery when your organization has that channel enabled."},"preferredLocale":{"type":"string","enum":["de","en"],"description":"Language the candidate prefers for their interview and their comms."},"currentJobTitle":{"type":"string","maxLength":300,"description":"The candidate's current role. Context for the interviewer."},"currentLocation":{"type":"string","maxLength":300,"description":"The candidate's current location. Context for the interviewer."},"cvFullMarkdown":{"type":"string","description":"The candidate's CV as plain text or Markdown, when you already hold it. Madita feeds it to the interviewer as context. To upload the file itself instead, use `POST /v1/candidates/{candidateId}/document`. Treat it as PII."},"externalReferenceId":{"type":"string","maxLength":255,"description":"Your own candidate or application identifier (free text, unique per organization). For a candidate Madita imported from a connected ATS this is that ATS's application id and cannot be changed, because decisions and evaluations are written back on it."}},"required":["jobId","firstName"]}}}},"responses":{"201":{"description":"Created candidate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"duplicate_external_reference_id (another candidate already uses it)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"candidate_erased (the externalReferenceId belongs to a candidate whose personal data has been erased)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/candidates/{candidateId}":{"get":{"summary":"Get a candidate","tags":["Candidates"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"cand_7b3e21"},"required":true,"description":"The candidate's id.","name":"candidateId","in":"path"}],"responses":{"200":{"description":"A candidate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a candidate","tags":["Candidates"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"cand_7b3e21"},"required":true,"description":"The candidate's id.","name":"candidateId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string","minLength":1,"maxLength":200,"description":"The candidate's first name. Used to address them in the invite and the interview."},"lastName":{"type":"string","maxLength":200,"description":"The candidate's last name, if you have it."},"email":{"type":"string","format":"email","description":"The candidate's email address. Required for Madita to send the invite itself; without a reachable channel `invite/send` returns `422 candidate_unreachable`."},"phone":{"type":"string","maxLength":40,"description":"The candidate's phone number, in E.164 form. Used for WhatsApp delivery when your organization has that channel enabled."},"preferredLocale":{"type":"string","enum":["de","en"],"description":"Language the candidate prefers for their interview and their comms."},"currentJobTitle":{"type":"string","maxLength":300,"description":"The candidate's current role. Context for the interviewer."},"currentLocation":{"type":"string","maxLength":300,"description":"The candidate's current location. Context for the interviewer."},"externalReferenceId":{"type":"string","maxLength":255,"description":"Your own candidate or application identifier. Rejected for a candidate Madita imported from a connected ATS, which owns that id."}}}}}},"responses":{"200":{"description":"Updated candidate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"duplicate_external_reference_id (another candidate already uses it), or external_reference_id_ats_owned (the candidate came from a connected ATS, which owns that field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"candidate_erased (this candidate's personal data has been erased)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/candidates/{candidateId}/invite":{"post":{"summary":"Create an interview invite link","tags":["Interviews"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"cand_7b3e21"},"required":true,"description":"The candidate's id.","name":"candidateId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ttlDays":{"type":"integer","minimum":1,"maximum":60,"description":"How long the interview link stays valid, in days (1-60). Defaults to your organization's configured invite deadline. The resulting date comes back as the interview's `expiresAt`, snapped to the end of that day in Europe/Berlin. An existing link keeps the deadline it was minted with."},"regenerate":{"type":"boolean","description":"Force a fresh link instead of reusing the candidate's existing usable one. By default an existing link is returned with `reused: true`. Rejected with `409 interview_in_progress` if the candidate is mid-interview."}}}}}},"responses":{"200":{"description":"Existing link reused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteResponse"}}}},"201":{"description":"New interview link","headers":{"Location":{"description":"Path of the interview this call created.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"An interview is in progress","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/candidates/{candidateId}/invite/send":{"post":{"summary":"Send an interview invite","tags":["Interviews"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"cand_7b3e21"},"required":true,"description":"The candidate's id.","name":"candidateId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ttlDays":{"type":"integer","minimum":1,"maximum":60,"description":"How long the interview link stays valid, in days (1-60). Defaults to your organization's configured invite deadline. The resulting date comes back as the interview's `expiresAt`, snapped to the end of that day in Europe/Berlin. An existing link keeps the deadline it was minted with."},"resend":{"type":"boolean","description":"Deliver the same link again. Sends are deduplicated by link, so a repeat send otherwise returns `409 invite_already_sent`. Cannot be combined with `regenerate`."},"regenerate":{"type":"boolean","description":"Mint a fresh link and send that one, invalidating the previous link. Cannot be combined with `resend`."}}}}}},"responses":{"202":{"description":"Invite queued for delivery","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendInviteResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"invite_already_sent, delivery_not_owned, job_not_active, candidate_not_eligible, or interview_in_progress","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"candidate_unreachable or not_deliverable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/candidates/{candidateId}/decision":{"post":{"summary":"Record a recruiter decision","tags":["Candidates"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"cand_7b3e21"},"required":true,"description":"The candidate's id.","name":"candidateId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"decision":{"type":"string","enum":["forwarded","rejected","on-hold"],"description":"The hiring decision: `forwarded`, `rejected` or `on-hold`. Recording a new decision supersedes the candidate's previous one."},"score":{"type":"integer","minimum":1,"maximum":4,"description":"Suitability rating from 1 to 4 (1 = strong no, 4 = strong yes)."},"sessionId":{"type":"string","description":"The interview the decision was made on. Must belong to this candidate, otherwise the call is rejected with `400 invalid_session`. Send it when a candidate has more than one interview, so the decision is attributed to the right one."},"forwardReason":{"type":"string","description":"Why the candidate was forwarded, free text. Stored only when `decision` is `forwarded`."},"rejectCategory":{"type":"string","enum":["no-fit","conditions-not-met","other"],"description":"Why the candidate was rejected: `no-fit`, `conditions-not-met` or `other`. A rejection needs this or `rejectReasonText`. Stored only when `decision` is `rejected`."},"rejectReasonText":{"type":"string","description":"The rejection reason in free text. A rejection needs this or `rejectCategory`. Stored only when `decision` is `rejected`."},"rejectSubReasons":{"type":"array","items":{"type":"string"},"description":"Finer-grained rejection reasons alongside `rejectCategory`. Stored only when `decision` is `rejected`."}},"required":["decision"]}}}},"responses":{"201":{"description":"Recorded decision","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"candidate_erased (this candidate's personal data has been erased)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/candidates/{candidateId}/document":{"post":{"summary":"Upload a candidate CV","tags":["Candidates"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"cand_7b3e21"},"required":true,"description":"The candidate's id.","name":"candidateId","in":"path"}],"requestBody":{"description":"The CV file itself, as multipart/form-data. Madita stores the document and parses it into `cvFullMarkdown`.","content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"The CV file. PDF, DOCX, JPEG or PNG, up to 10 MB."}},"required":["file"]}}}},"responses":{"202":{"description":"Document stored; parsing queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"cv_parse_in_progress (a previous upload is still parsing)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"candidate_erased (this candidate's personal data has been erased)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"file_too_large (over 10 MB)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"unsupported_media_type (not PDF, DOCX, JPEG or PNG)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/candidates/{candidateId}/decline":{"post":{"summary":"Record a candidate decline","tags":["Candidates"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"cand_7b3e21"},"required":true,"description":"The candidate's id.","name":"candidateId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","enum":["prefer_human","withdraw_application"],"description":"Why the candidate opted out of the AI interview: `prefer_human` (they want a human recruiter) or `withdraw_application` (they withdrew entirely). Declining kills any live interview link."}},"required":["reason"]}}}},"responses":{"200":{"description":"Declined candidate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"candidate_not_eligible (not in 'new' or 'link_generated' state)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"candidate_erased (this candidate's personal data has been erased)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/interviews":{"get":{"summary":"List interviews","tags":["Interviews"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size (max 100, default 25)."},"required":false,"description":"Page size (max 100, default 25).","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque keyset cursor from a prior response's meta.nextCursor."},"required":false,"description":"Opaque keyset cursor from a prior response's meta.nextCursor.","name":"cursor","in":"query"},{"schema":{"type":"string","description":"Only interviews for this candidate."},"required":false,"description":"Only interviews for this candidate.","name":"candidateId","in":"query"},{"schema":{"type":"string","description":"Only interviews for this job."},"required":false,"description":"Only interviews for this job.","name":"jobId","in":"query"}],"responses":{"200":{"description":"A page of interviews","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InterviewListResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/interviews/{interviewId}":{"get":{"summary":"Get an interview","tags":["Interviews"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"int_9a3f2c"},"required":true,"description":"The interview's id - the `interviewId` returned by the invite and send endpoints (equivalently the `sessionId` used by evaluations and decisions).","name":"interviewId","in":"path"}],"responses":{"200":{"description":"An interview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InterviewResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/interviews/{interviewId}/transcript":{"get":{"summary":"Get an interview transcript","tags":["Interviews"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"int_9a3f2c"},"required":true,"description":"The interview's id - the `interviewId` returned by the invite and send endpoints (equivalently the `sessionId` used by evaluations and decisions).","name":"interviewId","in":"path"}],"responses":{"200":{"description":"Transcript","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/evaluations":{"get":{"summary":"List evaluations","tags":["Evaluations"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size (max 100, default 25)."},"required":false,"description":"Page size (max 100, default 25).","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque keyset cursor from a prior response's meta.nextCursor."},"required":false,"description":"Opaque keyset cursor from a prior response's meta.nextCursor.","name":"cursor","in":"query"},{"schema":{"type":"string","description":"Only evaluations for this candidate."},"required":false,"description":"Only evaluations for this candidate.","name":"candidateId","in":"query"},{"schema":{"type":"string","description":"The evaluation for this interview. The precise way to read one interview's result when a candidate has more than one."},"required":false,"description":"The evaluation for this interview. The precise way to read one interview's result when a candidate has more than one.","name":"sessionId","in":"query"},{"schema":{"type":"string","description":"Only evaluations for this job."},"required":false,"description":"Only evaluations for this job.","name":"jobId","in":"query"}],"responses":{"200":{"description":"A page of evaluations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EvaluationListResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/evaluations/{evaluationId}":{"get":{"summary":"Get an evaluation","tags":["Evaluations"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","example":"eval_4c8d1a"},"required":true,"description":"The evaluation's id.","name":"evaluationId","in":"path"}],"responses":{"200":{"description":"An evaluation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EvaluationResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/evaluations/{evaluationId}/pdf":{"get":{"summary":"Download the evaluation PDF","tags":["Evaluations"],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"description":"Returns the rendered evaluation report as a PDF document - the same report a recruiter sees in the Madita dashboard. The response body is the PDF itself (`application/pdf`), not a link, so you can stream it straight to disk or forward it to your ATS. Returns `409 evaluation_not_ready` while the evaluation is still processing; poll `GET /v1/evaluations/{evaluationId}` until `status` is `complete` or `partial`.","parameters":[{"schema":{"type":"string","example":"eval_4c8d1a"},"required":true,"description":"The evaluation's id.","name":"evaluationId","in":"path"}],"responses":{"200":{"description":"The evaluation report as a PDF document","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but not scoped to an organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The evaluation is not finished yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}