Logo 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 — 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 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 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 (fetch natif, ESM + CJS, Node ≥ 18.17) 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. 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, REVALIDATE_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).

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.

Tapez pour rechercher...