API REST de constancias DC-3
Emite, consulta y anula constancias DC-3 desde tu propio sistema (nómina, LMS, ERP) y recibe avisos firmados por webhook. La constancia es la misma que sale del panel: mismo folio, mismo PDF oficial, misma verificación pública.
$KEY como esa llave.Índice
- Conceptos: base, autenticación, límites
- Códigos de respuesta
- Verificar un folio (público)
- Listar constancias (filtros)
- Emitir una constancia
- Emitir en lote (207)
- Obtener una constancia
- Descargar PDF o DOCX
- Anular una constancia
- Catálogos (claves SIRCE y del DC-3)
- Cumplimiento, empresas, portal, ACE
- Idempotency-Key
- Webhooks: eventos, payloads y firma HMAC
- El objeto constancia
- Compatibilidad y cambios
1. Conceptos
| Base | https://capacitatotal.mx/api.php/v1 — sin rewrite: la ruta va después de api.php. |
|---|---|
| Formato | JSON UTF-8 en cuerpo y respuesta. Manda Content-Type: application/json. Las fechas son AAAA-MM-DD. |
| Autenticación | Authorization: Bearer dc3_… en todas las rutas salvo GET /v1/verificar/{folio}. La llave pertenece a UN capacitador: sólo ve y crea lo suyo. |
| Permisos (scopes) | Una llave nace con constancias:read (todo GET) y constancias:write (POST). Sin el permiso: 403. |
| Límites de tráfico | Por llave: 120 peticiones por minuto (si tu integración necesita más, pídelo por contacto). Sin autenticar, por IP: 100 por minuto; /v1/verificar: 60 por minuto. Al pasarte: 429. |
| Cupo del plan | La constancia emitida por API cuenta igual que la del panel. El plan Gratis incluye 2 constancias de prueba; al agotarlo la emisión responde 403 con el motivo en texto. |
| Errores | Siempre JSON: {"error": "texto para una persona"}. El texto dice qué campo falló y cómo corregirlo; no hay códigos internos que descifrar. |
# Tu primera llamada: lista tu constancia más reciente (no emite nada)
curl -s "https://capacitatotal.mx/api.php/v1/constancias?limit=1" \
-H "Authorization: Bearer $KEY"
2. Códigos de respuesta
| Código | Cuándo |
|---|---|
| 200 | Consulta correcta; anulación hecha. |
| 201 | Constancia emitida; webhook o liga de portal creados. |
| 207 | Lote procesado: cada ítem trae su propio resultado (ok true/false). Uno malo no detiene a los demás. |
| 400 | Dato mal formado: tipo incorrecto, texto más largo que su tope, clave de catálogo inexistente, JSON inválido, Idempotency-Key inválida. |
| 401 | Sin cabecera Authorization, llave desconocida, llave revocada o cuenta suspendida (el texto distingue las tres últimas). |
| 403 | La llave no tiene el permiso; la capacidad no viene en el plan (portal, ACE); cupo del plan agotado; correo de la cuenta sin confirmar; el capacitador de la llave todavía no tiene RFC (se captura una sola vez en el panel, en Mi cuenta; no afecta a las llaves de los demás capacitadores de la cuenta). El texto dice cuál de todos es. |
| 404 | Folio, curso, empresa o catálogo que no existe (o no es tuyo); versión de API desconocida. |
| 405 | Método no soportado en esa ruta. |
| 409 | Ya estaba hecho (constancia ya anulada), carrera al crear, u otra petición en curso con la misma Idempotency-Key. |
| 422 | Falta un campo obligatorio o el valor no es válido para el negocio: CURP inválida, fechas incoherentes, área que no cuadra, Idempotency-Key reutilizada con otro cuerpo. |
| 429 | Límite de tráfico excedido. Espera y reintenta. |
| 500 | Falla nuestra. El cuerpo sigue siendo JSON ({"error":"error interno del servidor"}); el detalle queda en nuestro registro. |
| 503 | Reintenta en unos segundos: otra emisión de la misma cuenta está tomando el cupo, o el servidor no tiene la tabla de idempotencia. No se emitió nada. |
3. Verificar un folio (público)
/v1/verificar/{folio}sin autenticación · 60/min por IPLo mismo que ve cualquiera al escanear el QR, legible por máquina: autenticidad (integridad del contenido + firma RSA), estado, y los datos impresos con la CURP enmascarada. Acepta el folio actual, el folio del sistema anterior o el código de verificación. Nunca devuelve el RFC ni el código secreto.
curl -s "https://capacitatotal.mx/api.php/v1/verificar/DC3-3KSF-BE3H-JPZF0" HTTP/1.1 200 OK
{
"folio": "DC3-3KSF-BE3H-JPZF0",
"autentico": false,
"estado": "anulada",
"reemplazada": true,
"sustituida_por": "DC3-JA9Z-5N7G-GYZ61",
"sustituye_a": null,
"reemplazo_vigente": "DC3-JA9Z-5N7G-GYZ61",
"integridad_ok": true,
"firmada": true,
"firma_valida": true,
"trabajador": "LOPEZ MARTINEZ MARIA",
"curp_mascara": "LOMA**********RR06",
"curso": "NOM-009-STPS-2011 Trabajos en altura",
"duracion_horas": 8,
"fec_inicio": "2026-08-24",
"fec_termino": "2026-08-25",
"fecha_emision": "2026-09-02",
"empresa": "CONSTRUCTORA DEL NORTE SA DE CV",
"capacitador": "CAPACITACION DE PRUEBA SC",
"registro_ace": "CAPD-850101-ABC-0013",
"verificado_at": "2026-09-02T15:58:32-06:00"
} autentico es true sólo si la constancia está activa, su contenido no fue alterado y la firma es válida. Una constancia anulada conserva integridad y firma válidas (el papel es auténtico) pero autentico es false; si fue corregida, sustituida_por trae el folio de la versión vigente. Folio inexistente: 404 {"error":"no encontrada"}.
4. Listar constancias
/v1/constanciasconstancias:read| Parámetro | Filtra por |
|---|---|
curp | CURP del trabajador (18 caracteres). Sólo forma; sirve incluso para localizar una CURP mal capturada y anularla. |
curso_id | Id del curso (el que devuelve la emisión). |
empresa_rfc | RFC de la empresa. |
estado | activa o anulada. |
desde, hasta | Fecha de emisión, inclusivas, AAAA-MM-DD. Una fecha imposible (2026-13-99) responde 422, no una lista vacía. |
limit, offset | Paginación: limit 1-200 (50 por omisión). total es el universo completo del filtro, no la página. |
curl -s "https://capacitatotal.mx/api.php/v1/constancias?curp=PEGJ900515HDFRRN09" \
-H "Authorization: Bearer $KEY" HTTP/1.1 200 OK
{
"total": 1, "count": 1, "limit": 50, "offset": 0,
"constancias": [
{
"folio": "DC3-JA9Z-5N7G-GYZ61",
"fecha_emision": "2026-09-02",
"estado": "activa",
"empresa_rfc": "CNO050101AB2",
"empresa": "CONSTRUCTORA DEL NORTE SA DE CV",
"links": {
"pdf": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/pdf",
"docx": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/docx",
"verificar": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61",
"verificar_json": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61&format=json"
},
"curp": "PEGJ900515HDFRRN09",
"trabajador": "PEREZ GARCIA JUAN",
"curso_id": 4923,
"curso": "NOM-009-STPS-2011 Trabajos en altura",
"fec_inicio": "2026-08-24",
"fec_termino": "2026-08-25",
"duracion_horas": 8
}
]
} Otros ejemplos que funcionan: ?curso_id=4923&limit=1, ?desde=2026-09-01&hasta=2026-09-30, ?estado=anulada. Se pueden combinar.
5. Emitir una constancia
/v1/constanciasconstancias:write · acepta Idempotency-KeyUna llamada hace todo: da de alta (o reutiliza) la empresa por RFC, el curso por nombre o por curso_id, y al trabajador por CURP dentro de esa empresa; después emite, firma y numera la constancia. Los datos que ya existen no se sobrescriben con vacíos.
| Campo | Obligatorio | Notas |
|---|---|---|
empresa.rfc | sí | Llave de la empresa dentro de tu cuenta. Si el RFC no tiene la forma del SAT, la constancia se emite igual y se te avisa en avisos. |
empresa.razon_social | si la empresa es nueva | Hasta 250 caracteres. Opcionales: representante_legal, representante_trabajadores (200). |
curso_id o curso.nombre | sí (uno de los dos) | Con curso.nombre nuevo hace falta curso.duracion_horas (1-9999). curso.area_clave: clave de areas_tematicas; sin ella la casilla sale en blanco y se avisa. |
trabajador.curp | sí | Se valida el dígito verificador: una CURP mal capturada es 422. |
trabajador.nombre, primer_apellido | sí | segundo_apellido y puesto opcionales (120 / 200 caracteres). |
trabajador.ocupacion_area | recomendado | Código del reverso del DC-3, por ejemplo "04.6" (catálogo ocupacion_areas). También acepta el nombre exacto. Si código y nombre no cuadran entre sí, 422: no elegimos por ti. |
trabajador.ocupacion_clave | opcional | Ocupación específica del CNO (clave de ocupaciones). Si viene, es la que se imprime. |
trabajador.sirce.* | opcional | Datos que el reporte SIRCE pide y el DC-3 no imprime: escolaridad_clave, documento_probatorio_clave, institucion_clave, estado_clave + municipio_clave (van juntos). Claves en catálogos. |
fec_inicio, fec_termino | sí | AAAA-MM-DD; inicio no puede ser posterior a término. |
duracion_horas | opcional | Si no viene, se usa la del curso. |
notificar.email | opcional | Correo al que se envía la constancia (si el correo saliente está configurado). Se valida antes de emitir. |
curl -s -X POST "https://capacitatotal.mx/api.php/v1/constancias" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rh-2026-0001" \
-d '{
"empresa": {
"rfc": "CNO050101AB2",
"razon_social": "CONSTRUCTORA DEL NORTE SA DE CV",
"representante_legal": "MARIA LOPEZ RUIZ"
},
"curso": {"nombre": "NOM-009-STPS-2011 Trabajos en altura", "duracion_horas": 8, "area_clave": 1},
"trabajador": {
"curp": "PEGJ900515HDFRRN09",
"nombre": "JUAN", "primer_apellido": "PEREZ", "segundo_apellido": "GARCIA",
"puesto": "OPERADOR",
"ocupacion_area": "04.6",
"sirce": {"escolaridad_clave": 3, "documento_probatorio_clave": 2, "institucion_clave": 1,
"estado_clave": 9, "municipio_clave": 5}
},
"fec_inicio": "2026-08-24",
"fec_termino": "2026-08-25",
"notificar": {"email": "juan.perez@example.com"}
}' HTTP/1.1 201 Created
{
"folio": "DC3-JA9Z-5N7G-GYZ61",
"codigo_verificacion": "22ORpeF5WTsmpYhVyt06OF0Q",
"empresa_id": 4753,
"curso_id": 4923,
"fecha_emision": "2026-09-02",
"reactivado": false,
"links": {
"pdf": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/pdf",
"docx": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/docx",
"verificar": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61",
"verificar_json": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61&format=json"
},
"avisos": [
"La empresa no tiene capturado su número de trabajadores, así que no podemos decirte si le corresponde la firma del representante de los trabajadores: el formato la pide sólo por encima de 50."
]
} avisos. La constancia ya está emitida y firmada: es inmutable. Los avisos dicen qué casilla del DC-3 salió en blanco o qué dato conviene corregir en la ficha para las próximas; no se rechaza la emisión para no romper integraciones en marcha. Guarda codigo_verificacion como secreto: es lo único que no es público.reactivado: la persona volvió al padrón. Emitirle una DC-3 a alguien es declarar que sigue en la empresa, así que si su ficha estaba dada de baja, esta emisión la devuelve a activa —igual que por el panel y que por la importación— y te lo decimos: reactivado vale true y la frase entra también en avisos. Normalmente vale false. Si te sale true sin esperarlo, revisa la CURP que mandaste: puede que sea la de otra persona. Y si alguna vez no pudiéramos reactivar la ficha, la constancia no se pierde: la respuesta sigue siendo 201 con reactivado en false y un aviso que te dice sólo lo que sabemos de esa ficha — que seguía dada de baja y hay que reactivarla desde Personas, o que estaba activa y no había baja que deshacer, o que no pudimos comprobarla. Nunca te mandamos a corregir una baja que no existe. No repitas la petición: la constancia ya está emitida.Segunda emisión reutilizando el curso (curso_id) y la empresa ya dadas de alta:
curl -s -X POST "https://capacitatotal.mx/api.php/v1/constancias" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"empresa":{"rfc":"CNO050101AB2"},"curso_id":4923,
"trabajador":{"curp":"LOMA880202MDFPRR06","nombre":"MARIA","primer_apellido":"LOPEZ","segundo_apellido":"MARTINEZ","ocupacion_area":"04.6"},
"fec_inicio":"2026-08-24","fec_termino":"2026-08-25"}' Errores típicos, tal como salen:
422 {"error":"falta el campo 'trabajador'"}
422 {"error":"CURP inválida: XXXX000000XXXXXX00"}
422 {"error":"trabajador.ocupacion_area: '01.3 GANADERIA' no cuadra. El código 01.3 es «Pesca y acuacultura», no lo que dice el texto, y no elegimos por ti cuál de los dos querías: corrige uno y vuelve a mandarlo. (El sistema anterior de muchos despachos traía la ganadería con el código de la pesca.)"}
400 {"error":"trabajador.sirce: no existe el municipio 999 en el estado 9 (consulta GET /v1/catalogos/municipios?estado=9)"}
400 {"error":"cuerpo JSON inválido"}
6. Emitir en lote
/v1/constancias/loteconstancias:write · acepta Idempotency-KeyCuerpo {"constancias": [ …hasta 50 objetos iguales al de emitir… ]}. Cada ítem se procesa por separado con las mismas reglas y el mismo cupo; la respuesta es siempre 207 con un resultado por índice. Un ítem con error no impide los demás.
curl -s -X POST "https://capacitatotal.mx/api.php/v1/constancias/lote" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: lote-2026-0001" \
-d '{"constancias":[
{"empresa":{"rfc":"CNO050101AB2"},"curso_id":4923,"trabajador":{"curp":"LOMA880202MDFPRR06","nombre":"MARIA","primer_apellido":"LOPEZ"},"fec_inicio":"2026-08-27","fec_termino":"2026-08-27"},
{"empresa":{"rfc":"CNO050101AB2"},"curso_id":4923,"trabajador":{"curp":"XXXX000000XXXXXX00","nombre":"ERROR","primer_apellido":"CURP"},"fec_inicio":"2026-08-27","fec_termino":"2026-08-27"}
]}' HTTP/1.1 207 Multi-Status
{
"total": 2, "creadas": 1, "fallidas": 1,
"resultados": [
{"indice": 0, "ok": true, "folio": "DC3-1WXA-QKKF-NMNQG", "codigo_verificacion": "kr5Yoo6zAVmZkPvIv0e7VTmX",
"empresa_id": 4753, "curso_id": 4923, "fecha_emision": "2026-09-02", "reactivado": false,
"links": { "pdf": "…", "docx": "…", "verificar": "…", "verificar_json": "…" },
"avisos": ["La empresa no tiene capturado su número de trabajadores, …"]},
{"indice": 1, "ok": false, "error": "CURP inválida: XXXX000000XXXXXX00", "status": 422}
]
} Con Idempotency-Key, repetir un lote que creó al menos una constancia devuelve el mismo 207 (los ítems fallidos siguen fallidos: corrígelos y mándalos en un lote nuevo con otra llave). Un 207 que no creó ninguna (por ejemplo, todas rechazadas por cupo) no se guarda: el reintento con la misma llave vuelve a procesar, para que pagar y reintentar funcione.
7. Obtener una constancia
/v1/constancias/{folio}constancias:readDevuelve el objeto constancia: lo impreso (sale del contenido firmado, no de las fichas), ids, estado, motivo de anulación y enlaces.
curl -s "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61" -H "Authorization: Bearer $KEY" HTTP/1.1 200 OK
{
"folio": "DC3-JA9Z-5N7G-GYZ61",
"estado": "activa",
"fecha_emision": "2026-09-02",
"periodo": {"inicio": "2026-08-24", "fin": "2026-08-25"},
"duracion_horas": 8,
"trabajador": "PEREZ GARCIA JUAN",
"empresa": "CONSTRUCTORA DEL NORTE SA DE CV",
"curso": "NOM-009-STPS-2011 Trabajos en altura",
"links": {
"pdf": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/pdf",
"docx": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/docx",
"verificar": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61",
"verificar_json": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61&format=json"
},
"curp": "PEGJ900515HDFRRN09",
"puesto": "OPERADOR",
"ocupacion": "04.6 Procesos industriales",
"area_tematica": "Producción",
"empresa_id": 4753,
"empresa_rfc": "CNO050101AB2",
"empresa_razon_social": "CONSTRUCTORA DEL NORTE SA DE CV",
"curso_id": 4923,
"capacitador_id": 2661,
"verify_url": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61",
"pdf_url": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/pdf",
"anulada_motivo": null,
"sustituida_por": null
}
8. Descargar el PDF o el DOCX
/v1/constancias/{folio}/pdfconstancias:read/v1/constancias/{folio}/docxconstancias:readEl documento se genera al momento a partir del contenido firmado (por eso no hay HEAD). Respuesta binaria con Content-Disposition: attachment y el nombre DC3_<folio>.pdf. Folio ajeno o inexistente: 404.
curl -s "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/pdf" \
-H "Authorization: Bearer $KEY" -o DC3_DC3-JA9Z-5N7G-GYZ61.pdf
9. Anular una constancia
/v1/constancias/{folio}/anularconstancias:writeEl DC-3 no vence; la única forma de retirar una constancia con un error es anularla. La página pública pasa a decir «anulada» (o «reemplazada por…» si mandas sustituida_por), su contenido firmado no se toca y dispara el webhook constancia.anulada (hoy sólo cuando se anula por la API; el botón «Anular» del panel todavía no lo dispara).
| Campo | Notas |
|---|---|
motivo | Obligatorio, hasta 255 caracteres. Queda registrado y viaja en el webhook. |
sustituida_por | Opcional: folio de la constancia nueva (ya emitida, activa, tuya) que corrige a ésta. Con él, la verificación pública muestra la corrección como corrección y no como una anulación seca. |
curl -s -X POST "https://capacitatotal.mx/api.php/v1/constancias/DC3-3KSF-BE3H-JPZF0/anular" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"motivo":"Error de captura en el segundo apellido","sustituida_por":"DC3-JA9Z-5N7G-GYZ61"}' HTTP/1.1 200 OK
{
"folio": "DC3-3KSF-BE3H-JPZF0",
"estado": "anulada",
… el objeto constancia completo …
"anulada_motivo": "Error de captura en el segundo apellido",
"sustituida_por": "DC3-JA9Z-5N7G-GYZ61"
}
# repetirla:
HTTP/1.1 409 Conflict
{"error":"la constancia ya estaba anulada (motivo registrado: Error de captura en el segundo apellido); la sustituye DC3-JA9Z-5N7G-GYZ61"}
# sin motivo:
HTTP/1.1 422
{"error":"falta el campo 'motivo'"} 10. Catálogos
/v1/catalogosconstancias:read/v1/catalogos/{nombre}constancias:readLos catálogos oficiales (STPS, INEGI, CNO) con los que se llenan las claves de la emisión. Son iguales para todas las cuentas. El índice dice, para cada uno, qué campo del cuerpo se llena con qué columna.
| Catálogo | Devuelve | Se usa en |
|---|---|---|
escolaridad | clave, nombre | trabajador.sirce.escolaridad_clave |
documento_probatorio | clave, nombre | trabajador.sirce.documento_probatorio_clave |
institucion | clave, nombre | trabajador.sirce.institucion_clave |
estados | clave, nombre | trabajador.sirce.estado_clave |
municipios?estado=9 | estado_clave, clave, nombre | trabajador.sirce.municipio_clave (con su estado) |
areas_tematicas | clave, codigo, nombre | curso.area_clave = clave |
ocupacion_areas | codigo, nombre, division | trabajador.ocupacion_area = codigo («04.6») |
ocupaciones?q=soldador | clave, codigo, nombre (hasta 50) | trabajador.ocupacion_clave = clave |
curl -s "https://capacitatotal.mx/api.php/v1/catalogos/municipios?estado=9" -H "Authorization: Bearer $KEY" HTTP/1.1 200 OK
{
"catalogo": "municipios",
"uso": "trabajador.sirce.municipio_clave = clave (junto con estado_clave); requiere ?estado=",
"total": 16,
"items": [
{"estado_clave": 9, "clave": 1, "nombre": "ALVARO OBREGON"},
{"estado_clave": 9, "clave": 2, "nombre": "AZCAPOTZALCO"},
…
]
}
# sin estado: 422 {"error":"El catálogo 'municipios' requiere ?estado=<clave del estado> (ver /v1/catalogos/estados)."}
# nombre desconocido: 404 {"error":"catálogo desconocido: 'nada'. Los que existen: escolaridad, documento_probatorio, institucion, estados, municipios, areas_tematicas, ocupacion_areas, ocupaciones"} escolaridad:
1 Ninguno · 2 Primaria · 3 Secundaria · 4 Bachillerato · 5 Carrera técnica · 6 Licenciatura · 7 Especialidad · 8 Maestría · 9 Doctorado
documento_probatorio:
1 Título · 2 Certificado · 3 Diploma · 4 Otro · 5 Ninguno
institucion:
1 Pública · 2 Privada · 3 Ninguna
11. Cumplimiento, empresas, portal y ACE
/v1/cumplimientoconstancias:readResumen de cumplimiento de todas tus empresas: cobertura de plantilla, constancias por año, situación del SIRCE del año cerrado y nivel de riesgo.
HTTP/1.1 200 OK
{
"empresas": 1, "score_promedio": 100, "en_riesgo": 0,
"detalle": [
{"empresa_id": 4753, "empresa": "CONSTRUCTORA DEL NORTE SA DE CV", "plantilla": 2, "con_constancia": 2, "cobertura_pct": 100,
"constancias_total": 2, "constancias_por_anio": {"2026": 2},
"sirce": {"anio_cerrado": 2025, "constancias": 0, "fecha_limite": "2026-03-25", "dias_para_limite": -161, "dias_habiles": 60},
"score_cumplimiento": 100, "riesgo_inspeccion": "bajo", "alertas": []}
]
} /v1/empresas/{id}/cumplimientoconstancias:readEl mismo bloque, para una empresa (el detalle[] de arriba sin el envoltorio). {id} es empresa_id; una empresa que no es tuya responde 404.
/v1/empresas/{id}/panelconstancias:readLo que ve el portal de esa empresa: cumplimiento, por_nom (trabajadores y cursos por NOM), recapacitacion, recapacitacion_resumen y recapacitacion_config.
recapacitacion_resumen dice a cuántas PERSONAS dejó fuera la lista de recapacitacion por estar dadas de baja en la empresa (bajas) y con qué frase decirlo junto al número (bajas_texto, vacía cuando no hay ninguna). A quien ya no trabaja ahí no «le toca recapacitación», así que la lista lo excluye; sin este bloque una lista más corta —o vacía— no traía con qué explicarse.
HTTP/1.1 200 OK
{
"cumplimiento": { … },
"por_nom": [{"nom": "NOM-009", "trabajadores": 2, "cursos": ["NOM-009-STPS-2011 Trabajos en altura"]}],
"recapacitacion": [],
"recapacitacion_resumen": {"bajas": 1, "bajas_texto": "1 persona dada de baja no se cuenta"},
"recapacitacion_config": {"cursos": 1, "con_periodo": 1}
} /v1/empresas/{id}/portal-linkconstancias:write · plan con portalGenera una liga del portal de empresa (donde tu cliente descarga y verifica sus constancias). Si tu plan no incluye el portal: 403 {"error":"Portal de empresas no viene en tu plan."}.
curl -s -X POST "https://capacitatotal.mx/api.php/v1/empresas/4753/portal-link" -H "Authorization: Bearer $KEY"
HTTP/1.1 201 Created
{"portal_url": "https://capacitatotal.mx/portal.php?t=viDWGGMEifwtTNToTqB6ba9LjCwZcTumdzGy6HuP29Q"} /v1/ace/{registro}constancias:read · plan con ACESituación de un registro de agente capacitador externo según la copia local del padrón de la STPS (nunca consulta en vivo a la STPS). Si el registro no se ha cotejado todavía, lo dice.
HTTP/1.1 200 OK
{"rfc": "CAPD850101ABC0013", "estado": "sin_padron", "activo": false,
"mensaje": "Todavía no has comprobado este registro contra el padrón de la STPS. Puedes hacerlo desde «Mi registro ACE»."}
12. Idempotency-Key
Emitir no es reversible: se numera, se firma y descuenta cupo. Si tu sistema reintenta una petición que sí llegó (timeout de red, proceso reiniciado), saldrían dos constancias iguales. Para evitarlo, manda en POST /v1/constancias y POST /v1/constancias/lote la cabecera:
Idempotency-Key: rh-2026-0001 # letras, dígitos y . _ : - (hasta 128); una por operación tuya | Situación | Respuesta |
|---|---|
| Primera vez | Se procesa normal. Si emitió (201 o 207), la respuesta queda guardada 24 horas bajo esa llave y tu API key. |
| Misma llave, mismo cuerpo (byte a byte), dentro de 24 h | La respuesta original, con la cabecera Idempotent-Replayed: true. No se emite nada. |
| Misma llave, otro cuerpo u otra ruta | 422. Una llave identifica una operación. |
| Misma llave mientras la primera sigue en curso | 409: espera su respuesta. |
| La primera falló (4xx/5xx) | No se guarda: el reintento con la misma llave vuelve a intentarlo. |
| Llave mal formada | 400 antes de tocar nada. |
# segunda vez, mismo cuerpo:
HTTP/1.1 201 Created
Idempotent-Replayed: true
{"folio":"DC3-JA9Z-5N7G-GYZ61", …exactamente la misma respuesta… }
# misma llave, otro cuerpo:
HTTP/1.1 422
{"error":"Esta Idempotency-Key ya se usó con otro cuerpo (o en otra ruta). Una llave identifica UNA operación: usa una llave nueva para una petición distinta. No se creó nada."}
13. Webhooks
/v1/webhooksconstancias:writeRegistra una URL https pública (puerto 443 u 8443; nada de http, localhost ni IP privadas) y recibirás un POST firmado por cada evento al que te suscribas. También se registran desde Panel › Integraciones, donde además hay un botón «Probar».
curl -s -X POST "https://capacitatotal.mx/api.php/v1/webhooks" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"url":"https://example.com/webhooks/rh","eventos":["constancia.creada","constancia.anulada"]}'
HTTP/1.1 201 Created
{
"id": 100,
"secret": "whsec_TjDpagKHdLMREaKQWBwc-ocSeXGFK994",
"eventos": ["constancia.creada", "constancia.anulada"],
"nota": "Guarda el secret (se muestra una vez). Firma: X-DC3-Signature = v1=HMAC_SHA256(secret, X-DC3-Delivery + \".\" + X-DC3-Timestamp + \".\" + cuerpo). Cómo verificarla: https://capacitatotal.mx/api-docs#webhooks"
}
# evento inexistente: 400 {"error":"'eventos' sólo acepta: constancia.creada, constancia.anulada, lote.completado"}
# http o red interna: 422 {"error":"La URL del webhook no es válida. Debe ser una URL https pública."} Sin eventos te suscribes a los tres. El secret se muestra una sola vez.
Eventos
| Evento | Cuándo se dispara |
|---|---|
constancia.creada | Al emitir una constancia por la API (individual o en lote). Las emitidas desde el panel hoy no disparan este evento. |
constancia.anulada | Al anular por POST /v1/constancias/{folio}/anular. Trae motivo y, cuando aplique, sustituida_por. |
lote.completado | Cuando termina de generarse el ZIP de un lote del panel (importación de Excel o grupo). Trae lote_id, total y fallidas. |
webhook.test | El botón «Probar» del panel. Mismas cabeceras y misma firma; cuerpo {"event":"webhook.test","mensaje":"…","endpoint_id":N}. |
constancia.creada y lote.completado. Para recibir constancia.anulada, registra un endpoint nuevo con los tres eventos.Cabeceras de cada entrega
POST /webhooks/rh HTTP/1.1
Content-Type: application/json
User-Agent: CapacitaTotal-DC3-Webhook/1
X-DC3-Event: constancia.creada
X-DC3-Delivery: 531
X-DC3-Timestamp: 1788386135
X-DC3-Signature: v1=881d58dddfb440b0b9adcecb8384eff51d5adddd8ee931acf83107e417782629 Responde 2xx en menos de 10 segundos (el cuerpo de tu respuesta se descarta). Cualquier otra cosa se reintenta: hasta 8 intentos con espera creciente (2, 4, 8… minutos, tope 6 horas). Tras 20 fallos seguidos el endpoint se desactiva; registra uno nuevo cuando lo hayas arreglado. X-DC3-Delivery es el id de la entrega: si recibes la misma dos veces (reintento tras un 2xx que no nos llegó), descártala por ese id.
Payload de constancia.creada
Las claves evento, folio, occurred_at y nombre (nombre de pila del trabajador) son las de siempre. Todo lo demás es el objeto constancia completo, añadido en septiembre de 2026: una integración que sólo lee folio y nombre sigue funcionando sin cambios.
{
"evento": "constancia.creada",
"folio": "DC3-JA9Z-5N7G-GYZ61",
"occurred_at": "2026-09-02T15:58:11-06:00",
"nombre": "JUAN",
"estado": "activa",
"fecha_emision": "2026-09-02",
"periodo": {"inicio": "2026-08-24", "fin": "2026-08-25"},
"duracion_horas": 8,
"trabajador": "PEREZ GARCIA JUAN",
"empresa": "CONSTRUCTORA DEL NORTE SA DE CV",
"curso": "NOM-009-STPS-2011 Trabajos en altura",
"links": {
"pdf": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/pdf",
"docx": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/docx",
"verificar": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61",
"verificar_json": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61&format=json"
},
"curp": "PEGJ900515HDFRRN09",
"puesto": "OPERADOR",
"ocupacion": "04.6 Procesos industriales",
"area_tematica": "Producción",
"empresa_id": 4753,
"empresa_rfc": "CNO050101AB2",
"empresa_razon_social": "CONSTRUCTORA DEL NORTE SA DE CV",
"curso_id": 4923,
"capacitador_id": 2661,
"verify_url": "https://capacitatotal.mx/verifica.php?f=DC3-JA9Z-5N7G-GYZ61",
"pdf_url": "https://capacitatotal.mx/api.php/v1/constancias/DC3-JA9Z-5N7G-GYZ61/pdf",
"anulada_motivo": null,
"sustituida_por": null
} Payload de constancia.anulada
{
"evento": "constancia.anulada",
"folio": "DC3-3KSF-BE3H-JPZF0",
"occurred_at": "2026-09-02T15:58:27-06:00",
"motivo": "Error de captura en el segundo apellido",
"sustituida_por": "DC3-JA9Z-5N7G-GYZ61",
"estado": "anulada",
"fecha_emision": "2026-09-02",
"periodo": {"inicio": "2026-08-24", "fin": "2026-08-25"},
"duracion_horas": 8,
"trabajador": "LOPEZ MARTINEZ MARIA",
"empresa": "CONSTRUCTORA DEL NORTE SA DE CV",
"curso": "NOM-009-STPS-2011 Trabajos en altura",
"links": { "pdf": "…", "docx": "…", "verificar": "…", "verificar_json": "…" },
"curp": "LOMA880202MDFPRR06",
"puesto": null,
"ocupacion": "04.6 Procesos industriales",
"area_tematica": "Producción",
"empresa_id": 4753,
"empresa_rfc": "CNO050101AB2",
"empresa_razon_social": "CONSTRUCTORA DEL NORTE SA DE CV",
"curso_id": 4923,
"capacitador_id": 2661,
"verify_url": "https://capacitatotal.mx/verifica.php?f=DC3-3KSF-BE3H-JPZF0",
"pdf_url": "https://capacitatotal.mx/api.php/v1/constancias/DC3-3KSF-BE3H-JPZF0/pdf",
"anulada_motivo": "Error de captura en el segundo apellido"
} Payload de lote.completado
{"evento": "lote.completado", "lote_id": 812, "occurred_at": "2026-09-02T16:10:03-06:00", "total": 48, "fallidas": 0}
Verificar la firma
La firma es HMAC-SHA256 con tu secret sobre la cadena X-DC3-Delivery + "." + X-DC3-Timestamp + "." + cuerpo, en hexadecimal y con el prefijo v1=. Usa el cuerpo crudo (los bytes tal cual llegaron, no el JSON re-serializado), compara en tiempo constante y rechaza timestamps de más de 5 minutos. Los tres fragmentos siguientes se probaron contra entregas reales.
$secret = getenv('DC3_WEBHOOK_SECRET'); // el whsec_… que te dio el registro
$body = file_get_contents('php://input');
$delivery = $_SERVER['HTTP_X_DC3_DELIVERY'] ?? '';
$timestamp = $_SERVER['HTTP_X_DC3_TIMESTAMP'] ?? '';
$firma = $_SERVER['HTTP_X_DC3_SIGNATURE'] ?? ''; // "v1=<hex>"
$esperada = 'v1=' . hash_hmac('sha256', $delivery . '.' . $timestamp . '.' . $body, $secret);
$valida = hash_equals($esperada, $firma) && abs(time() - (int) $timestamp) < 300;
if (!$valida) { http_response_code(401); exit; }
$evento = json_decode($body, true); // $evento['evento'], $evento['folio'], …
http_response_code(200); import hashlib, hmac, os, time
def webhook_valido(headers: dict, body_bytes: bytes) -> bool:
secret = os.environ["DC3_WEBHOOK_SECRET"]
delivery = headers["X-DC3-Delivery"]
timestamp = headers["X-DC3-Timestamp"]
firma = headers["X-DC3-Signature"] # "v1=<hex>"
mensaje = f"{delivery}.{timestamp}.".encode("utf-8") + body_bytes
esperada = "v1=" + hmac.new(secret.encode("utf-8"), mensaje, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, firma) and abs(time.time() - int(timestamp)) < 300
# Flask: webhook_valido(request.headers, request.get_data()) # get_data() = cuerpo crudo const crypto = require('crypto');
function webhookValido(headers, rawBody /* Buffer con el cuerpo crudo */) {
const secret = process.env.DC3_WEBHOOK_SECRET;
const delivery = headers['x-dc3-delivery'];
const timestamp = headers['x-dc3-timestamp'];
const firma = headers['x-dc3-signature']; // "v1=<hex>"
const mensaje = Buffer.concat([Buffer.from(`${delivery}.${timestamp}.`, 'utf8'), rawBody]);
const esperada = 'v1=' + crypto.createHmac('sha256', secret).update(mensaje).digest('hex');
return esperada.length === firma.length
&& crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(firma))
&& Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
}
// Express: app.post('/webhooks/dc3', express.raw({ type: 'application/json' }), (req, res) => {
// if (!webhookValido(req.headers, req.body)) return res.sendStatus(401);
// const evento = JSON.parse(req.body.toString('utf8'));
// res.sendStatus(200);
// });
14. El objeto constancia
Es la misma forma en GET /v1/constancias/{folio}, en la respuesta de anular y dentro de los webhooks constancia.creada y constancia.anulada, para que escribas un solo lector. Lo impreso sale del contenido firmado al emitir (lo que dice el papel), no de las fichas vivas.
| Clave | Contenido |
|---|---|
folio | Folio público, impreso en el documento y en el QR. |
estado | activa o anulada. |
fecha_emision, periodo.inicio, periodo.fin, duracion_horas | Fechas AAAA-MM-DD y horas del curso. |
trabajador | Texto, en el orden del formato: apellido paterno, materno, nombre(s). |
empresa, curso | Texto: razón social y nombre del curso tal como se imprimieron. |
links.pdf, links.docx | Descargas (con tu API key). |
links.verificar, verify_url | La página pública de verificación: la misma URL que lleva el QR. |
links.verificar_json | La misma verificación, en JSON y sin autenticación. |
curp, puesto, ocupacion, area_tematica | Lo impreso en esas casillas (null si quedó en blanco). |
empresa_id, empresa_rfc, empresa_razon_social, curso_id, capacitador_id | Ids para tus cruces y el RFC del patrón. |
pdf_url | Igual que links.pdf. |
anulada_motivo | Texto del motivo cuando está anulada; si no, null. |
sustituida_por | Folio de la constancia que la corrige, cuando aplique; si no, null. |
La respuesta de emitir es más corta a propósito (folio, código de verificación, ids y enlaces): lo que hace falta para archivar. Si necesitas el objeto completo tras emitir, pídelo con GET /v1/constancias/{folio} o recíbelo por webhook.
15. Compatibilidad y cambios
La API es un contrato: nunca se quita ni se renombra una clave existente; lo nuevo se añade. Tu lector debe ignorar las claves que no conozca.
| Fecha | Cambio (todo aditivo) |
|---|---|
| 2026-09 | Webhook constancia.creada con el objeto constancia completo (antes: solo folio y nombre). Evento nuevo constancia.anulada. Ruta POST /v1/constancias/{folio}/anular. Filtros curp, curso_id, estado en el listado y columnas nuevas por fila. Bloque trabajador.sirce en la emisión. Cabecera Idempotency-Key. Rutas /v1/catalogos. links.verificar_json. Claves nuevas en GET /v1/constancias/{folio}. La respuesta de POST /v1/webhooks añade eventos y corrige el texto de la nota (la firma siempre incluyó el id de entrega). Motivo nuevo de 403 al emitir: perfil del capacitador sin completar (mismo código, texto propio). reactivado en la respuesta de emitir (y en cada ítem del lote): la emisión devuelve a activa a una persona dada de baja, como ya hacían el panel y la importación. Bloque recapacitacion_resumen en GET /v1/empresas/{id}/panel. |
| 2026-08 | trabajador.ocupacion_area en la emisión; avisos en la respuesta de emitir. |
¿Falta algo para tu integración? Escríbenos desde contacto: la API crece con lo que los integradores necesitan.
Preguntas frecuentes
¿Necesito ser programador para usar la API?
Sí, o tener a alguien que lo sea. La API es para conectar otro software (nómina, LMS, ERP, un sistema de RH) con la plataforma. Si emites constancias desde el panel, no necesitas nada de esto.
¿Qué pasa si mi sistema reintenta una emisión por un fallo de red?
Manda la cabecera Idempotency-Key con un identificador tuyo por operación. La misma llave con el mismo cuerpo, dentro de 24 horas, devuelve la respuesta original (mismo folio) y no emite una segunda constancia.
¿Los webhooks llegan a una URL http o a una IP interna?
No. Sólo se entregan a URL https públicas (puertos 443 u 8443). Cada envío va firmado con HMAC-SHA256 y trae un identificador de entrega para que puedas descartar repeticiones.
¿La constancia emitida por API es igual a la del panel?
Es la misma constancia: mismo folio, mismo PDF oficial, misma firma verificable y misma página pública de verificación. Cuenta igual para el cupo del plan.
¿Puedo probar sin pagar?
Sí. Una cuenta del plan Gratis puede generar su API key y usar todas las rutas; el cupo de constancias del plan aplica igual por API que por el panel.
Conecta tu sistema hoy
Crea tu cuenta, genera tu API key en Integraciones y emite tu primera DC-3 desde tu propio software.
Empieza gratis