Webhooks et intégration ATS
Version 1 · septembre 2026 · Documentation en anglais avec résumé en français. Fonction de l'offre Pro (essai inclus).
Résumé (FR)
RecrutIA envoie à l'URL https de votre choix chaque événement utile, au format JSON, signé HMAC-SHA256 avec le secret affiché dans Paramètres → Webhooks sortants. Chaque envoi est tenté jusqu'à 5 fois (1 min, 5 min, 30 min, 2 h, 12 h) et apparaît dans le journal des envois, avec l'erreur éventuelle et un bouton Rejouer. Votre ATS peut aussi pousser des candidats vers RecrutIA via l'endpoint entrant (clé dans Paramètres). Les événements job.* (publication, mise à jour, retrait, clôture, expiration d'un poste) permettent à votre multidiffuseur ou à un outil comme Make ou Zapier de publier et retirer vos offres ; le connecteur de candidatures (page Diffusion, administrateurs) reçoit en retour les candidatures signées, avec leur CV ; chaque candidat reçoit de RecrutIA l'accusé de réception complet ou, si votre multidiffuseur en envoie déjà un, une information courte sur l'analyse par IA et ses droits (jamais supprimée ; plafond de 50 e-mails par 24 h pendant l'essai, 2 000 avec un abonnement). RecrutIA n'a de partenariat actif avec aucun site d'emploi ni multidiffuseur : c'est votre compte chez eux qui publie. Les événements de prospection (bd.*) ne transmettent que des identifiants, jamais le nom ni l'adresse d'un prospect.
Outbound events
| Event | When | Main fields in data |
|---|---|---|
candidate.analyzed | A CV has been scored | candidate_id, name, email, job_title, job_id, score, verdict, summary, strengths[], gaps[], missing[], anonymized |
candidate.stage | Assignment stage changed (sent, feedback, interview, offer, placed, dropped) | candidate_id, stage, note, actor |
client.feedback | Your client answered on the portal | candidate_id, decision (retain | reject | interview), rating, comment, author_name, portal |
decision.human | Recruiter's final decision (AI Act art. 14) | candidate_id, decision (RETENU | ECARTE), reason, by, contradicts_ai, ai_verdict |
dossier.approved | A candidate file version was approved and published | candidate_id, version, approved_by, anonymized, url |
reference.analyzed | A reference report is ready | candidate_id, reference_id, referee_role, reliability, consistency, summary |
candidate.confirmed | The candidate answered a confirmation request | candidate_id, confirmation_id, answers |
review.approved | A candidate file passed the pre-sharing review | candidate_id, approved_by, items, kept |
brief.approved | A job brief version was approved | job_id, version, approved_by, affected[] |
interview.scheduled | The candidate picked an interview slot | candidate_id, proposal_id, start, duration |
placement.created | A placement (or hire) was recorded | placement_id, candidate_id, job_id, start_date |
offer.approve · offer.send · offer.accept · offer.decline · offer.withdraw | Offer letter workflow | offer_id, candidate_id, version |
job.published | A job is online on the career site (first publication, republication, reopening, or extension after expiry) | job_id, reference (slug-id, same as referencenumber in the feeds), status, published, title, company, url (with utm_source=multiposter), career_url, description_html, description_text, location, remote, full_remote, country (ISO 2), contract, salary_min, salary_max, salary_currency, salary_period, published_at, expires_at, syndicate |
job.updated | The job changed while online — title, location, salary, contract, criteria, public description — or a new brief version was approved, or the publication was extended. Sent as soon as the change is saved, because the career page and the feeds already show it. | |
job.unpublished | The job was removed from the career site | |
job.closed | The job was closed | |
job.expired | The publication period ended (60 days, extendable by 30): the job left the career site, the feeds and Google for Jobs | |
terms.sent | Terms of business sent to a client for acceptance | document_id, client_id, job_id, number, version, sha256 |
terms.accepted | A client accepted your terms of business (electronic acceptance) | document_id, client_id, job_id, number, version, sha256, accepted_by, accepted_at |
terms.declined | A client declined your terms of business | document_id, client_id, job_id, number, version, reason |
signature.completed | Every signer has signed a signature request (simple electronic signature) | envelope_id, number, title, candidate_id, client_id, job_id, doc_sha256, completed_at, signers[] (name, role, signed_at), certificate_url |
signature.declined | A signer declined to sign; the request is closed | envelope_id, number, title, candidate_id, client_id, job_id, declined_by (name, role), reason, declined_at |
bd.deal.stage | A business development deal changed stage (to_contact, contacted, meeting, proposal, won, lost) | deal_id, client_id, from, stage |
bd.deal.won | A deal became won — sent once per win: when it is marked as won, or when an open deal is converted directly (client, draft assignment and mandate) | deal_id, client_id, job_id (0 if no assignment was created at that moment) |
bd.deal.converted | A deal was converted into a client, a draft assignment and a mandate (also sent for a deal already marked as won, without a second bd.deal.won) | deal_id, client_id, job_id, mandate_version |
bd.deal.lost | A deal was lost | deal_id, client_id, lost_reason (no_need | competitor | price | no_answer | internal | other) |
webhook.test | Sent from Settings | message, cabinet, by |
Request format
POST <your URL>
Content-Type: application/json
User-Agent: RecrutIA-Webhooks/1.0
X-RecrutIA-Event: candidate.analyzed
X-RecrutIA-Delivery: 4812 (unique id — use it to ignore duplicates)
X-RecrutIA-Timestamp: 1757000000 (unix seconds)
X-RecrutIA-Signature: v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>" with your secret>
{"event":"candidate.analyzed","tenant_id":42,"occurred_at":"2026-09-07T09:12:31.000Z","data":{"candidate_id":918,"name":"Élodie Marchand","score":92,"verdict":"A_RENCONTRER", ...}}
Verifying the signature (Node.js)
const crypto = require('crypto');
function verify(req, rawBody, secret) {
const ts = req.headers['x-recrutia-timestamp'], sig = String(req.headers['x-recrutia-signature'] || '').replace('v1=', '');
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // reject old deliveries (replay)
const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
return expected.length === sig.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
Delivery rules
- Answer with any 2xx status within 8 seconds; anything else (timeout, 3xx, 4xx, 5xx) is retried up to 5 times with growing delays: 1 min, 5 min, 30 min, 2 h, 12 h. After that the delivery is marked failed and logged.
- Deliveries may arrive out of order or twice: deduplicate on
X-RecrutIA-Delivery. - Only https URLs are accepted; private and local addresses are refused. Regenerate the secret at any time from Settings.
- The delivery log keeps the last 30 days and every delivery can be replayed from Settings.
Inbound endpoint (your ATS → RecrutIA)
POST https://recrutia-ai.com/api/webhooks/inbound/<your key>
{"event":"candidate.created","candidate":{"name":"…","email":"…","job_title":"…"}}Creates the candidate in your workspace with source webhook. Your key is shown in Settings (Pro).
Application connector (multiposter, Zapier, Make → RecrutIA)
Create a connector on the Distribution page (📣 menu, /diffusion, administrators). You get a secret address and a signing secret, shown once; New address and secret replaces both and the old ones are refused immediately. A workspace may hold up to 10 active connectors at a time; revoke one before creating another. Applications are signed exactly like outbound webhooks and are processed like career-site applications: candidate file, AI analysis queue (deferred if the trial is over or the quota reached), tracking link, email to the candidate and a task for your team.
Email to the candidate. By default RecrutIA sends a full acknowledgement. If your multiposter already acknowledges applications, untick full acknowledgement for the connector: RecrutIA then sends only a short notice (AI-assisted pre-selection, right to object, tracking link). That notice cannot be turned off, because your organisation, as data controller, must inform each candidate of its own AI-assisted processing, which a multiposter cannot do on your behalf. These emails are capped at 50 per 24 hours during the trial and 2,000 per 24 hours on a subscription, for all your connectors together; beyond the cap, or once the trial has ended, the application is still created without any email, and a task naming that candidate is created on their record, asking your team to inform them.
POST https://recrutia-ai.com/api/connectors/<64-hex token>/applications
Content-Type: application/json
X-RecrutIA-Timestamp: 1757000000 (unix seconds, accepted within ±300 s)
X-RecrutIA-Signature: v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>" with the connector secret>
{"external_id": "broadbean-88213", // required, unique per connector (1-120 chars: letters, digits, . : @ - / + = # _)
"job_ref": "your-slug-42", // required: reference from the feeds or job.* events (or the job id)
"board": "indeed", // optional: job board name, stored as the channel (unknown values → "other")
"applicant": {"name": "Élodie Marchand", "email": "elodie@example.org", "phone": "+33 6 00 00 00 00"},
"resume": {"filename": "cv.pdf", "content_type": "application/pdf", "data": "<base64>"}, // PDF or plain text, 15 MB max; filename and content_type optional (type read from the content)
"message": "optional cover note", "lang": "fr", "consent_at": "2026-09-17T08:12:00Z", "received_at": "2026-09-17T08:12:03Z"}
| Status | Meaning |
|---|---|
201 | {"ok":true,"candidate_id":…,"email":"ack"} — application created; email is ack (full acknowledgement), notice (short notice) or none (cap reached or trial ended) |
200 | {"ok":true,"duplicate":true} — this external_id was already received for the same job and the same applicant: nothing is created again (safe to retry) |
400 | invalid_json, external_id_required, applicant_required, resume_required, resume_invalid |
401 | invalid_signature or signature_expired (timestamp outside ±300 s) |
404 | unknown or revoked address, or job_not_found (the job is looked up by its number, only in your own workspace; the address prefix of job_ref may be an older career-site address) |
409 | job_closed, job_unpublished (the job was taken off the career site), job_expired (its publication has expired: extend it from the Distribution page) or external_id_conflict (this external_id was already used for a different job or a different applicant: give each application its own id, otherwise the second one would be silently dropped) |
413 · 415 | file_too_large (CV over 15 MB, or request body over 21 MB) · unsupported_media_type (body must be JSON), unsupported_encoding (compressed bodies are refused: send plain JSON) or unsupported_file (only real PDF or plain text) |
429 | more than 300 signed applications in 5 minutes for this connector, more than 60 requests rejected on their headers alone in 5 minutes (wrong content type, compressed body, oversized announcement — these never use up the quota for real applications), or more than 30 unknown addresses in a minute from the same IP address |
503 | busy: too many large applications are being processed at the same time; retry after the delay given in Retry-After (nothing was created) |
// Node.js — sending a signed application
const body = JSON.stringify(application); const ts = Math.floor(Date.now() / 1000);
const sig = crypto.createHmac('sha256', CONNECTOR_SECRET).update(`${ts}.${body}`).digest('hex');
await fetch(CONNECTOR_URL, { method: 'POST', body, headers: { 'Content-Type': 'application/json', 'X-RecrutIA-Timestamp': String(ts), 'X-RecrutIA-Signature': 'v1=' + sig } });
Refusals (code only, never the CV) are listed on the Distribution page for 30 days; after 3 signature refusals or 5 refusals in 24 hours a task is created for your team. The body is read only after the address, the size announced in Content-Length and the content type have been checked.