Talqo Doc

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

URL de base
https://talqo.fr
Auth
X-API-Key
Format
JSON · UTF-8

Lister les offres :

bash
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) :

bash
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)

http
X-API-Key: votre-clé-api-secrète

Méthode 2 — Authorization: Bearer

http
Authorization: Bearer votre-clé-api-secrète
Ne jamais exposer la clé API côté navigateur. Utilisez un proxy serveur (API Route, Cloudflare Worker, Wix Secrets, etc.) et stockez la clé dans une variable d'environnement.

Où trouver ma clé API ?

Interface ATS Talqo → Entreprise → section API & Sites carrière.

Erreurs d'authentification

CodeCas
401Header manquant, clé vide, clé inconnue, clé désactivée
500Configuration 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
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 :

json
{
  "status": "error",
  "data": null,
  "message": "Description lisible de l'erreur"
}

Codes HTTP globaux

CodeSignification
200OK
201Ressource créée (candidature)
400Requête invalide (champs manquants, UUID mal formé, multipart mal formé, offre fermée, CV trop lourd…)
401Clé API manquante ou invalide
403Ressource hors de votre entreprise
404Ressource introuvable
415Content-Type non supporté (utiliser multipart/form-data sur POST /api/job_applications)
429Rate limit atteint
500Erreur serveur / configuration

Lister les offres d'emploi

GET/api/job_offers

Liste paginée des offres d'emploi de l'entreprise associée à la clé API.

Paramètres de requête

ParamètreTypeDéfautDescription
pageinteger1Numéro de page (≥ 1)
per_pageinteger20Éléments par page (1 à 100, au-delà tronqué)
statusstringopenStatus à filtrer (open, closed, draft...)
locationstringRecherche insensible à la casse (ILIKE %...%)
job_typestringFiltre exact (CDI, CDD, Freelance...)
published_afterISO 8601published_at >= published_after
published_beforeISO 8601published_at <= published_before
Tri par défaut : published_at DESC. Les offres sans date de publication sont listées en dernier.

Exemple

bash
curl "https://talqo.fr/api/job_offers?status=open&per_page=20" \
  -H "X-API-Key: <VOTRE_API_KEY>"

Réponse 200

