First time? Set up Authentication
Learn how to authenticate your API requests with your API key.
POST /v1/search/talents does not: it evaluates every matching profile against your qualificationCriterias, and it keeps working on its own until it has found the number of green talents you asked for or spent the credits you allowed. You start it, then read the results as they arrive.
When to use it
Typical use cases:
- Fill a shortlist in your ATS: ask for
target.greens: 10, subscribe a webhook togreenonly, and push eachtalent_judgedevent into the job’s pipeline. - Budget-capped qualification per job: set
target.maxCreditsper requisition; Kalent never spends more on that call, and you decide with a continue whether the next budget is worth it. - Sourcing from a job description: send the description as a
prompttoPOST /v1/search/talents/qualified/by-prompt; Kalent turns it into filters and criteria, then qualifies.
Search, call, wave
A call stops at the first of:
target.greens greens found during this call, target.maxCredits spent, no more matching talents, or a failed wave.
Scoring language
Every verdict text of a search (recap, criterion title, criterion result) is written in one language, en or fr. It is resolved once, when the search starts: the optional scoringLanguage of the start request, otherwise the language of the user who owns the API key, set at app.kalent.ai/settings/profile (English when unset). The resolved value is stored on the search, returned as scoringLanguage by GET, and reused by every continue; sending scoringLanguage on a continue returns validation_error. Quotes in references are copied verbatim from the profile and are never translated.
The language is part of what identifies a search. The same filters or prompt and criteria in another language start a new search, with a new castingId, a fresh analysis, and new credits: verdicts are never translated after the fact. Changing the profile language only changes the default of the next start; running searches keep theirs.
The flow
- Start with
POST /v1/search/talents/qualifiedor…/by-prompt. The response is202with acastingIdandstatus: running. - Follow the call: poll
GET /v1/search/talents/qualified/{castingId}every 10–20 seconds whilenextActioniswait, or wait for the end-of-call webhook. GET never costs credits. - Act on
nextAction:POST …/continuewhen it iscontinue(open a new call) orretry(rerun the failed wave). Stop when it isdone.
canContinue on GET is kept for compatibility and equals nextAction === "continue".
Credits
- Two credits per judged talent, whatever the verdict, red included. Charged at the end of each wave. Talents that Kalent opens but cannot judge, and talents never judged because the green quota was met first, are not charged.
- Reservation. When a call opens, Kalent reserves
min(workspace balance, target.maxCredits)from your balance and sizes the first wave on that reservation. Each following wave of the same call reserves what is left of the call’s budget, again capped by the balance at that moment. - Return of unused credits. When a wave ends, the credits of its reservation that were not charged go back to the balance. So the balance dips by the reservation right after start or continue, then settles to what was actually charged.
- Not enough credits. A start or continue that cannot reserve at least 2 credits fails with
402 insufficient_api_credits. When the balance is lower thantarget.maxCredits, the call simply spends less; nothing fails.
Worked example
Your balance is 200. You start withtarget: { greens: 1, maxCredits: 4 }.
- Kalent reserves
min(200, 4) = 4. The start response carriesX-Api-Credits-Remaining: 196andstatus: running. - The first wave may open up to two talents (4 credits ÷ 2), but with one green still needed it judges them one at a time. The first judged talent is green: the call has its green and stops there. No second judgement is started.
- GET now returns
status: can_continue,nextAction: continue,canContinue: true,counts: { green: 1, amber: 0, red: 0 },creditsCharged: 2,creditsRemaining: 0. - The 2 unused reserved credits are returned: your balance is 198.
Why a call never overpays
Kalent never judges more talents in parallel than greens the call still needs: with one green missing, judgements run one at a time; with three missing, at most three run at once; as each green lands the allowance shrinks, and once the quota is met nothing else is started and nothing is left in flight. So a call never pays for a judgement started after its green quota was met. Amber and red talents judged before that point are charged like any other verdict, the call never exceedstarget.maxCredits, and the price of this guarantee is time: the smaller the remaining green quota, the less parallel the last judgements, and the slower the end of the call.
Edge cases
- Retry of a failed wave. A continue while
nextActionisretryreruns the failed wave inside the same call: same call index, budget = what the call has not spent yet, again capped by the balance. Talents already charged on that wave are not charged again. If the balance is empty, GET keepsnextAction: retrywithcreditsRemaining: 0, and the continue answers402 insufficient_api_creditsuntil credits are added. - Retry with nothing left to judge. If the failed call had already found its
target.greens, the continue does not start a wave: it closes the call and answers200withstatus: can_continue,nextAction: continue. The next continue opens a new call. - Start whose launch failed. If the search was created but its first wave could not start, the next identical start (same filters or prompt and criteria, same resolved scoring language, no
forceNew) relaunches the first call on the samecastingIdand answers202withreused: true. forceNew. Without it, a start whose filters or prompt, criteria, and resolved scoring language match an existing search returns that search (reused: true;200if it is not running). WithforceNew: true, Kalent always creates a new search.- Concurrent continues. A continue while a call is running returns
409 casting_run_in_progress; a second start for the same request while the first is being created returns409 casting_start_in_progress. - Partial
targetorwebhookon continue. Each nested field is independent: a field you omit keeps its stored value,{ "target": { "greens": 20 } }leaves the credit cap untouched. Omitting the whole object keeps everything. - GET
verdictsfilter. It only filters thetalentsarray.countsstay the totals for the whole search and nothing is charged.
Webhooks
Setwebhook.url on start (or replace it on continue) to receive events instead of polling. The URL must be HTTPS and must not point to localhost, a private network address, or a cloud metadata endpoint; other URLs are rejected with 400 validation_error.
Events
Every event shares the same envelope:
eventType, castingId, customParams (the object stored on the search, or null), sentAt (ISO 8601), and data. Kalent sends one end-of-call event per call, to the URL stored on the search at that moment; nothing is sent between the internal waves of a call. Filtering webhook.verdicts changes neither the analysis nor the bill, and counts in the end event are always the full totals. creditsRemaining is not part of the webhook; read it on GET.
Delivery
- Expected response. Any
2xxwithin 5 seconds. Redirects are not followed. The response body is ignored. - Retries. A network error, a timeout,
408,429,5xx, or any other non-2xxresponse that is not a4xxis retried: at most 3 attempts in total, about 1 second then 5 seconds apart. Any other4xxmarks the delivery failed with no retry. Retries happen in the same process that produced the event; if that process stops, the remaining attempts are not resumed later. - Idempotency. Each event is delivered at most once per search: one
talent_judgedper talent and run, one end-of-call event per call. All attempts of one event carry the sameX-Kalent-Delivery-Id, so deduplicate on it if you may receive the same attempt twice. - Ordering. Deliveries are sent as events happen and independently retried, so a
talent_judgedcan arrive after thewave_finishedof its call. Treat GET as the source of truth and webhooks as notifications.
Authenticating webhooks
Kalent does not sign webhook payloads. Put a secret value incustomParams when you start the search (for example { "webhookSecret": "…" }): it is stored on the search and echoed unchanged on every event, so your endpoint can reject any request that does not carry it. Rotate it with a continue that sends new customParams.
