API publique des offres d'emploi
Construisez votre propre site d'emploi (par exemple avec Next.js sur Vercel) en vous appuyant sur votre pipeline de recrutement Kit. Listez les postes publiés et recevez des candidatures via une API REST publique, un SDK TypeScript officiel et un modèle Next.js en un clic.
Pourquoi c’est important
Le portail carrière hébergé par Kit et le widget intégrable couvrent la plupart des besoins. Mais si vous voulez un contrôle total sur le design — un site carrière sur mesure, une page de destination dédiée par poste ou un site d’emploi assorti à votre produit — l’API publique des offres d’emploi vous permet de lire vos offres publiées et d’envoyer les candidatures directement dans votre pipeline Kit, pendant que Kit continue de gérer la présélection, les étapes, les entretiens et la communication avec les candidats.
Il existe aussi un SDK TypeScript officiel et un modèle Next.js déployable en un clic, pour livrer un site d’emploi sur mesure en quelques minutes — ou passez outre les deux et appelez directement les points de terminaison REST ci-dessous avec n’importe quel client HTTP.
Clés API
Créez une paire de clés sous Recrutement → Portail carrière → Clés API publiques. Chaque paire comprend :
-
Clé publiable (
pk_…) — peut être embarquée sans risque dans un navigateur. Elle peut lire les offres publiées et soumettre des candidatures, rien de plus, et n’expose jamais les données des candidats. Avant de pouvoir soumettre des candidatures ou demander des téléversements présignés, vous devez configurer au moins une protection anti-bot — une liste d’origines autorisées ou votre propre widget Cloudflare Turnstile —, faute de quoi ces requêtes sont rejetées avec403 bot_protection_required. La lecture des offres fonctionne sans cette configuration. -
Clé secrète (
sk_…) — réservée à un usage côté serveur (par exemple une Server Action Next.js). Elle contourne les vérifications navigateur d’origine et de Turnstile. Ne l’exposez jamais dans du code côté client. Aucune des deux clés ne peut lire les données personnelles des candidats.
La clé secrète n’est affichée qu’une seule fois, à sa création ou à sa rotation. Vous pouvez effectuer sa rotation à tout moment depuis la page de paramètres de la clé ; l’ancienne clé secrète cesse de fonctionner immédiatement.
Authentifiez chaque requête avec un en-tête bearer :
Authorization: Bearer sk_your_secret_key
Points de terminaison
URL de base : https://startupkit.app (ou votre domaine carrière personnalisé).
Lister les offres publiées
GET /api/public/v1/jobs?department=&location=&employment_type=&remote=&page=&per_page=
Retourne uniquement les postes publiés de votre compte.
{
"data": [
{
"id": "JdK2hQ8…",
"title": "Senior Rails Developer",
"department": "Engineering",
"location": "Remote",
"employment_type": "full_time",
"remote": true,
"published_at": "2026-06-01T12:00:00Z",
"url": "https://careers.yourco.com/JdK2hQ8…",
"salary": { "min": 120000, "max": 160000, "currency": "USD", "period": "YEAR" }
}
],
"pagination": { "current_page": 1, "total_pages": 3, "total_count": 42, "per_page": 20 }
}
L’id est le jeton public de l’offre — utilisez-le pour les points de terminaison de détail et de candidature.
Récupérer une offre et son formulaire de candidature
GET /api/public/v1/jobs/:public_token
Retourne l’offre, accompagnée d’un application_form décrivant précisément les champs et questions à afficher, la mention de consentement à présenter, si le CV est obligatoire ainsi que les types et la taille de fichier acceptés, et si Turnstile est requis.
{
"id": "JdK2hQ8…",
"title": "Senior Rails Developer",
"description_html": "<p>We're hiring…</p>",
"accepting_applications": true,
"stages": [{ "name": "Application Review", "type": "application_form" }],
"application_form": {
"fields": [
{ "name": "cover_letter", "type": "textarea", "label": "Cover letter", "required": false }
],
"questions": [
{ "key": "why_us", "type": "text", "prompt": "Why do you want to join?", "required": true, "max_length": 2000 }
],
"consent_disclosure_html": "<p>By applying you agree…</p>",
"resume": {
"required": false,
"content_types": ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
"max_byte_size": 10485760
},
"turnstile": { "required": false, "sitekey": null }
}
}
Les indicateurs required sont appliqués côté serveur. Lorsque resume.required vaut true, une candidature sans resume_signed_id est rejetée avec 422 validation_failed — il en va de même pour tout champ ou question marqué required: true resté sans réponse. Si vous générez le formulaire dynamiquement à partir de ce schéma, respectez ces indicateurs pour éviter aux candidats un rejet après avoir tout rempli.
Téléverser un CV (URL présignée)
Les CV sont téléversés directement vers le stockage : ils ne transitent jamais par votre serveur (ce qui évite les limites de taille de corps des environnements serverless).
POST /api/public/v1/direct_uploads
{ "blob": { "filename": "cv.pdf", "byte_size": 102400, "checksum": "<base64 MD5>", "content_type": "application/pdf" } }
{
"signed_id": "eyJf…",
"direct_upload": { "url": "https://…s3…", "headers": { "Content-Type": "application/pdf", "Content-MD5": "…" } }
}
Envoyez les octets du fichier en PUT vers direct_upload.url avec les headers retournés, puis transmettez le signed_id comme resume_signed_id au moment de soumettre la candidature.
Soumettre une candidature
POST /api/public/v1/jobs/:public_token/applications
{
"application": {
"email": "[email protected]",
"first_name": "Ada",
"last_name": "Lovelace",
"phone": "+1 555 0100",
"responses": { "cover_letter": "…", "why_us": "…" },
"resume_signed_id": "eyJf…"
},
"turnstile_token": "<token>"
}
Retourne 201 avec une confirmation minimale, sans données personnelles :
{ "id": "app_9fQ…", "status": "submitted", "job": "JdK2hQ8…", "submitted_at": "2026-06-11T09:30:00Z" }
Le turnstile_token n’est nécessaire que pour les soumissions depuis un navigateur (pk_) lorsque Turnstile est configuré sur la clé ; les appels côté serveur (sk_) s’en passent.
Vivier de talents
Toutes les personnes qui apprécient votre entreprise ne correspondent pas à un poste ouvert aujourd’hui. Le vivier de talents recueille ces profils — e-mail, LinkedIn, éventuellement un CV — pour que vous puissiez les inviter à postuler quand le bon poste s’ouvre. Les entrées arrivent non vérifiées : Kit envoie un lien de confirmation par e-mail, et personne n’entre dans votre vivier par ce point de terminaison tant qu’il n’a pas cliqué dessus. (Les administrateurs peuvent aussi importer des CV directement — une porte distincte, avec ses propres règles.)
Une inscription au vivier de talents, c’est quelqu’un qui vous demande de conserver ses coordonnées sans poste auquel postuler : elle exige donc un consentement explicite — une case réellement cochée, et non une simple mention que vous lui affichez. Kit rejette une inscription publique qui s’en dispense. Un import par un administrateur consigne à la place une attestation de base légale, puisque personne n’est là pour cocher quoi que ce soit.
Lire le formulaire d’inscription
GET /api/public/v1/talent_pool
{
"accepting_signups": true,
"consent": {
"required": true,
"disclosure_html": "<p>Keep my details on file for 24 months…</p>",
"retention_months": 24,
"privacy_policy_url": "https://yourco.com/privacy"
},
"fields": [
{ "name": "email", "required": true },
{ "name": "linkedin_url", "required": false },
{ "name": "resume_signed_id", "required": false }
],
"resume": {
"required": false,
"content_types": ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
"max_byte_size": 10485760
},
"turnstile": { "required": false, "sitekey": null }
}
Affichez consent.disclosure_html comme libellé d’une case à cocher qui démarre décochée. Il s’agit de votre propre texte de consentement — modifiez-le sous Recrutement → Paramètres → Consentement, en même temps que retention_months, qui détermine à partir de quand Kit anonymise les entrées que personne n’a renouvelées.
Rejoindre le vivier de talents
POST /api/public/v1/talent_pool/entries
{
"talent_pool_entry": {
"email": "[email protected]",
"linkedin_url": "https://linkedin.com/in/ada",
"resume_signed_id": "eyJf…",
"consent": true,
"consent_ip_address": "203.0.113.7"
},
"turnstile_token": "<token>"
}
Retourne 201 avec une confirmation sans données personnelles :
{ "id": "tpe_9fQ…", "status": "pending_verification", "submitted_at": "2026-06-11T09:30:00Z" }
Les CV empruntent le même téléversement présigné que les candidatures — demandez un signed_id, envoyez les octets en PUT, puis transmettez-le comme resume_signed_id.
Consigner qui a consenti
Kit conserve un justificatif de consentement avec chaque entrée : le texte exact affiché, l’horodatage et l’adresse IP de la personne qui a accepté. C’est sur ce dernier champ que les intégrations côté serveur se trompent, sans que rien ne le signale.
Lors d’une soumission depuis un navigateur avec une clé pk_, Kit observe lui-même l’adresse IP et ignore tout consent_ip_address que vous envoyez — une clé embarquée dans le JavaScript de la page ne doit pas pouvoir rédiger son propre justificatif.
Lors d’une soumission depuis votre serveur avec une clé sk_, l’adresse IP que Kit voit est celle de votre serveur, pas celle du candidat. Envoyez la vraie dans consent_ip_address — dans une Server Action Vercel, c’est le premier saut de x-forwarded-for :
import { headers } from "next/headers";
const forwarded = (await headers()).get("x-forwarded-for");
const consentIp = forwarded?.split(",")[0]?.trim();
Cette valeur est déclarée par vous, elle n’est pas prouvée : Kit enregistre ce que vous envoyez dès lors que cela s’analyse comme une adresse IP, et rejette tout le reste avec 422 invalid_consent_ip. Omettez-la si vous l’ignorez réellement — Kit ne consigne alors aucune adresse IP, ce qui constitue une lacune assumée plutôt qu’un justificatif désignant la mauvaise machine.
Erreurs
Les erreurs sont retournées dans une enveloppe cohérente :
{ "error": { "code": "validation_failed", "message": "Email can't be blank", "fields": { "email": ["can't be blank"] } } }
| Statut | Code | Signification |
|---|---|---|
| 401 | invalid_key |
Clé API manquante ou invalide |
| 403 | bot_protection_required |
La clé publiable (pk_) n’a ni liste d’origines autorisées ni Turnstile configuré |
| 403 | origin_not_allowed |
Origine du navigateur absente de la liste autorisée de la clé |
| 404 | not_found |
Offre introuvable ou non publiée |
| 409 | already_applied |
Cette adresse e-mail a déjà postulé à cette offre |
| 409 | already_in_talent_pool |
Cette adresse e-mail figure déjà dans le vivier de talents |
| 422 | validation_failed |
Champs de candidature invalides — y compris un CV obligatoire manquant ou un champ ou une question obligatoire sans réponse (voir fields) |
| 422 | consent_required |
Inscription au vivier de talents reçue sans case de consentement cochée |
| 422 | invalid_consent_ip |
consent_ip_address n’est pas une adresse IP valide |
| 422 | turnstile_failed |
Échec de la vérification Turnstile |
| 422 |
invalid_content_type / file_too_large / invalid_byte_size
|
Téléversement de CV refusé |
Suivi des statuts via webhooks
Pour suivre les candidatures après leur soumission, configurez des webhooks sortants. Les événements pertinents incluent application.submitted, application.advanced et application.rejected, ainsi que job_posting.published/paused/closed. Les payloads de candidature contiennent à la fois l’id numérique et le prefix_id de l’API (app_…), ainsi que le public_token de l’offre, pour corréler les événements webhook avec les données de l’API.
SDK et modèle Next.js
Deux points de départ officiels et open source s’appuient sur le contrat REST ci-dessus — utilisez l’un des deux, ou passez outre et appelez directement les points de terminaison avec n’importe quel client HTTP.
-
SDK TypeScript —
@startupkit-app/jobs. Un client typé et sans dépendances (fetchnatif, ESM + CJS, Node ≥ 18.17) qui tourne sous Node, dans les navigateurs et les runtimes edge. Installez-le avecnpm install @startupkit-app/jobs, puis :import { createClient } from "@startupkit-app/jobs"; const kit = createClient({ secretKey: process.env.KIT_SECRET_KEY }); const page = await kit.listJobs({ department: "Engineering", remote: true }); const job = await kit.getJob(page.data[0].id); const { signed_id } = await kit.uploadFile(resumeFile); await kit.apply(job.id, { email: "[email protected]", resume_signed_id: signed_id });Dans le code navigateur, passez
publishableKey(pk_…) au lieu desecretKey. Autres méthodes :allJobs()itère de façon asynchrone sur toutes les pages,createUpload()offre un contrôle de plus bas niveau sur le téléversement présigné, etgetTalentPool()/joinTalentPool()couvrent l’inscription au vivier de talents décrite ci-dessus. Les réponses non-2xx lèventKitApiError(avec.codeet.fields) ; les échecs qui n’atteignent jamais l’API lèventKitNetworkError. Le client utilise par défaut l’URL de basehttps://app.startupkit.app; passezbaseUrlpour la remplacer par un domaine carrière personnalisé. -
Modèle Next.js —
nextjs-job-board. Un site carrière prêt pour la production (Next.js App Router, Server Components + Server Actions, ISR avec revalidation par tag) que vous pouvez forker ou déployer en un clic. Il génère le formulaire de candidature dynamiquement à partir du schéma de l’API, effectue des téléversements de CV présignés directement vers le stockage, émet du JSON-LD schema.orgJobPosting(prêt pour Google for Jobs) et revalide optionnellement de façon instantanée via des webhooks. Définissez une seule variable d’environnement —STARTUPKIT_SECRET_KEY(votre clésk_…) — et déployez :Démo en ligne : nextjs-job-board-orcin.vercel.app. Variables d’environnement optionnelles :
STARTUPKIT_BASE_URL(par défauthttps://app.startupkit.app),NEXT_PUBLIC_TURNSTILE_SITE_KEY,REVALIDATE_SECRETetNEXT_PUBLIC_COMPANY_NAME.
Les deux consomment le contrat ci-dessus, vous pouvez donc aussi développer avec n’importe quel framework via du simple HTTP. Un site d’emploi personnalisé typique enchaîne quatre appels : lister les offres, récupérer une offre avec son formulaire, demander un téléversement présigné pour le CV et soumettre la candidature. Comme il s’agit de simple JSON sur HTTPS, la même intégration fonctionne côté serveur (clé sk_) comme dans le navigateur (clé pk_ avec une protection anti-bot configurée).
Limites de débit
Les soumissions de candidatures sont limitées à 10/heure par IP et les requêtes de téléversement à 30/heure par IP et 300/heure par clé, en plus des limites de débit globales de l’API. Les clés navigateur sont en outre protégées par leur liste d’origines autorisées et, en option, par Turnstile.
Les inscriptions au vivier de talents sont limitées à 100/heure par clé. Celles qui viennent d’un navigateur (pk_) sont en outre soumises à 5/heure par IP ; celles qui viennent d’un serveur (sk_) ne le sont pas, car elles parviennent toutes à Kit depuis la même adresse de sortie et un plafond par IP briderait tout votre site plutôt qu’une seule personne.