StartupKit
ES

API pública de empleos

Construye tu propio sitio de empleo (por ejemplo, con Next.js en Vercel) sobre tu pipeline de contratación de Kit. Lista los puestos publicados y recibe candidaturas a través de una API REST pública, un SDK de TypeScript oficial y una plantilla de Next.js con un clic.

Por qué es importante

El portal de empleo alojado por Kit y el widget integrable cubren la mayoría de las necesidades. Pero si quieres control total sobre el diseño (un sitio de empleo a medida, una landing page propia para cada puesto o una bolsa de trabajo que encaje con tu producto), la API pública de empleos te permite leer tus ofertas publicadas y enviar candidaturas directamente a tu pipeline de Kit, mientras Kit sigue encargándose del cribado, las etapas, las entrevistas y la comunicación con los candidatos.

También hay un SDK de TypeScript oficial y una plantilla de Next.js desplegable con un clic para publicar un sitio de empleo a medida en minutos. También puedes prescindir de ambos y llamar directamente a los endpoints REST de abajo con cualquier cliente HTTP.

Para ver cómo lo usa un cliente, consulta el ejemplo de la web de empleo a medida de Fourthwall. También encontrarás el proyecto inicial de Next.js de Kit.

Claves de API

Crea un par de claves en Contratación → Portal de empleo → Claves de API públicas. Cada par incluye:

  • Clave publicable (pk_…): segura para incluir en un navegador. Puede leer ofertas publicadas y enviar candidaturas, nada más, y nunca expone datos de los candidatos. Antes de poder enviar candidaturas o solicitar subidas prefirmadas, debes configurar al menos una defensa antibots (una lista de orígenes permitidos o tu propio widget de Cloudflare Turnstile); de lo contrario, esas peticiones se rechazan con 403 bot_protection_required. La lectura de ofertas funciona sin esa configuración.
  • Clave secreta (sk_…): solo para uso en el servidor (por ejemplo, una Server Action de Next.js). Se salta las comprobaciones de origen y Turnstile del navegador. Nunca la expongas en código del lado del cliente. Ninguna de las dos claves puede leer datos personales de los candidatos.

La clave secreta se muestra una sola vez, al crearla o rotarla. Puedes rotarla en cualquier momento desde la página de configuración de la clave; la clave secreta anterior deja de funcionar de inmediato.

Autentica cada petición con una cabecera bearer:

Authorization: Bearer sk_your_secret_key

Endpoints

URL base: https://startupkit.app o https://app.startupkit.app (la predeterminada del SDK; ambas sirven la misma API), o tu dominio personalizado de empleo.

Listar ofertas publicadas

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

Devuelve únicamente los puestos publicados de tu cuenta.

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

El id es el token público de la oferta; úsalo en los endpoints de detalle y de candidatura. El valor de employment_type puede ser full_time, part_time, b2b, contract o internship. Puedes pasar ese mismo valor al endpoint de listado para filtrar las ofertas publicadas.

Ubicación y puestos remotos

Muestra location_display. Es la etiqueta de lugar ya terminada: la ubicación, la palabra «Remote» y dónde pueden estar los candidatos en remoto, cada cosa dicha una sola vez (Poznań, Poland · Remote (Poland, EU), Berlin, Remote). Pasa locale=de, fr, es o pl para recibirla en ese idioma; por defecto llega en inglés.

No juntes location y remote por tu cuenta. location es texto libre, tal cual lo escribió quien publicó la oferta, y puede decir ya «Remote»: así es como un portal acaba imprimiéndolo dos veces.

Los campos estructurados sirven para filtros, ordenación y el marcado de Google for Jobs:

Campo Descripción
city, region La oficina o ciudad del puesto, cuando se conoce. Si no, null.
country_code País de ese lugar en ISO 3166-1 alfa-2 (PL), o null.
remote true cuando el puesto se puede desempeñar en remoto.
remote_regions Dónde pueden estar los candidatos en remoto: códigos ISO 3166-1 alfa-2, más EU para cualquier Estado miembro de la UE. Vacío cuando el puesto no es remoto o no fija ninguna restricción.