json
{
  "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

GET/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ètreTypeDescription
job_offer_iduuidFormat strict xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Tout autre format retourne 400 immédiatement.

Exemple

bash
curl "https://talqo.fr/api/job_offers/550e8400-e29b-41d4-a716-446655440000" \
  -H "X-API-Key: <VOTRE_API_KEY>"

Erreurs

CodeCas
400UUID invalide ou manquant
403L'offre appartient à une autre entreprise
404Offre introuvable
Le champ company_id n’est jamais exposé aux sites carrière.

Créer une candidature

POST/api/job_applications

Cré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.

Pourquoi multipart ? Le CV circule en binaire brut. Payloads plus légers, encodage plus simple côté client, pas de transformation supplémentaire côté serveur.

Requête — multipart/form-data

Envoyez les champs à plat dans un FormData ; le CV est un vrai fichier dans le champ cv.

ChampTypeRequisNotes
job_offer_iduuid (text)L'offre doit appartenir à votre entreprise, et être en statut open ou active.
first_namestring (text)trim() appliqué côté serveur.
last_namestring (text)trim() appliqué côté serveur.
emailstring (text)Normalisé : trim() + toLowerCase().
cvfilePDF ou DOCX, ≤ 10 MB. Voir règles ci-dessous.
phonestring (text)Format libre.
linkedin_urlstring (text)URL complète (https://...).
cover_letterstring (text)Lettre de motivation libre. Si absent, valeur vide.
sourcestringIgnoréLa source est déduite du header Origin ou Referer.
Ne pas poser vous-même Content-Type: multipart/form-data. Votre client HTTP (curl -F, fetch(FormData), wp_remote_post...) ajoute automatiquement le boundary nécessaire.

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)
  • Tout cv_url fourni est ignoré.

Exemple

bash
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
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
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" :

Casdata.status
Candidature créée (cas nominal)"new"
Auto-reject IA déclenché"rejected"
Candidature existante retournée telle quellecurrent status

Erreurs spécifiques

CodemessageCause
400job_offer_id is requiredChamp manquant
400first_name, last_name, and email are requiredChamps candidat incomplets
400cv file is required (multipart field "cv")Fichier CV manquant
400CV validation failed: …Empty file, > 10 MB, invalid format
400Invalid multipart payloadMalformed multipart body
400Job offer is not open for applicationsL'offre n'est pas open/active
403Job offer does not belong to your companyL'offre appartient à une autre entreprise
404Job offer not foundUnknown job_offer_id
415Unsupported Content-Type. Use multipart/form-data …Sent as application/json (or other) instead of multipart
500Failed to extract text from CV: …PDF/DOCX extraction error
500AI scoring failed: …Échec du scoring IA
500Failed to upload CV: …Supabase Storage upload failure
Latence : cette route peut prendre plusieurs secondes (extraction PDF, scoring IA, upload storage, insertions). Prévoyez un timeout client ≥ 30 s.

Récupérer une candidature

GET/api/job_applications/{application_id}

Retourne l'état d'une candidature.

Contrairement à /api/job_offers/{id}, cet endpoint ne pré-valide pas le format UUID via regex. Un identifiant mal formé retourne généralement 404.

Exemple

bash
curl "https://talqo.fr/api/job_applications/c3e5b1d6-4f77-4d3a-9e36-0e6fbbfb77c2" \
  -H "X-API-Key: <VOTRE_API_KEY>"

Réponse 200

json
{
  "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

typescript
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.

typescript
// 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

typescript
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

typescript
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)

ValeurSignification
newNouvelle candidature (par défaut côté API)
in_progressEn cours de traitement interne
acceptedAcceptée
rejectedRejeté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

StackPaquets
React / Next.jsreact-markdown + remark-gfm
Vuevue-markdown-render, markdown-it
Angularngx-markdown
Vanilla JSmarked, markdown-it
PHPleague/commonmark

Cycle de vie d'une candidature

Après un POST /api/job_applications réussi, le serveur exécute les étapes suivantes :

  1. Validation IP / rate-limit (429 possible).
  2. Validation de la clé API (401 possible).
  3. Validation des champs requis + du CV (lecture binaire du multipart cv), taille ≤ 10 MB, détection PDF/DOCX via magic bytes.
  4. 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).
  5. Scoring IA génère AI_resume_summary, rating, first_impression_text.
  6. Détection candidat existant via normalisation (email + first_name + last_name, minuscules et accents supprimés).
  7. Upload du CV dans le bucket resumes. Si le candidat existe déjà, les anciens CV sont nettoyés.
  8. Déduplication: si une candidature existe déjà pour (candidate_id, job_offer_id), elle est retournée telle quelle (HTTP 200).
  9. Création de job_application (statut "new", source = domaine extrait du header, cover_letter = texte optionnel (vide si absent)).
  10. Création de la première étape (candidate_steps) :
    • "awaiting_advance" dans le cas nominal
    • "rejected" si l’auto-rejet IA est activé et que rating < auto_reject_threshold (job_application is also set to rejected and an automatic rejection email is scheduled.)
  11. Email de confirmation au candidat (sauf auto-rejet IA).
  12. Log API (api_logs) : endpoint, statut HTTP, latence, IP, user-agent.
Important: seule la première étape est créée automatiquement. Les étapes suivantes sont gérées manuellement par les recruteurs dans l’ATS.

Détection de la source

  1. Header Origin → hostname.
  2. Sinon header Referer → hostname.
  3. 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

typescript
// 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

N’appelez jamais https://talqo.fr directement depuis un frontend public : votre clé API serait exposée. Utilisez toujours un proxy côté serveur.
javascript
// 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

php
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é :

  1. 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.
  2. 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

400cv 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.

415Unsupported 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.

400CV validation failed: CV file too large…

Taille maximale du CV : 10 MB. Compressez le PDF avant upload si nécessaire.

400Invalid multipart payload

Vous 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...).

400Job offer is not open for applications

L’offre est en draft, closed, paused, etc. Seuls les statuts open et active acceptent les candidatures.

401API key is required / Invalid API key

Header 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 company

L’identifiant existe mais appartient à une autre entreprise. Vérifiez la clé API et l’UUID.

429Too many requests

Respectez le header Retry-After. Réduisez les requêtes redondantes (cache proxy, etc.).

500AI 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.