StartupKit
FR

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 ; vous pouvez aussi passer outre les deux et appeler directement les points de terminaison REST ci-dessous avec n’importe quel client HTTP.

Découvrez un exemple chez un client avec le site carrière sur mesure de Fourthwall. La présentation couvre aussi le modèle Next.js de Kit.

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 avec 403 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 https://app.startupkit.app (valeur par défaut du SDK ; les deux servent la même API), ou votre domaine carrière personnalisé.

Lister les offres publiées

GET /api/public/v1/jobs?department=&location=&employment_type=&remote=&locale=&page=&per_page=

Retourne uniquement les postes publiés de votre compte.

{
  "data": [
    {
      "id": "JdK2hQ8…",
      "title": "Senior Rails Developer",
      "department": "Engineering",
      "location": "Poznań, Poland",
      "employment_type": "full_time",
      "remote": true,
      "location_display": "Poznań, Poland · Remote (Poland, EU)",
      "city": "Poznań",
      "region": null,
      "country_code": "PL",
      "remote_regions": ["PL", "EU"],
      "published_at": "2026-06-01T12:00:00Z",
      "url": "https://careers.yourco.com/JdK2hQ8…",
      "apply_url": "https://careers.yourco.com/JdK2hQ8…/apply",
      "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. Le champ employment_type prend l’une des valeurs full_time, part_time, b2b, contract ou internship. Vous pouvez transmettre cette même valeur au point de terminaison de liste pour filtrer les offres publiées.

Localisation et postes en télétravail

Affichez location_display. C’est le libellé de lieu prêt à l’emploi : la localisation, la mention « Remote » et les pays où les candidats en télétravail peuvent être basés, chacun énoncé une seule fois (Poznań, Poland · Remote (Poland, EU), Berlin, Remote). Passez locale=de, fr, es ou pl pour l’obtenir dans cette langue ; par défaut, il est en anglais.

Ne concaténez pas location et remote vous-même. location est du texte libre, tel que le recruteur l’a saisi, et peut déjà contenir « Remote » : c’est ainsi qu’un site d’emploi finit par l’afficher deux fois.

Les champs structurés servent aux filtres, au tri et au balisage Google for Jobs :

Champ Description
city, region Le bureau ou la ville du poste, lorsqu’ils sont connus. Sinon null.
country_code Pays de ce lieu au format ISO 3166-1 alpha-2 (PL), ou null.
remote true lorsque le poste peut être exercé à distance.
remote_regions Où les candidats en télétravail peuvent être basés : codes ISO 3166-1 alpha-2, plus EU pour tout État membre de l’UE. Vide lorsque le poste n’est pas en télétravail ou n’impose aucune restriction.

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, et 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 effectivement 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, car 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 : Kit ne consigne alors aucune adresse IP, ce qui constitue une lacune assumée plutôt qu’un justificatif désignant la mauvaise machine.

Statistiques des offres

Le tableau de bord Recrutement → Analytique du portail carrière de Kit (vues, visiteurs uniques, sources de trafic, campagnes UTM, pays, appareils, pages d’entrée et l’entonnoir de la vue à la candidature) se remplit aussi depuis votre propre site. Votre site signale ce que font les visiteurs ; Kit l’enregistre exactement comme les visites du portail carrière hébergé, si bien que chaque graphique et l’onglet statistiques de chaque offre fonctionnent sans rien changer.

Tant que votre site n’envoie pas d’événements, le tableau de bord affiche un rappel dès que l’une de vos clés API a été utilisée sur la période sélectionnée.

Les événements viennent du navigateur du visiteur, avec une clé publiable (pk_). Kit lit l’adresse IP (masquée), l’appareil et le pays du visiteur dans cette requête : relayer les événements par votre serveur enregistrerait chaque visiteur comme étant votre serveur. Les clés secrètes sont refusées avec publishable_key_required. Si la clé a une liste d’origines autorisées, ajoutez-y l’origine de votre site.

Envoyer des événements

POST /api/public/v1/events

{
  "events": [
    {
      "name": "job.viewed",
      "job": "abc123",
      "url": "https://careers.acme.com/jobs/abc123?utm_source=linkedin",
      "referrer": "https://www.linkedin.com/",
      "time": "2026-09-22T10:00:00.000Z"
    }
  ]
}
Événement Quand job
job_board.viewed La page de liste des offres est affichée Sans objet
job.viewed Une page d’offre est affichée Obligatoire
application.started Le visiteur interagit pour la première fois avec le formulaire de candidature Obligatoire
application.submitted Une candidature a été acceptée Obligatoire
talent_pool.joined Une inscription au vivier de talents a été acceptée Sans objet
  • job est l’id de l’offre (jeton public) retourné par GET /jobs.
  • url est la page où l’événement s’est produit (location.href) et referrer vaut document.referrer. Le premier événement d’une visite fixe sa page d’entrée, son domaine référent et sa campagne utm_*. Une url sur un autre hôte que l’Origin de la requête est ignorée, et un référent venant de votre propre site compte comme du trafic direct.
  • time est facultatif. Kit ne le conserve que s’il tombe dans la dernière minute ; sinon il enregistre l’heure d’arrivée de l’événement.
  • Envoyez jusqu’à 20 événements par requête. Les robots d’indexation sont écartés silencieusement.

La réponse est 202 Accepted avec un verdict par événement, si bien qu’un jeton mal saisi n’écarte pas le reste du lot :

{ "accepted": 1, "rejected": [{ "index": 1, "code": "unknown_job" }] }

Les codes par événement sont unknown_event, job_required et unknown_job.

Le SDK TypeScript (à partir de la version 0.5) encapsule tout cela dans createTracker. Il regroupe les événements, les envoie à la fermeture de l’onglet, ne fait rien pendant le rendu côté serveur et ne lève jamais d’exception :

import { createTracker } from "@startupkit-app/jobs";

const tracker = createTracker({ publishableKey: process.env.NEXT_PUBLIC_STARTUPKIT_PUBLISHABLE_KEY });

tracker.jobBoardViewed();
tracker.jobViewed(job.id);
tracker.applicationStarted(job.id);
tracker.applicationSubmitted(job.id);
tracker.talentPoolJoined();

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é
403 publishable_key_required Événements de statistiques envoyés avec une clé secrète (sk_)
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 too_many_events Plus de 20 événements de statistiques dans une même requête
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 (fetch natif, ESM + CJS, Node ≥ 20.19) qui tourne sous Node, dans les navigateurs et les runtimes edge. Installez-le avec npm 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 de secretKey. 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é, et getTalentPool() / joinTalentPool() couvrent l’inscription au vivier de talents décrite ci-dessus, et createTracker() envoie les statistiques des offres. Les réponses non-2xx lèvent KitApiError (avec .code et .fields) ; les échecs qui n’atteignent jamais l’API lèvent KitNetworkError. Le client utilise par défaut l’URL de base https://app.startupkit.app ; passez baseUrl pour 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.org JobPosting (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 :

    Deploy with Vercel

    Démo en ligne : nextjs-job-board-orcin.vercel.app. Variables d’environnement optionnelles : STARTUPKIT_BASE_URL (par défaut https://app.startupkit.app), NEXT_PUBLIC_TURNSTILE_SITE_KEY, KIT_WEBHOOK_SIGNING_SECRET et NEXT_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).

Convention pour les pages d’offres externes

Pour recevoir les visiteurs des liens Kit publiés sur les sites d’emploi, votre site doit proposer les détails des postes à {careers_homepage}/jobs/{public_token}. Utilisez l’id du poste fourni par l’API (le jeton public), et non l’identifiant numérique de la base de données ni un slug composé uniquement du titre. Pour une page d’accueil à https://example.com/careers, l’URL détaillée est https://example.com/careers/jobs/senior-engineer-abc123. Si vous déployez le modèle Next.js sous /careers, configurez basePath dans Next.js en conséquence.

Activez la page d’accueil externe et Afficher les offres sur votre site carrière externe dans les paramètres du portail carrière uniquement lorsque ces pages fonctionnent. Les comptes existants conservent les pages Kit tant qu’ils n’activent pas l’option.

Les URL publiques habituelles des offres Kit suivent le réglage des pages externes lorsque le poste est public et accepte les candidatures. Cela comprend les liens partagés, ceux des sites d’emploi et l’url du poste renvoyée par l’API publique. Ajoutez destination=kit pour afficher explicitement l’offre sur Kit. Désactiver l’option permet aux mêmes URL publiques d’afficher à nouveau la page Kit. Les intégrations embarquées, les réponses JSON et les routes de candidature Kit restent sur Kit. Les paramètres de requête et les fragments de l’URL de la page d’accueil ne sont pas repris dans le chemin externe du poste.

Utilisez l’apply_url de l’API pour le formulaire de candidature Kit : elle reste toujours sur Kit. Si ce champ manque dans une ancienne réponse de l’API, ajoutez /apply uniquement si url utilise un domaine carrière Kit actif ou une route Kit valide sous /careers/:slug/:public_token. Conservez les paramètres de requête. Si url pointe vers un domaine d’entreprise non vérifié, mettez à jour l’intégration API et utilisez apply_url plutôt que de construire un lien de candidature. Kit transmet utm_source, utm_medium, utm_campaign, utm_content, utm_term et une locale explicitement demandée. Conservez les paramètres de campagne dans le lien menant de la page externe au formulaire de candidature afin de préserver l’attribution. Utilisez destination=kit pour revenir à la description du poste sur Kit. Vous pouvez aussi utiliser votre propre page de candidature utilisant l’API. Le modèle possède sa propre page /jobs/{public_token}/apply.

Les anciens liens se terminant par #apply restent compatibles : le navigateur ouvre la route de candidature dédiée de Kit avant de suivre une redirection externe. Les autres visites HTML habituelles mènent à la page externe de l’offre. Sans JavaScript, la page de transition propose Voir l’offre et Postuler ici. Les requêtes HEAD redirigent directement vers la page externe. Pour les nouveaux liens de candidature, utilisez apply_url ou /{public_token}/apply. Les candidats connectés consultent les descriptions des postes dans leur portail candidat, avec un lien de retour à leurs candidatures.

Conservez cette route lors d’une refonte du site. Un jeton public introuvable doit renvoyer 404 ; un poste fermé doit clairement indiquer que les candidatures sont closes. Testez un véritable lien de site d’emploi et le parcours de candidature avant d’activer les redirections. Les destinations doivent utiliser HTTPS sur un domaine d’entreprise vérifié. Kit ne consulte jamais le site externe pour le valider.

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 événements de statistiques sont limités à 60 requêtes/minute et 600/heure par IP, et à 10 000 requêtes/heure par clé. Ils échappent à la limite d’API par clé, qui plafonnerait sinon un site très fréquenté partagé par tous ses visiteurs.

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.

Tapez pour rechercher...