Obtener una oferta con su formulario de candidatura

GET /api/public/v1/jobs/:public_token

Devuelve la oferta junto con un application_form que describe exactamente qué campos y preguntas mostrar, el aviso de consentimiento que hay que mostrar, si el currículum es obligatorio junto con los tipos y el tamaño aceptados, y si Turnstile es obligatorio.

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

Los indicadores required se aplican en el servidor. Cuando resume.required es true, una candidatura sin resume_signed_id se rechaza con 422 validation_failed. Lo mismo ocurre con cualquier campo o pregunta marcado como required: true que quede sin responder. Si renderizas el formulario dinámicamente a partir de este esquema, respeta estos indicadores para que los candidatos no se topen con un rechazo después de haberlo rellenado todo.

Subir un currículum (URL prefirmada)

Los currículums se suben directamente al almacenamiento, así que nunca pasan por tu servidor (lo que evita los límites de tamaño del cuerpo en entornos 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": "…" } }
}

Envía los bytes del archivo con PUT a direct_upload.url usando los headers devueltos, y luego pasa el signed_id como resume_signed_id al enviar la candidatura.

Enviar una candidatura

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>"
}

Devuelve 201 con una confirmación mínima, sin datos personales:

{ "id": "app_9fQ…", "status": "submitted", "job": "JdK2hQ8…", "submitted_at": "2026-06-11T09:30:00Z" }

El turnstile_token solo se necesita en envíos desde el navegador (pk_) cuando la clave tiene Turnstile configurado; las llamadas desde el servidor (sk_) se lo saltan.

Bolsa de talento

No todo el que se interesa por tu empresa encaja hoy en un puesto abierto. La bolsa de talento recoge a esas personas (correo, LinkedIn y, si quieren, un currículum) para que puedas avisarlas cuando se abra el puesto adecuado. Las altas llegan sin verificar: Kit envía por correo un enlace de confirmación y nadie entra en tu bolsa por este endpoint hasta que lo pulsa. (Los administradores también pueden importar CV directamente, que es otra puerta con sus propias reglas.)

Un alta en la bolsa de talento es alguien que te pide guardar sus datos sin ninguna oferta a la que presentar candidatura, así que requiere consentimiento explícito: una casilla marcada por la persona, no un aviso que le muestras. Kit rechaza cualquier alta pública que lo omita. Una importación de administrador registra en su lugar una declaración de base jurídica, porque allí no hay nadie que pueda marcar nada.

Leer el formulario de alta

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 }
}

Renderiza consent.disclosure_html como etiqueta de una casilla que empieza sin marcar. Es tu propio texto de consentimiento; edítalo en Contratación → Configuración → Consentimiento, junto con retention_months, que determina cuándo Kit anonimiza las altas que nadie renovó.

Unirse a la bolsa de talento

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>"
}

Devuelve 201 con una confirmación sin datos personales:

{ "id": "tpe_9fQ…", "status": "pending_verification", "submitted_at": "2026-06-11T09:30:00Z" }

Los currículums usan la misma subida prefirmada que las candidaturas: solicita un signed_id, envía los bytes con PUT y luego pásalo como resume_signed_id.

Dejar constancia de quién dio su consentimiento

Kit guarda un comprobante de consentimiento en cada alta: el texto exacto que se mostró, la marca de tiempo y la dirección IP de quien lo aceptó. Ese último campo es donde las integraciones del lado del servidor fallan sin hacer ruido.

Si el envío llega desde un navegador con una clave pk_, Kit observa la IP por su cuenta e ignora cualquier consent_ip_address que le mandes, porque una clave que viaja en el JavaScript de la página no puede escribir su propio comprobante.

