Claves de API y acceso programático
Crea formularios, clónalos y genera enlaces de admisión de clientes desde tu propio backend usando claves de API con alcance de workspace.
Si ya operas un CRM, un sistema de gestión de casos o un portal interno, no tienes que entrar al panel para crear cada formulario. Las claves de API permiten que tu backend invoque la API v1 de DS160.io directamente: crear formularios, clonarlos y generar enlaces de admisión de clientes en tu propio horario.
Las claves de API están disponibles en workspaces de tipo Business. Cada clave está vinculada a un único workspace y solo puede operar dentro de él.
URL base de la API
Todos los endpoints v1 tienen como raíz:
https://ds160.io/api/v1
Estos documentos pueden alojarse en el dominio de marca blanca de tu agencia, pero las llamadas a la API siempre van a ds160.io. Todos los ejemplos de código en esta página usan la URL completa para que puedas copiar y pegar sin reescribir.
Referencia de endpoints
/v1 es estable — las rutas y nombres de campos no cambiarán sin una versión mayor /v2 (ver Versionado y soporte). Las rutas de la tabla siguiente se muestran relativas al prefijo de workspace /workspaces/:workspaceId/; las formas completas de las solicitudes y respuestas se documentan en la sección de cada endpoint.
| Método | Ruta | Scope | Cuerpo de la solicitud |
|---|---|---|---|
POST | /forms | forms:write | name? |
POST | /forms/:formId/clone | forms:clone | disabledSections? |
POST | /forms/:formId/client-links | client-links:write | expiresInDays, defaultLanguage, hideBranding? |
GET | /forms | forms:read / forms:write | query: limit?, cursor? |
GET | /forms/:formId | forms:read / forms:write | — |
Todos los cuerpos y respuestas son JSON. El scope forms:read otorga acceso solo de lectura a los metadatos de formularios; forms:write otorga lectura y escritura, así que una clave que solo escribe también tiene lectura gratis.
1. Crear una clave de API
Abre Configuración del Espacio de Trabajo → Claves de API y haz clic en Crear clave de API. Elige un nombre descriptivo (recomendamos una clave por integración, p. ej. Production CRM o Staging webhook handler) y selecciona los scopes que la integración necesita:
forms:read— leer metadatos de formularios (listar / obtener)forms:write— crear formularios; también otorga lecturaforms:clone— duplicar un formulario existenteclient-links:write— generar enlaces de admisión de clientes
Solo los Propietarios y Administradores del workspace pueden generar claves.
Cuando haces clic en Crear clave, la plataforma te muestra el secreto completo una sola vez. Cópialo inmediatamente a tu gestor de secretos: tras cerrar el modal solo se mostrarán los últimos cuatro caracteres. Si pierdes una clave, revócala y genera una nueva.
2. Autenticar
Todos los endpoints v1 esperan un encabezado Authorization: Bearer <secret>.
export DS160_KEY="...the secret you just copied..."
export DS160_WORKSPACE="your-workspace-id"
curl https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms \
-H "Authorization: Bearer $DS160_KEY" El id del workspace aparece en la URL de cada página del panel (/workspaces/<workspaceId>/...). El workspace de la clave está vinculado en el servidor: llamar a un endpoint de otro workspace con la clave equivocada devuelve 403 Forbidden.
Consejo: las pestañas de lenguaje se sincronizan entre todos los fragmentos de esta página. Elige tu lenguaje una vez y el resto de la página lo seguirá.
3. Crear un formulario
curl -X POST https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Smith / B1 — 2026-05" }'
# → { "formId": "65f2…" } Cada llamada consume un crédito de formulario de tu plan de facturación (igual que crear un formulario desde el panel). Si tu workspace se queda sin créditos, la llamada devuelve 402 Payment Required: recarga antes de reintentar.
El campo opcional name establece una etiqueta legible para el formulario (hasta 200 caracteres). Omítelo para obtener un nombre generado automáticamente — de cualquier modo puedes renombrar el formulario desde el panel más adelante.
4. Clonar un formulario existente
Si tienes un formulario plantilla que reemites con frecuencia (por ejemplo, el mismo programa J-1 del mismo empleador), clónalo en lugar de partir de cero. Opcionalmente pasa disabledSections para omitir páginas específicas — cualquier elemento listado se reemplaza con campos vacíos en el nuevo formulario, de modo que el cliente lo complete desde cero:
curl -X POST https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$SOURCE_FORM_ID/clone \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "disabledSections": ["spouse-info-page", "security-background-page-5"] }'
# → { "formId": "65f3…" } Clonar consume un crédito de formulario igual que crear uno nuevo.
Valores válidos de disabledSections
Pasa un arreglo vacío (u omite el campo) para copiar todas las páginas. De lo contrario, usa cualquier combinación de los identificadores que aparecen abajo — cualquier otro valor devuelve 400 Bad Request.
| Identificador | Página |
|---|---|
personal-info-page-1 | Personal Information - Part 1 |
personal-info-page-2 | Personal Information - Part 2 |
visa-purpose-page | Purpose of Visa |
travel-companions-page | Travel Companions |
previous-us-travel-page | Previous U.S. Travel History |
address-and-phone-page | Address and Phone Details |
passport-page | Passport Information |
contact-info-page | Contact Information |
family-info-page | Family Information |
spouse-info-page | Spouse Information |
deceased-spouse-info-page | Deceased Spouse Information |
former-spouse-info-page | Former Spouse Information |
present-occupation-page | Current Occupation |
previous-occuptation-page | Previous Occupation |
additional-occuptation-page | Additional Occupation Details |
security-background-page-1 | Security Background - Part 1 |
security-background-page-2 | Security Background - Part 2 |
security-background-page-3 | Security Background - Part 3 |
security-background-page-4 | Security Background - Part 4 |
security-background-page-5 | Security Background - Part 5 |
student-visa-page-1 | Student Visa Details - Part 1 |
student-visa-page-2 | Student Visa Details - Part 2 |
temporary-visa-page | Temporary Visa Information |
crew-visa-page | Crew Visa Information |
Solo las secciones relevantes para la categoría de visa del formulario están realmente presentes; listar una página que no existe en el formulario fuente no tiene efecto.
5. Generar un enlace de admisión de cliente
Una vez que existe un formulario, genera una URL con token que el cliente puede usar para completarlo:
curl -X POST https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$FORM_ID/client-links \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "expiresInDays": 7, "defaultLanguage": "en" }' Cuerpo de la solicitud
| Campo | Requerido | Notas |
|---|---|---|
expiresInDays | sí | Entero 1–365. Sin valor por defecto — omitirlo devuelve 400 Bad Request. El expiresAt del enlace se calcula como now + expiresInDays y el registro de token subyacente se elimina automáticamente en ese momento. |
defaultLanguage | sí | Uno de los códigos en Valores válidos de defaultLanguage. Establece el prefijo de localización en la url devuelta y el idioma en el que se abre el formulario. |
hideBranding | no | Booleano. Cuando es true, el formulario se muestra sin marca — el logo y tema whitelabel de la agencia se ocultan para este enlace. Por defecto es false (se muestra la marca whitelabel del workspace si está configurada). |
Ejemplo de respuesta (200 OK)
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NmMxYmQ0ZjZmNGFkNTQwMzMxNDhmYmEiLCJmb3JtSWQiOiI2NWYyYTkxMTNkZjAxYzAwMTI3YjY4MmEiLCJ3b3Jrc3BhY2VJZCI6IjY1ZjJhOTAwM2RmMDFjMDAxMjdiNjgwYSIsImV4cCI6MTc0ODA0ODI0NywiaWF0IjoxNzQ3NDQzNDQ3LCJkZWZhdWx0TGFuZ3VhZ2UiOiJlbiIsImhpZGVCcmFuZGluZyI6ZmFsc2V9.SiGNATuRe",
"url": "https://intake.your-agency.com/client-intake/65f2a9113df01c00127b682a?token=eyJhbGciOi…",
"expiresAt": "2026-05-24T01:50:46.000Z"
}
| Campo | Notas |
|---|---|
token | El JWT también incrustado en url. Normalmente no necesitas usarlo directamente — los clientes abren url y el servidor valida el token incrustado. El jti del token es el ID único del enlace; guárdalo en tu sistema si quieres revocar este enlace específico más adelante (ver Revocar un enlace de admisión de cliente). |
url | El enlace compartible. Usa tu dominio personalizado si está verificado y con SSL provisionado para el workspace; de lo contrario, recurre a ds160.io. El prefijo de ruta del idioma (/es, /cn, …) se ajusta según defaultLanguage. |
expiresAt | Marca de tiempo ISO-8601. El registro correspondiente se elimina automáticamente en ese punto, por lo que los enlaces no se pueden reutilizar tras la expiración. |
Envía la url a tu cliente. El token en la URL es de un solo uso y expira en expiresAt — no se requiere autenticación adicional para que el cliente lo complete.
Revocar un enlace de admisión de cliente
No existe un endpoint v1 para revocar enlaces de cliente emitidos — revoca desde el panel (Workspace → Formulario → Compartir → Revocar enlace). La revocación es inmediata y permanente: la siguiente solicitud que use el enlace devuelve 401. Si se sospecha una filtración y no hay nadie disponible para revocar desde la UI, la alternativa más segura es dejar que el enlace expire (limita expiresInDays en consecuencia).
Valores válidos de defaultLanguage
defaultLanguage controla el idioma de la interfaz de admisión cuando el destinatario abre el enlace por primera vez. Usa cualquiera de los códigos que aparecen abajo — cualquier otro valor devuelve 400 Bad Request. Los códigos cortos (en, cn, …) son los identificadores de localización internos de DS160.io; la columna BCP-47 muestra lo que emitimos en <html lang>, hreflang y las APIs de Intl.*. Usa el código corto en las solicitudes a la API; la forma BCP-47 es informativa.
| Código | Idioma | Nombre nativo | BCP-47 |
|---|---|---|---|
en | English | English | en-US |
ru | Russian | Русский | ru-RU |
ro | Romanian | Română | ro-RO |
es | Spanish | Español | es-ES |
cn | Chinese | 中文 | zh-CN |
vi | Vietnamese | Tiếng Việt | vi-VN |
hi | Hindi | हिन्दी | hi-IN |
nl | Dutch | Nederlands | nl-NL |
Trata los enlaces de cliente como contraseñas
La url es una credencial portadora — cualquiera que la posea puede completar el formulario hasta que expire, sin segundo factor.
- Envíala por un canal de confianza; no la publiques en lugares donde pueda filtrarse (chat público, páginas indexadas, paquetes compartidos).
- Redacta
?token=en los logs. El claimjtipor sí solo es seguro de registrar. - Revoca ante la sospecha de una filtración — la revocación es inmediata y permanente.
expiresInDayses un compromiso: más corto = ventana de fuga más pequeña, más largo = menos fricción para reemitir.
6. Listar u obtener formularios
# All forms in the workspace
curl https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms \
-H "Authorization: Bearer $DS160_KEY"
# A single form by id
curl https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$FORM_ID \
-H "Authorization: Bearer $DS160_KEY" Estos endpoints de lectura aceptan el scope forms:read (solo lectura) o forms:write (que también otorga lectura).
Paginación
GET /v1/workspaces/:workspaceId/forms está paginado. Usa los parámetros de query ?limit= y ?cursor= para recorrer los resultados:
| Parámetro | Significado |
|---|---|
limit | Máximo de formularios a devolver en esta página. Por defecto 50, tope 200. |
cursor | id del formulario del nextCursor de la página anterior. Omítelo en la primera solicitud para empezar desde el formulario más nuevo. |
Cada respuesta incluye un campo nextCursor. Cuando no hay más páginas, nextCursor es null. Ejemplo:
{
"forms": [
{ "id": "65f2a911…", "name": "Smith / B1", "status": "in_progress", "workspaceId": "65f2a900…", "userId": "65f2a8f0…", "preferredConsulate": null, "createdAt": "2026-05-14T09:12:33.000Z", "archivedAt": null },
{ "id": "65f2a8a3…", "name": "Garcia / F1", "status": "completed", "workspaceId": "65f2a900…", "userId": "65f2a8f0…", "preferredConsulate": "MAD", "createdAt": "2026-05-13T18:01:09.000Z", "archivedAt": null }
],
"nextCursor": "65f2a8a3…"
}
Avanza enviando ?cursor=65f2a8a3…&limit=50 en la siguiente solicitud. Los formularios se ordenan del más nuevo al más antiguo por id, así que un cursor fija tu posición de lectura incluso si se crean nuevos formularios entre llamadas — no verás duplicados ni te perderás nada que existiera en el momento en que empezaste a paginar.
Valores de status del formulario
El campo status en cada respuesta de metadatos de formulario es uno de:
| Valor | Cuándo aplica |
|---|---|
not_started | El formulario se creó (vía API o panel) pero ningún campo se ha respondido aún. |
in_progress | Al menos un campo se ha respondido. La mayoría de los formularios pasan la mayor parte de su vida en este estado. |
completed | El solicitante ha terminado y enviado la admisión; la agencia ya puede descargar/enviar el PDF DS-160. |
archived | El formulario fue archivado desde el panel. Excluido de los resultados predeterminados de listado a menos que consultes explícitamente por elementos archivados. |
El formato de cable usa guiones bajos (in_progress, no in-progress). Comparar contra las cadenas anteriores tal cual siempre funcionará.
Acceso a PII vía la API
Los endpoints de metadatos de formularios v1 (GET /workspaces/:workspaceId/forms y GET /workspaces/:workspaceId/forms/:formId) devuelven solo metadatos operacionales: id, name, status, workspaceId, userId, preferredConsulate, createdAt, archivedAt. No exponen las respuestas del solicitante — ningún nombre, fecha de nacimiento, número de pasaporte, historial de viajes ni ningún otro campo del DS-160 es accesible a través de /v1.
Los PDFs DS-160 completados y los datos completos de los campos son accesibles solo a través del panel, bajo el propietario auditado del solicitante. Si tu integración necesita leer datos ingresados por el solicitante, hazlo a través de tu propio flujo de admisión de cliente (tu CRM recopila los datos primero, tú los envías a DS160.io) — nunca asumas que la API te los devolverá.
Límites de tasa
Cada clave está limitada a 60 solicitudes por minuto. Cada respuesta incluye:
| Encabezado | Significado |
|---|---|
X-RateLimit-Limit | El tope (60). |
X-RateLimit-Remaining | Solicitudes restantes en la ventana actual. |
X-RateLimit-Reset | Marca de tiempo Unix cuando se reinicia la ventana. |
Si superas el límite, la API devuelve 429 Too Many Requests con un encabezado Retry-After. Si tu integración legítimamente necesita un límite mayor, contacta a soporte — podemos elevar el tope para claves específicas.
Idempotencia y reintentos
La API v1 no soporta una cabecera Idempotency-Key. Concretamente:
POST /formsyPOST /forms/:formId/cloneno son idempotentes y consumen un crédito de formulario en cada llamada. Un reintento ingenuo tras un timeout de red creará un formulario duplicado y consumirá un segundo crédito.POST /forms/:formId/client-linkstampoco es idempotente pero no consume créditos — un reintento simplemente acuña un segundo enlace con su propiojtiyexpiresAt.- Todos los endpoints
GETson seguros para reintentar libremente.
Patrón de reintento recomendado para create/clone: si una POST /forms (o /clone) hace timeout o devuelve un 5xx, no reintentes ciegamente. En su lugar, llama a GET /workspaces/:workspaceId/forms (los formularios se ordenan del más nuevo al más antiguo) y verifica si se creó un formulario con el name que proporcionaste en los últimos segundos. Si así fue, trata la llamada original como exitosa y omite el reintento. Si no, la escritura original definitivamente no se efectuó y un reintento es seguro.
Podríamos añadir soporte para Idempotency-Key en una futura versión menor; las integraciones deberían planificarlo pero no depender de ello hoy.
Revocar una clave
Si una clave se filtra, se retira de servicio o simplemente no se usa, revócala desde Configuración del Espacio de Trabajo → Claves de API → Revocar. La revocación surte efecto de inmediato — la próxima solicitud que use la clave devolverá 401 Unauthorized. Las claves revocadas permanecen en la lista para fines de auditoría pero no pueden reactivarse; genera una nueva si necesitas mantener la integración funcionando.
Buenas prácticas
- Una clave por integración. Más fácil de revocar quirúrgicamente cuando un sistema se retira, y la columna
Last useden el panel te indica qué integraciones siguen activas. - Scopes mínimos. Una clave que solo genera enlaces de cliente no debería tener
forms:clone. Scopes más pequeños significan menor radio de daño si el secreto se filtra. - Guarda los secretos en una bóveda. Nunca los comitas al control de versiones ni los incrustes en un bundle de frontend — cada pestaña del navegador los expondría.
- Rota con periodicidad. Genera una clave nueva, migra la integración y luego revoca la antigua. El panel muestra cuándo se usó cada clave por última vez para que confirmes el corte antes de revocar.
Errores
Todas las respuestas no-2xx comparten la misma estructura JSON:
{ "error": "mensaje legible por humanos" }
El valor de error es una cadena. Para 400 Bad Request por validación de esquema, es un array JSON serializado describiendo cada restricción violada; para todo lo demás, es un mensaje corto que puedes mostrar o registrar directamente.
| Estado | Cuándo ocurre | Cuerpo de ejemplo |
|---|---|---|
400 | El cuerpo de la solicitud o los parámetros de ruta fallaron la validación (campo requerido faltante, valor fuera de rango, valor de enum desconocido, etc.). | { "error": "[{\"keyword\":\"required\",\"params\":{\"missingProperty\":\"expiresInDays\"}}]" } |
401 | Falta la cabecera Authorization, token Bearer malformado, el secreto no coincide con ninguna clave, o la clave fue revocada. | { "error": "Missing API key" } · { "error": "Invalid API key" } · { "error": "API key revoked" } |
402 | El workspace se quedó sin créditos de formulario. Solo create/clone. Recarga el workspace y reintenta. | { "error": "Workspace has no remaining credits" } |
403 | La clave carece del scope requerido, o el :workspaceId en la ruta no coincide con el workspace de la clave. | { "error": "Missing required scope: forms:write" } · { "error": "API key does not match workspace" } |
404 | Formulario no encontrado, o el formulario existe pero vive en un workspace diferente al de la clave. (Se devuelve como 404 en lugar de 403 para no filtrar la existencia entre workspaces.) | { "error": "Form not found" } |
429 | Límite de tasa por clave excedido (predeterminado 60 req/min). Espera hasta el valor Retry-After (segundos) y reintenta. | { "error": "Rate limit exceeded" } |
5xx | Error transitorio del servidor. Seguro reintentar GETs libremente; para endpoints de escritura, ver Idempotencia y reintentos antes de reintentar. | — |
Ejemplo de extremo a extremo
Un flujo de admisión completo — crear un formulario, acuñar un enlace de cliente, enviarlo y consultar la finalización — usando curl. Las integraciones reales usarán su propio cliente HTTP; la forma es la misma en cualquier lenguaje.
# 0. credenciales (definir una vez)
export DS160_KEY="..."
export DS160_WORKSPACE="65f2a900..."
# 1. Crear un formulario. Capturar el formId de la respuesta.
FORM_ID=$(curl -s -X POST \
https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Smith / B1 — 2026-05" }' \
| jq -r .formId)
# 2. Acuñar un enlace de cliente válido por 14 días.
LINK_JSON=$(curl -s -X POST \
https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$FORM_ID/client-links \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "expiresInDays": 14, "defaultLanguage": "en" }')
CLIENT_URL=$(echo "$LINK_JSON" | jq -r .url)
# 3. Enviar $CLIENT_URL al solicitante por tu canal habitual
# (email, portal seguro, etc.). El solicitante completa el formulario.
# 4. Consultar la finalización. Transiciones de estado:
# not_started → in_progress → completed
curl -s https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$FORM_ID \
-H "Authorization: Bearer $DS160_KEY" \
| jq .status
# → "completed" cuando el solicitante ha terminado
# 5. Descargar el PDF DS-160 desde el panel
# (sin endpoint v1 para esto hoy).
Algunas notas sobre la receta:
- Cadencia de polling: cada 5–15 minutos es suficiente. Con un techo de 60 req/min, una sola clave puede consultar cómodamente miles de formularios en curso.
- Los webhooks aún no están disponibles. Si necesitas notificaciones push de finalización, vigila esta página o contacta a soporte — está en el roadmap.
- El paso 5 (descargar el PDF completado) requiere una sesión del panel hoy. La API v1 intencionalmente no expone las respuestas del solicitante — ver Acceso a PII vía la API.
Versionado y soporte
Estabilidad. /v1 es la superficie estable de la API. Añadimos nuevos campos opcionales, nuevos endpoints y nuevos valores de enum dentro de /v1 sin incrementar la versión mayor, pero no renombramos, eliminamos ni cambiamos el tipo de ningún campo existente. Si alguna vez necesitamos un cambio incompatible, se publicará bajo /v2 y /v1 seguirá funcionando durante al menos 12 meses junto a él.
Adiciones retrocompatibles que deberías planificar:
- Nuevos campos opcionales en las solicitudes aparecerán con el tiempo. Las solicitudes existentes que no los envíen seguirán funcionando.
- Pueden añadirse nuevos campos en las respuestas. Trata los campos desconocidos como ignorables — no falles si aparece un campo futuro.
- Se añadirán nuevos valores de enum para
status,defaultLanguageydisabledSections. No te bloquees con valores desconocidos; regístralos y continúa.
Soporte:
- Límites de tasa más altos, dominios personalizados, acceso anticipado a webhooks y preguntas de integración: contacta a tu gerente de cuenta o support@ds160.io.
- Reportes de errores contra la API v1: misma dirección. Cada respuesta lleva una cabecera
x-request-id— inclúyela en tu reporte para que podamos encontrar la llamada exacta en nuestros logs.