Talqo Doc
Connectez votre acquisition candidats à Talqo de 2 façons : intégration API avancée, ou partage direct des liens d'offres générés par Talqo (zéro intégration technique).
Choisissez votre scénario
Choisissez votre scénario : vous pouvez commencer par le lien Talqo, puis passer à l'API plus tard.
Introduction
Cette API permet à un site carrière externe (Webflow, WordPress, Wix, Bubble, Next.js, etc.) de récupérer les offres publiées par votre entreprise et de créer des candidatures dans l'ATS Talqo.
- L'identité de l'entreprise est entièrement déduite de la clé API — vous n'avez jamais à fournir de company_id.
- Le CV est obligatoire, envoyé comme fichier dans un body multipart/form-data (champ cv, PDF ou DOCX, ≤ 10 MB). Tout autre Content-Type est rejeté avec 415.
- Le champ cover_letter est optionnel (texte libre) et stocké tel quel ; s’il est absent, la valeur enregistrée est vide.
- La source de la candidature est déduite automatiquement du header Origin (ou à défaut Referer) — tout source envoyé dans le body est ignoré.
- Versioning implicite (pas de préfixe /v1/). Tout changement cassant sera annoncé.
Démarrage API rapide
Lister les offres :
curl "https://talqo.fr/api/job_offers?status=open&per_page=20" \
-H "X-API-Key: <VOTRE_API_KEY>"Créer une candidature (multipart/form-data, CV envoyé comme fichier) :
curl -X POST "https://talqo.fr/api/job_applications" \
-H "X-API-Key: <VOTRE_API_KEY>" \
-H "Origin: https://careers.mon-entreprise.com" \
-F "job_offer_id=550e8400-e29b-41d4-a716-446655440000" \
-F "first_name=Jean" \
-F "last_name=Dupont" \
-F "email=jean.dupont@example.com" \
-F "phone=+33123456789" \
-F "linkedin_url=https://linkedin.com/in/jeandupont" \
-F "cover_letter=Je suis motive par ce poste et disponible rapidement." \
-F "cv=@/chemin/vers/cv_jean_dupont.pdf;type=application/pdf"Authentification
Toutes les routes exigent une clé API valide, passée dans un header HTTP.
Méthode 1 — Header X-API-Key (recommandée)
X-API-Key: votre-clé-api-secrèteMéthode 2 — Authorization: Bearer
Authorization: Bearer votre-clé-api-secrèteOù trouver ma clé API ?
Interface ATS Talqo → Entreprise → section API & Sites carrière.
Erreurs d'authentification
| Code | Cas |
|---|---|
401 | Header manquant, clé vide, clé inconnue, clé désactivée |
500 | Configuration serveur (variable SUPABASE_SERVICE_ROLE_KEY absente) |
Limitation de débit
Chaque adresse IP est limitée à 100 requêtes par minute sur l'ensemble des routes Career Sites (fenêtre fixe). La limite est pilotée par la variable d'environnement CAREER_API_RATE_LIMIT_PER_IP.
En cas de dépassement, l’API renvoie 429 Too Many Requests avec un header Retry-After :
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
Retry-After: 37
Content-Type: application/json
{
"status": "error",
"data": null,
"message": "Too many requests. Please retry later."
}Gestion des erreurs
Toutes les erreurs suivent le même format JSON :
{
"status": "error",
"data": null,
"message": "Description lisible de l'erreur"
}Codes HTTP globaux
| Code | Signification |
|---|---|
200 | OK |
201 | Ressource créée (candidature) |
400 | Requête invalide (champs manquants, UUID mal formé, multipart mal formé, offre fermée, CV trop lourd…) |
401 | Clé API manquante ou invalide |
403 | Ressource hors de votre entreprise |
404 | Ressource introuvable |
415 | Content-Type non supporté (utiliser multipart/form-data sur POST /api/job_applications) |
429 | Rate limit atteint |
500 | Erreur serveur / configuration |
Lister les offres d'emploi
/api/job_offersListe paginée des offres d'emploi de l'entreprise associée à la clé API.
Paramètres de requête
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
page | integer | 1 | Numéro de page (≥ 1) |
per_page | integer | 20 | Éléments par page (1 à 100, au-delà tronqué) |
status | string | open | Status à filtrer (open, closed, draft...) |
location | string | — | Recherche insensible à la casse (ILIKE %...%) |
job_type | string | — | Filtre exact (CDI, CDD, Freelance...) |
published_after | ISO 8601 | — | published_at >= published_after |
published_before | ISO 8601 | — | published_at <= published_before |
Exemple
curl "https://talqo.fr/api/job_offers?status=open&per_page=20" \
-H "X-API-Key: <VOTRE_API_KEY>"Réponse 200
{
"status": "success",
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Développeur Full Stack",
"description": "# À propos du poste\n\nNous recherchons...",
"location": "Paris, France",
"job_type": "CDI",
"published_at": "2024-01-15T10:00:00Z",
"status": "open",
"updated_at": "2024-01-15T10:00:00Z"
}
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 45,
"total_pages": 3
}
}Récupérer une offre d'emploi
/api/job_offers/{job_offer_id}Retourne le détail d'une offre. L'UUID est strictement validé avant toute requête DB.
Paramètres d'URL
| Paramètre | Type | Description |
|---|---|---|
job_offer_id | uuid | Format strict xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Tout autre format retourne 400 immédiatement. |
Exemple
curl "https://talqo.fr/api/job_offers/550e8400-e29b-41d4-a716-446655440000" \
-H "X-API-Key: <VOTRE_API_KEY>"Erreurs
| Code | Cas |
|---|---|
400 | UUID invalide ou manquant |
403 | L'offre appartient à une autre entreprise |
404 | Offre introuvable |
Créer une candidature
/api/job_applicationsCrée une candidature depuis un site carrière. Le CV est obligatoire. Transport unique : multipart/form-data. Tout autre Content-Type est rejeté avec 415.
Requête — multipart/form-data
Envoyez les champs à plat dans un FormData ; le CV est un vrai fichier dans le champ cv.
| Champ | Type | Requis | Notes |
|---|---|---|---|
job_offer_id | uuid (text) | ✅ | L'offre doit appartenir à votre entreprise, et être en statut open ou active. |
first_name | string (text) | ✅ | trim() appliqué côté serveur. |
last_name | string (text) | ✅ | trim() appliqué côté serveur. |
email | string (text) | ✅ | Normalisé : trim() + toLowerCase(). |
cv | file | ✅ | PDF ou DOCX, ≤ 10 MB. Voir règles ci-dessous. |
phone | string (text) | — | Format libre. |
linkedin_url | string (text) | — | URL complète (https://...). |
cover_letter | string (text) | — | Lettre de motivation libre. Si absent, valeur vide. |
source | string | Ignoré | La source est déduite du header Origin ou Referer. |
Règles du fichier cv
- Required: missing or empty file ⇒ 400.
- Taille ≤ 10 MB (au-delà ⇒ 400).
- Formats autodétectés par magic bytes (le Content-Type du formulaire n’est pas de confiance) :
- PDF (
%PDF) - DOCX (
PK…zip) - Unknown ⇒ fallback to PDF (not recommended)
- PDF (
- Tout cv_url fourni est ignoré.
Exemple
curl -X POST "https://talqo.fr/api/job_applications" \
-H "X-API-Key: <VOTRE_API_KEY>" \
-H "Origin: https://careers.mon-entreprise.com" \
-F "job_offer_id=550e8400-e29b-41d4-a716-446655440000" \
-F "first_name=Jean" \
-F "last_name=Dupont" \
-F "email=jean.dupont@example.com" \
-F "phone=+33123456789" \
-F "linkedin_url=https://linkedin.com/in/jeandupont" \
-F "cover_letter=Je suis motive par ce poste et disponible rapidement." \
-F "cv=@/chemin/vers/cv_jean_dupont.pdf;type=application/pdf"Réponse 201 — Candidature créée
HTTP/1.1 201 Created
Content-Type: application/json
{
"status": "success",
"data": {
"application_id": "c3e5b1d6-4f77-4d3a-9e36-0e6fbbfb77c2",
"candidate_id": "3d4b1c9b-1a2a-4f51-8a82-2a9f3a2b56aa",
"status": "new",
"created_at": "2024-01-15T10:00:00Z"
}
}Réponse 200 — Candidature existante
Si le même candidat (email normalisé + first_name + last_name) a déjà postulé à la même offre, l’API retourne la candidature existante sans créer de doublon :
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"data": {
"application_id": "c3e5b1d6-4f77-4d3a-9e36-0e6fbbfb77c2",
"candidate_id": "3d4b1c9b-1a2a-4f51-8a82-2a9f3a2b56aa",
"status": "new",
"created_at": "2024-01-15T10:00:00Z"
},
"message": "Application already exists"
}Attention au champ data.status
Le statut retourné n’est pas forcément "new". Si l’auto-rejet IA est activé et que le score du CV est sous votre seuil configuré, la candidature passe immédiatement à "rejected" :
| Cas | data.status |
|---|---|
| Candidature créée (cas nominal) | "new" |
| Auto-reject IA déclenché | "rejected" |
| Candidature existante retournée telle quelle | current status |
Erreurs spécifiques
| Code | message | Cause |
|---|---|---|
400 | job_offer_id is required | Champ manquant |
400 | first_name, last_name, and email are required | Champs candidat incomplets |
400 | cv file is required (multipart field "cv") | Fichier CV manquant |
400 | CV validation failed: … | Empty file, > 10 MB, invalid format |
400 | Invalid multipart payload | Malformed multipart body |
400 | Job offer is not open for applications | L'offre n'est pas open/active |
403 | Job offer does not belong to your company | L'offre appartient à une autre entreprise |
404 | Job offer not found | Unknown job_offer_id |
415 | Unsupported Content-Type. Use multipart/form-data … | Sent as application/json (or other) instead of multipart |
500 | Failed to extract text from CV: … | PDF/DOCX extraction error |
500 | AI scoring failed: … | Échec du scoring IA |
500 | Failed to upload CV: … | Supabase Storage upload failure |
Récupérer une candidature
/api/job_applications/{application_id}Retourne l'état d'une candidature.
Exemple
curl "https://talqo.fr/api/job_applications/c3e5b1d6-4f77-4d3a-9e36-0e6fbbfb77c2" \
-H "X-API-Key: <VOTRE_API_KEY>"Réponse 200
{
"status": "success",
"data": {
"id": "c3e5b1d6-4f77-4d3a-9e36-0e6fbbfb77c2",
"candidate_id": "3d4b1c9b-1a2a-4f51-8a82-2a9f3a2b56aa",
"job_offer_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "in_progress",
"source": "careers.mon-entreprise.com",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
}Schémas de données
JobOffer
interface JobOffer {
id: string; // uuid
title: string;
description: string; // Markdown (CommonMark / GFM)
location?: string | null;
job_type?: string | null;
published_at?: string | null; // ISO 8601
status?: string; // "open" | "closed" | "active" | "draft" | ...
updated_at?: string | null; // ISO 8601
}CreateJobApplicationRequest
Format de requête : multipart/form-data.
// Transport unique : multipart/form-data
// Les champs sont à plat dans le FormData, "cv" est un vrai fichier.
interface CreateJobApplicationMultipartFields {
job_offer_id: string; // uuid (text field)
first_name: string;
last_name: string;
email: string; // normalisé en minuscules côté serveur
cv: File | Blob; // fichier binaire (PDF ou DOCX), <= 10 MB
phone?: string;
linkedin_url?: string;
cover_letter?: string; // lettre de motivation (texte libre)
// source?: string // IGNORÉ : déduit de Origin / Referer
}CreateJobApplicationResponse
interface CreateJobApplicationResponse {
application_id: string; // uuid
candidate_id: string; // uuid
status: "new" | "rejected" | string; // "rejected" si auto-reject IA
created_at: string; // ISO 8601
}JobApplication
interface JobApplication {
id: string; // uuid
candidate_id: string; // uuid
job_offer_id: string; // uuid
status: string; // candidate_status
source?: string; // domaine (ex. "careers.mon-entreprise.com")
created_at: string; // ISO 8601
updated_at: string; // ISO 8601
}Enum candidate_status (valeurs usuelles)
| Valeur | Signification |
|---|---|
new | Nouvelle candidature (par défaut côté API) |
in_progress | En cours de traitement interne |
accepted | Acceptée |
rejected | Rejetée (y compris auto-reject IA) |
D'autres statuts personnalisés peuvent exister selon la configuration de l'entreprise.
Format Markdown de la description
Le champ JobOffer.description utilise le format Markdown (CommonMark + GFM). Utilisez une librairie dédiée et évitez de construire un parseur regex maison.
Librairies recommandées
| Stack | Paquets |
|---|---|
| React / Next.js | react-markdown + remark-gfm |
| Vue | vue-markdown-render, markdown-it |
| Angular | ngx-markdown |
| Vanilla JS | marked, markdown-it |
| PHP | league/commonmark |
Cycle de vie d'une candidature
Après un POST /api/job_applications réussi, le serveur exécute les étapes suivantes :
- Validation IP / rate-limit (429 possible).
- Validation de la clé API (401 possible).
- Validation des champs requis + du CV (lecture binaire du multipart cv), taille ≤ 10 MB, détection PDF/DOCX via magic bytes.
- Récupération en parallèle : offre ciblée, texte du CV, métadonnées entreprise, première étape de pipeline, plan de souscription (auto-reject activé ou non).
- Scoring IA génère
AI_resume_summary,rating,first_impression_text. - Détection candidat existant via normalisation (
email+first_name+last_name, minuscules et accents supprimés). - Upload du CV dans le bucket resumes. Si le candidat existe déjà, les anciens CV sont nettoyés.
- Déduplication: si une candidature existe déjà pour
(candidate_id, job_offer_id), elle est retournée telle quelle (HTTP200). - Création de
job_application(statut"new",source= domaine extrait du header,cover_letter= texte optionnel (vide si absent)). - Création de la première étape (
candidate_steps) :"awaiting_advance"dans le cas nominal"rejected"si l’auto-rejet IA est activé et querating < auto_reject_threshold(job_application is also set to rejected and an automatic rejection email is scheduled.)
- Email de confirmation au candidat (sauf auto-rejet IA).
- Log API (api_logs) : endpoint, statut HTTP, latence, IP, user-agent.
Détection de la source
- Header Origin → hostname.
- Sinon header Referer → hostname.
- Sinon valeur par défaut "career_site".
Tout champ source envoyé dans le body est ignoré.
Guides d'intégration
Node / Next.js (proxy côté serveur) — multipart
// app/api/career/apply/route.ts
import { NextRequest, NextResponse } from 'next/server';
const TALQO_API_BASE = 'https://talqo.fr/api';
const TALQO_API_KEY = process.env.TALQO_API_KEY!;
export async function POST(request: NextRequest) {
const incoming = await request.formData();
// Reconstitue un FormData destiné à Talqo : le CV reste un fichier binaire.
const out = new FormData();
for (const key of ['job_offer_id', 'first_name', 'last_name', 'email', 'phone', 'linkedin_url', 'cover_letter'] as const) {
const v = incoming.get(key);
if (typeof v === 'string' && v) out.append(key, v);
}
const cv = incoming.get('cv');
if (!(cv instanceof File)) {
return NextResponse.json({ error: 'cv file is required' }, { status: 400 });
}
out.append('cv', cv, cv.name);
const res = await fetch(`${TALQO_API_BASE}/job_applications`, {
method: 'POST',
headers: {
// ⚠️ Ne pas fixer Content-Type : fetch ajoute le bon boundary multipart.
'X-API-Key': TALQO_API_KEY,
'Origin': request.headers.get('origin') ?? '',
},
body: out,
});
const body = await res.json();
return NextResponse.json(body, { status: res.status });
}Browser JavaScript (via proxy) — multipart
// Envoie le fichier tel quel au proxy ; le proxy relaie vers talqo.fr.
// Payload allégé, pas de CPU inutile côté client.
async function submitApplication({ jobOfferId, firstName, lastName, email, phone, coverLetter, cvFile }) {
const form = new FormData();
form.append('job_offer_id', jobOfferId);
form.append('first_name', firstName);
form.append('last_name', lastName);
form.append('email', email.toLowerCase());
if (phone) form.append('phone', phone);
if (coverLetter) form.append('cover_letter', coverLetter);
form.append('cv', cvFile, cvFile.name);
const res = await fetch('/api/career/apply', {
method: 'POST',
body: form, // ⚠️ pas de Content-Type manuel — le navigateur gère le boundary.
});
if (!res.ok) throw new Error((await res.json()).message);
return res.json();
}PHP / WordPress — multipart
function talqo_submit_application(string $job_offer_id, array $candidate, string $cv_path): array {
// WordPress : on compose soi-même le multipart pour pouvoir envoyer le CV en binaire.
$boundary = wp_generate_password(24, false);
$eol = "\r\n";
$body = '';
$fields = array_merge(['job_offer_id' => $job_offer_id], $candidate);
// $candidate attendu : ['first_name'=>..., 'last_name'=>..., 'email'=>..., 'phone'=>..., 'linkedin_url'=>..., 'cover_letter'=>...]
foreach ($fields as $name => $value) {
if ($value === null || $value === '') continue;
$body .= "--{$boundary}{$eol}";
$body .= "Content-Disposition: form-data; name=\"{$name}\"{$eol}{$eol}";
$body .= $value . $eol;
}
$cv_bytes = file_get_contents($cv_path);
$filename = basename($cv_path);
$body .= "--{$boundary}{$eol}";
$body .= "Content-Disposition: form-data; name=\"cv\"; filename=\"{$filename}\"{$eol}";
$body .= "Content-Type: application/pdf{$eol}{$eol}";
$body .= $cv_bytes . $eol;
$body .= "--{$boundary}--{$eol}";
$response = wp_remote_post('https://talqo.fr/api/job_applications', [
'timeout' => 30,
'headers' => [
'X-API-Key' => getenv('TALQO_API_KEY'),
'Content-Type' => "multipart/form-data; boundary={$boundary}",
'Origin' => home_url(),
],
'body' => $body,
]);
if (is_wp_error($response)) {
return ['error' => $response->get_error_message()];
}
return json_decode(wp_remote_retrieve_body($response), true);
}Webflow
Webflow n’appelle pas directement des API tierces. Setup recommandé :
- Hébergez un petit proxy (Vercel/Cloudflare Workers/API Route) qui reçoit le multipart/form-data de Webflow et le relaie tel quel vers https://talqo.fr/api/job_applications avec le header X-API-Key.
- Configurez l’action webhook du formulaire Webflow pour cibler ce proxy.
Bubble.io
- Créez un API Connector : POST https://talqo.fr/api/job_applications, header X-API-Key, type de body : Form-data (champs texte + fichier cv).
- Le plugin Bubble File Uploader fournit l’objet fichier à injecter directement dans le champ cv.
Wix
- Stockez TALQO_API_KEY dans Wix Secrets.
- Créez un endpoint HTTP dans backend/http-functions.js qui relaie les requêtes vers l’API Talqo.
Dépannage
400 — cv file is required (multipart field "cv")Vous utilisez multipart/form-data mais le champ fichier ne s’appelle pas cv (ex. resume, file) ou il est absent. Vérifiez que votre client HTTP envoie bien un File/Blob.
415 — Unsupported Content-Type. Use multipart/form-data …Vous envoyez un Content-Type autre que multipart/form-data (souvent application/json). Recodez la requête en multipart et envoyez le CV en fichier binaire dans le champ cv.
400 — CV validation failed: CV file too large…Taille maximale du CV : 10 MB. Compressez le PDF avant upload si nécessaire.
400 — Invalid multipart payloadVous définissez manuellement Content-Type: multipart/form-data sans boundary. Laissez votre client HTTP générer automatiquement le bon Content-Type (curl -F, fetch(FormData), wp_remote_post...).
400 — Job offer is not open for applicationsL’offre est en draft, closed, paused, etc. Seuls les statuts open et active acceptent les candidatures.
401 — API key is required / Invalid API keyHeader X-API-Key manquant ou mal orthographié, clé révoquée dans l’ATS (is_active=false), ou espaces en trop autour de la clé.
403 — …does not belong to your companyL’identifiant existe mais appartient à une autre entreprise. Vérifiez la clé API et l’UUID.
429 — Too many requestsRespectez le header Retry-After. Réduisez les requêtes redondantes (cache proxy, etc.).
500 — AI scoring failed… / Failed to extract text from CV…Un CV scanné/protégé/corrompu peut empêcher l’extraction. En cas de latence/erreur OpenAI : réessayez. Contactez le support avec timestamp UTC et application_id si créé.
Bonnes pratiques
Sécurité
- N’exposez jamais X-API-Key dans le code navigateur.
- Utilisez HTTPS uniquement.
- Stockez la clé dans des variables d’environnement.
- Prévoyez un process de révocation/régénération.
Fiabilité
- Réessayez avec backoff exponentiel sur 429/5xx.
- Les requêtes sont idempotentes par (candidate, job_offer).
- Timeout client ≥ 30 secondes.
Performance
- Mettez en cache la liste des offres (TTL 5–15 minutes).
- Pagination : per_page ≤ 50.
- Évitez les Promise.all() agressifs (rate limits).
UX
- Affichez un loader pendant le POST (peut durer plusieurs secondes).
- Réécrivez les erreurs techniques pour les utilisateurs finaux.
- Un email de confirmation est envoyé automatiquement au candidat.
Besoin d'aide ?
Contact talqo.contact@gmail.com avec endpoint, timestamp UTC et payload (sans clé API).
Cette page n’est pas indexée par les moteurs de recherche. Partagez-la uniquement avec vos intégrateurs.