Si el envío llega desde tu servidor con una clave sk_, la IP que Kit ve es la de tu servidor, no la de la persona. Manda la real en consent_ip_address. En una Server Action de Vercel, es el primer salto de x-forwarded-for:

import { headers } from "next/headers";

const forwarded = (await headers()).get("x-forwarded-for");
const consentIp = forwarded?.split(",")[0]?.trim();

Este valor lo declaras tú; Kit no lo demuestra: guarda lo que envíes en cuanto se interpreta como una dirección IP, y rechaza cualquier otra cosa con 422 invalid_consent_ip. Omítelo si no lo sabes: así Kit no registra ninguna IP, en lugar de guardar un comprobante que señala a la máquina equivocada.

Analítica de ofertas

El panel Contratación → Analítica del portal de empleo de Kit (vistas, visitantes únicos, fuentes de tráfico, campañas UTM, países, dispositivos, páginas de entrada y el embudo de vista a candidatura) también se alimenta de tu propio sitio. Tu sitio informa de lo que hacen los visitantes; Kit lo registra exactamente igual que las visitas al portal de empleo alojado, así que todas las gráficas y la pestaña de analítica de cada oferta funcionan sin cambios.

Hasta que tu sitio envíe eventos, el panel muestra un recordatorio siempre que alguna de tus claves de API se haya usado en el periodo seleccionado.

Los eventos salen del navegador del visitante con una clave publicable (pk_). Kit lee de esa petición la IP (enmascarada), el dispositivo y el país del visitante, así que reenviar los eventos a través de tu servidor registraría a cada visitante como si fuera tu servidor; las claves secretas se rechazan con publishable_key_required. Si la clave tiene una lista de orígenes permitidos, añade a ella el origen de tu sitio.

Enviar eventos

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"
    }
  ]
}
Evento Cuándo job
job_board.viewed Se muestra la página con la lista de ofertas No aplica
job.viewed Se muestra la página de una oferta Obligatorio
application.started El visitante interactúa por primera vez con el formulario de candidatura Obligatorio
application.submitted Se aceptó una candidatura Obligatorio
talent_pool.joined Se aceptó un alta en la bolsa de talento No aplica
  • job es el id de la oferta (token público) que devuelve GET /jobs.
  • url es la página en la que ocurrió el evento (location.href) y referrer es document.referrer. El primer evento de una visita fija su página de entrada, su dominio de referencia y su campaña utm_*. Una url en un host distinto del Origin de la petición se ignora, y un referrer de tu propio sitio cuenta como tráfico directo.
  • time es opcional. Kit solo lo conserva si cae dentro del último minuto; si no, registra la hora en que llegó el evento.
  • Envía hasta 20 eventos por petición. Los rastreadores se descartan en silencio.

La respuesta es 202 Accepted con un veredicto por evento, así que un token mal escrito no descarta el resto del lote:

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

Los códigos por evento son unknown_event, job_required y unknown_job.

El SDK de TypeScript (0.5 en adelante) encapsula todo esto en createTracker. Agrupa los eventos, los envía al cerrar la pestaña, no hace nada durante el renderizado en el servidor y nunca lanza excepciones:

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();

Errores

Los errores se devuelven siempre con la misma estructura:

{ "error": { "code": "validation_failed", "message": "Email can't be blank", "fields": { "email": ["can't be blank"] } } }
Estado Código Significado
401 invalid_key Clave de API ausente o inválida
403 bot_protection_required La clave publicable (pk_) no tiene configurada ni lista de orígenes permitidos ni Turnstile
403 origin_not_allowed El origen del navegador no está en la lista de permitidos de la clave
403 publishable_key_required Eventos de analítica enviados con una clave secreta (sk_)
404 not_found Oferta no encontrada o no publicada
409 already_applied Este correo ya envió una candidatura a esta oferta
409 already_in_talent_pool Este correo ya está en la bolsa de talento
422 validation_failed Campos de candidatura inválidos, incluido un currículum obligatorio que falta o un campo o pregunta obligatorios sin responder (consulta fields)
422 consent_required El alta en la bolsa de talento llegó sin la casilla de consentimiento marcada
422 invalid_consent_ip consent_ip_address no es una dirección IP válida
422 turnstile_failed Falló la verificación de Turnstile
422 too_many_events Más de 20 eventos de analítica en una sola petición
422 invalid_content_type / file_too_large / invalid_byte_size Subida de currículum rechazada

