RecrutIA

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

EventWhenMain fields in data
candidate.analyzedA CV has been scoredcandidate_id, name, email, job_title, job_id, score, verdict, summary, strengths[], gaps[], missing[], anonymized
candidate.stageAssignment stage changed (sent, feedback, interview, offer, placed, dropped)candidate_id, stage, note, actor
client.feedbackYour client answered on the portalcandidate_id, decision (retain | reject | interview), rating, comment, author_name, portal
decision.humanRecruiter's final decision (AI Act art. 14)candidate_id, decision (RETENU | ECARTE), reason, by, contradicts_ai, ai_verdict
dossier.approvedA candidate file version was approved and publishedcandidate_id, version, approved_by, anonymized, url
reference.analyzedA reference report is readycandidate_id, reference_id, referee_role, reliability, consistency, summary
candidate.confirmedThe candidate answered a confirmation requestcandidate_id, confirmation_id, answers
review.approvedA candidate file passed the pre-sharing reviewcandidate_id, approved_by, items, kept
brief.approvedA job brief version was approvedjob_id, version, approved_by, affected[]
interview.scheduledThe candidate picked an interview slotcandidate_id, proposal_id, start, duration
placement.createdA placement (or hire) was recordedplacement_id, candidate_id, job_id, start_date
offer.approve · offer.send · offer.accept · offer.decline · offer.withdrawOffer letter workflowoffer_id, candidate_id, version
job.publishedA 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.updatedThe 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.unpublishedThe job was removed from the career site
job.closedThe job was closed
job.expiredThe publication period ended (60 days, extendable by 30): the job left the career site, the feeds and Google for Jobs
terms.sentTerms of business sent to a client for acceptancedocument_id, client_id, job_id, number, version, sha256
terms.acceptedA client accepted your terms of business (electronic acceptance)document_id, client_id, job_id, number, version, sha256, accepted_by, accepted_at
terms.declinedA client declined your terms of businessdocument_id, client_id, job_id, number, version, reason
signature.completedEvery 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.declinedA signer declined to sign; the request is closedenvelope_id, number, title, candidate_id, client_id, job_id, declined_by (name, role), reason, declined_at
bd.deal.stageA business development deal changed stage (to_contact, contacted, meeting, proposal, won, lost)deal_id, client_id, from, stage
bd.deal.wonA 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.convertedA 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.lostA deal was lostdeal_id, client_id, lost_reason (no_need | competitor | price | no_answer | internal | other)
webhook.testSent from Settingsmessage, 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

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"}
StatusMeaning
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)
400invalid_json, external_id_required, applicant_required, resume_required, resume_invalid
401invalid_signature or signature_expired (timestamp outside ±300 s)
404unknown 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)
409job_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 · 415file_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)
429more 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
503busy: 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.

Settings · Sécurité · contact@recrutia-ai.com