Actualizaciones de estado vía webhooks

Para hacer seguimiento de las candidaturas después del envío, configura webhooks salientes. Los eventos relevantes incluyen application.submitted, application.advanced y application.rejected, además de job_posting.published/paused/closed. Los payloads de candidatura incluyen tanto el id numérico como el prefix_id de la API (app_…), y el public_token de la oferta, para que puedas correlacionar los eventos de webhook con los registros de la API.

SDK y plantilla de Next.js

Dos puntos de partida oficiales y de código abierto se apoyan en el contrato REST de arriba. Usa cualquiera de los dos, o prescinde de ambos y llama a los endpoints directamente con cualquier cliente HTTP.

  • SDK de TypeScript: @startupkit-app/jobs. Un cliente tipado y sin dependencias (fetch nativo, ESM + CJS, Node ≥ 20.19) que se ejecuta en Node, navegadores y runtimes edge. Instálalo con npm install @startupkit-app/jobs y luego:

    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 });
    

    En código de navegador, pasa publishableKey (pk_…) en lugar de secretKey. Otros métodos: allJobs() itera de forma asíncrona por todas las páginas, createUpload() da control de más bajo nivel sobre la subida prefirmada y getTalentPool() / joinTalentPool() cubren el alta en la bolsa de talento descrita arriba, y createTracker() envía la analítica de ofertas. Las respuestas que no son 2xx lanzan KitApiError (con .code y .fields); los fallos que nunca llegan a la API lanzan KitNetworkError. El cliente usa por defecto la URL base https://app.startupkit.app; pasa baseUrl para sobrescribirla con un dominio de empleo personalizado.

  • Plantilla de Next.js: nextjs-job-board. Un sitio de empleo listo para producción (Next.js App Router, Server Components + Server Actions, ISR con revalidación por etiqueta) que puedes bifurcar o desplegar con un clic. Renderiza el formulario de candidatura dinámicamente a partir del esquema de la API, hace subidas de currículum prefirmadas directas al almacenamiento, emite JSON-LD schema.org JobPosting (listo para Google for Jobs) y, opcionalmente, revalida al instante mediante webhooks. Define una sola variable de entorno, STARTUPKIT_SECRET_KEY (tu clave sk_…), y despliega:

    Deploy with Vercel

    Demo en vivo: nextjs-job-board-orcin.vercel.app. Variables de entorno opcionales: STARTUPKIT_BASE_URL (por defecto https://app.startupkit.app), NEXT_PUBLIC_TURNSTILE_SITE_KEY, KIT_WEBHOOK_SIGNING_SECRET y NEXT_PUBLIC_COMPANY_NAME.

Ambos consumen el contrato de arriba, así que también puedes desarrollar con cualquier framework usando HTTP plano. Un sitio de empleo personalizado típico encadena cuatro llamadas: listar las ofertas, obtener una oferta con su formulario, solicitar una subida prefirmada para el currículum y enviar la candidatura. Como todo es JSON plano sobre HTTPS, la misma integración funciona desde el servidor (clave sk_) o desde el navegador (clave pk_ con una defensa antibots configurada).

Convención para las páginas de ofertas externas

Para recibir visitas desde los enlaces de Kit publicados en portales de empleo, tu sitio debe mostrar los detalles de cada oferta en {careers_homepage}/jobs/{public_token}. Usa el id de la oferta que devuelve la API (el token público), no el identificador numérico de la base de datos ni un slug formado solo por el título. Para una página principal en https://example.com/careers, la URL de detalle es https://example.com/careers/jobs/senior-engineer-abc123. Si despliegas la plantilla de Next.js bajo /careers, configura basePath en Next.js según corresponda.

Activa la página principal externa y Mostrar las ofertas en tu sitio de empleo externo en los ajustes del portal de empleo solo cuando esas páginas funcionen. Las cuentas existentes conservan las páginas de Kit hasta que activen la opción.

Las URL públicas habituales de las ofertas de Kit siguen el ajuste de páginas externas cuando la oferta es pública y acepta candidaturas. Esto incluye los enlaces compartidos, los de portales de empleo y el url de la oferta que devuelve la API pública. Añade destination=kit para mostrar expresamente la oferta en Kit. Al desactivar la opción, esas mismas URL públicas vuelven a mostrar la página de Kit. Los elementos incrustados, las respuestas JSON y las rutas de candidatura de Kit permanecen en Kit. Los parámetros de consulta y los fragmentos de la URL de la página principal no se copian en la ruta externa de la oferta.

Usa el apply_url de la API para el formulario de candidatura de Kit; siempre permanece en Kit. Si una respuesta de una versión anterior no incluye ese campo, añade /apply solo si url usa un dominio de empleo de Kit activo o una ruta válida de Kit bajo /careers/:slug/:public_token. Conserva los parámetros de consulta. Si url apunta a un dominio de empresa no verificado, actualiza la integración de la API y usa apply_url en lugar de construir un enlace de candidatura. Kit transmite utm_source, utm_medium, utm_campaign, utm_content, utm_term y una locale solicitada explícitamente. Conserva los parámetros de campaña al enlazar desde la página externa al formulario de candidatura para mantener la atribución. Usa destination=kit para volver a la descripción de la oferta en Kit. También puedes usar tu propia página de candidatura basada en la API. La plantilla usa su propia página /jobs/{public_token}/apply.

Los enlaces antiguos que terminan en #apply siguen funcionando: el navegador abre la ruta de candidatura dedicada de Kit antes de seguir una redirección externa. Las demás visitas HTML habituales llevan a la página externa de la oferta. Sin JavaScript, la página de transición ofrece Ver oferta y Presentar candidatura aquí. Las solicitudes HEAD redirigen directamente a la página externa. Para nuevos enlaces de candidatura, usa apply_url o /{public_token}/apply. Los candidatos que han iniciado sesión consultan las descripciones de los puestos en su portal de candidatos, con un enlace para volver a sus candidaturas.

Mantén esta ruta cuando rediseñes tu sitio. Un token público inexistente debe devolver 404; una oferta cerrada debe indicar claramente que ya no acepta candidaturas. Prueba un enlace real de un portal de empleo y su proceso de candidatura antes de activar las redirecciones. Los destinos deben usar HTTPS en un dominio de empresa verificado. Kit no consulta el sitio externo para validarlo.

Límites de uso

Los envíos de candidaturas están limitados a 10/hora por IP y las peticiones de subida a 30/hora por IP y 300/hora por clave, además de los límites de uso globales de la API. Las claves de navegador están protegidas adicionalmente por su lista de orígenes permitidos y, de forma opcional, por Turnstile.

Los eventos de analítica están limitados a 60 peticiones/minuto y 600/hora por IP, y a 10 000 peticiones/hora por clave. Están exentos del límite de la API por clave, que de otro modo frenaría un sitio con mucho tráfico compartido por todos sus visitantes.

Las altas en la bolsa de talento están limitadas a 100/hora por clave. Las altas desde el navegador (pk_) suman un límite adicional de 5/hora por IP; las del servidor (sk_) no, porque todas llegan a Kit desde la misma dirección de salida y un tope por IP limitaría tu sitio entero en lugar de a una sola persona.

Escriba para buscar...