Integra afiliaciones de salud paso a paso
Esta guia explica como autenticar tu sistema, consultar los catalogos, enviar una afiliacion de salud con documentos PDF en base64, identificarla con tu propio ID, consultar su estado y descargar el formulario soporte.
Inicio rapido
La integracion completa se puede resumir en seis pasos. No escribas nombres libres cuando exista un catalogo: envia siempre el id entregado por la API.
Entra a tu usuario y abre Configuracion > Integracion API.
Consulta GET /me con las dos cabeceras.
Obtiene IDs de EPS, IPS, documentos y ubicaciones.
Convierte cada PDF de una pagina a base64.
Usa POST /affiliation-requests.
Consulta por tu external_id y descarga el PDF.
Sandbox de pruebas
Antes de tocar produccion, integra contra el sandbox. Tiene las mismas rutas, los mismos campos y las mismas respuestas: cuando termines, cambias la URL base y las credenciales y ya estas en produccion.
Lo que envias al sandbox no llega a ninguna EPS, no lo procesa ningun robot, no le aparece a nadie en el portal y se borra solo a los 8 dias. Aun asi, no mandes datos de personas reales: son pruebas, no un archivo.
URL base del sandbox
https://acicolombia.com.co/api/external/sandbox/v1Credenciales publicas
Estas credenciales son compartidas y estan aqui a proposito: no tienes que pedirle una cuenta a nadie para empezar.
X-ACI-API-Key: aci_test_S4XKFEANS5PzgCtoflEgKUhrj81QEEnLOAmWYRphUwb6eMV3oW1r5uklLE5EwgYN
X-ACI-Secret-Key: aci_test_secret_Rvv2uXgqZeLJeAJOfPZwNncrWEeSiuo47Rg6qgVQIsTvXXkyadVhdv80LEpKl49pFlJJgbKZ
Las credenciales del sandbox solo funcionan en /api/external/sandbox/v1 y las de produccion solo en /api/external/v1. Si las cruzas recibes 403. Es deliberado: evita que una prueba termine radicando de verdad.
Probar el acceso
curl --request GET "https://acicolombia.com.co/api/external/sandbox/v1/me" \
--header "X-ACI-API-Key: aci_test_S4XKFEANS5PzgCtoflEgKUhrj81QEEnLOAmWYRphUwb6eMV3oW1r5uklLE5EwgYN" \
--header "X-ACI-Secret-Key: aci_test_secret_Rvv2uXgq..." \
--header "Accept: application/json"
{
"data": {
"id": 966,
"name": "SANDBOX API",
"environment": "sandbox"
}
}
Provocar el desenlace
En produccion el resultado lo da la EPS y tarda. En el sandbox lo provocas tu, para poder probar tu webhook sin esperar.
https://acicolombia.com.co/api/external/sandbox/v1/affiliation-requests/{external_id}/simulate{ "status": "RADICADA" }
{
"status": "RECHAZADA",
"rejection_reason": "FALTAN_DOCUMENTOS",
"rejection_notes": "Falta el documento de identidad."
}
El cambio dispara tu webhook por el mismo camino que en produccion, asi que lo que recibas aqui es exactamente lo que vas a recibir alla.
Empezar de cero
https://acicolombia.com.co/api/external/sandbox/v1/resetBorra todas tus solicitudes de prueba de una vez, sin esperar los 8 dias.
| Diferencia | Sandbox | Produccion |
|---|---|---|
| Llega a la EPS | No | Si |
| Cobra creditos | No | Si |
| Se borra solo | A los 8 dias | No |
Endpoint /simulate | Si | No existe |
| Credenciales | Publicas y compartidas | Tuyas, desde Configuracion |
| Prefijo de la llave | aci_test_ | aci_live_ |
API key y secret key
Cada usuario genera sus propias credenciales desde Configuracion > Integracion API. Las dos son obligatorias y representan los permisos y el alcance de ese usuario.
Las credenciales completas se muestran una sola vez. Guardalas en el administrador de secretos de tu servidor. Nunca las publiques en JavaScript del navegador, repositorios, capturas o correos.
Cabeceras obligatorias
X-ACI-API-Key: aci_live_TU_API_KEY
X-ACI-Secret-Key: aci_secret_TU_SECRET_KEY
Accept: application/json
Content-Type: application/json
Comprobar autenticacion
https://acicolombia.com.co/api/external/v1/mecurl --request GET "https://acicolombia.com.co/api/external/v1/me" \
--header "X-ACI-API-Key: aci_live_TU_API_KEY" \
--header "X-ACI-Secret-Key: aci_secret_TU_SECRET_KEY" \
--header "Accept: application/json"
{
"data": {
"id": 25,
"name": "USUARIO INTEGRACION",
"email": "integracion@empresa.com"
}
}
Forma de las respuestas
Todo lo que devuelve esta API va envuelto en data. No hay excepciones: ni al crear, ni al consultar, ni en los catalogos. Si tu cliente lee el cuerpo directamente va a encontrar un objeto con una sola llave.
// GET /affiliation-requests/AFI-394900
{ "data": { "id": 394900, "tracking_id": "AFI-394900", ... } }
// GET /catalogs/ips?eps_id=3
{ "data": [ { "id": 1193, "code": "1193", "name": "..." }, ... ],
"meta": { "total": 1224, "returned": 1224, "truncated": false, "limit": 5000 } }
Con que identificas una afiliacion
Hay tres cosas que la identifican y no sirven para lo mismo:
| Campo | Ejemplo | Para que |
|---|---|---|
tracking_id | AFI-394900 | Este es el que debes guardar. Es el identificador de seguimiento y el que va en la URL de consulta. |
external_id | VENTA-2026-000184 | Tu propia referencia, la que mandaste al crear. Tambien sirve en la URL de consulta. |
id | 394900 | Nuestro consecutivo interno. Se devuelve por transparencia y la URL lo acepta, pero no es el campo de seguimiento: no lo guardes como tal. |
El campo links
Cada afiliacion trae links.detail, links.status y links.support_pdf ya armados, en el mismo entorno desde el que preguntaste: si consultaste el sandbox, los enlaces son del sandbox.
Corregido el 5 de octubre de 2026. Hasta esa fecha links devolvia siempre las URL de produccion, tambien cuando la respuesta venia del sandbox. Seguir uno de esos enlaces con una llave de sandbox daba 401, porque las credenciales de los dos entornos no se cruzan. Era un error nuestro. Si lo rodeaste armando la URL a mano, puedes seguir haciendolo -es igual de valido- o volver a confiar en links.
Armarla a mano es siempre una via valida; la URL es predecible:
https://acicolombia.com.co/api/external/v1/affiliation-requests/{tracking_id|external_id}
https://acicolombia.com.co/api/external/v1/affiliation-requests/{tracking_id|external_id}/status
https://acicolombia.com.co/api/external/v1/affiliation-requests/{tracking_id|external_id}/support-pdf
Catalogos necesarios
Consulta los catalogos antes de construir el formulario. Conserva en cache los resultados y actualizalos periodicamente; las respuestas incluyen cabeceras de cache para evitar llamadas innecesarias.
| Endpoint | Para que se usa | Filtro |
|---|---|---|
GET /catalogs/document-types | Tipo de documento del afiliado, empresa y beneficiarios. | Sin filtro. |
GET /catalogs/eps | EPS activas y reglas de cada entidad. | Sin filtro. |
GET /catalogs/ips | IPS disponibles para la EPS y la ciudad. | eps_id, city_id. |
GET /catalogs/arls | ARL informada dentro de la afiliacion de salud. | Sin filtro. |
GET /catalogs/pension-funds | Fondo de pension informado dentro de la afiliacion de salud. | Sin filtro. |
GET /catalogs/countries | Pais de nacimiento. | Sin filtro. |
GET /catalogs/departments | Departamentos por pais. | country_id opcional. |
GET /catalogs/cities | Ciudades por departamento. | department_id recomendado. |
GET /catalogs/affiliation-statuses | Estados por los que pasa una solicitud. | Sin filtro. |
GET /catalogs/rejection-reasons | Causales con que se rechaza una afiliacion. | Sin filtro. |
Como mapear tus catalogos con los nuestros
Es la duda mas frecuente, asi que aqui va completa. Cada catalogo devuelve tres campos y cada uno tiene su oficio:
| Campo | Que es | Como se usa |
|---|---|---|
id | Nuestra llave interna. | Es lo que se manda al crear la afiliacion (eps_id, ips_id, document_type_id, expedition_city_id...). No lo deduzcas ni lo escribas a mano: sacalo del catalogo. |
code | El codigo oficial de la entidad. | Es por aqui por donde se mapea. En departamentos y ciudades es el codigo DANE (2 y 5 digitos). En EPS, ARL y fondos de pension es el codigo de la entidad. En paises, ISO de dos letras. |
name | El nombre para mostrar. | Para la pantalla. No mapees por nombre: cambia, lleva tildes y hay homonimos (BOGOTA aparece como departamento y como ciudad). |
La receta: cruza tu catalogo con el nuestro por code, guarda nuestro id, y manda el id. Cruzar por nombre es lo que falla.
Los dos que van encadenados
Dos catalogos dependen de otro y no se pueden pedir sueltos con sentido:
- Ciudades: cuelgan del departamento. Pide
GET /catalogs/departments, y para cada unoGET /catalogs/cities?department_id={id}. Cada ciudad trae sudepartment_id, asi que tambien puedes pedirlas todas de una vez y agrupar. - IPS: cuelgan de la EPS y de la ciudad.
GET /catalogs/ips?eps_id={id}&city_id={id}. La IPS que mandes tiene que ser de la EPS de la afiliacion; si no, la creacion falla con un error de validacion.
Comprueba que recibiste el catalogo completo
Los catalogos grandes traen la cuenta en meta. Miralo antes de dar tu mapeo por bueno:
{ "data": [ ... ],
"meta": { "total": 2161, "returned": 2161, "truncated": false, "limit": 5000 } }
Campo de meta | Que dice |
|---|---|
total | Cuantos hay en total con el filtro que pediste. |
returned | Cuantos van en este cuerpo. |
truncated | true si no caben todos. Entonces hay que filtrar (por eps_id o city_id las IPS, por department_id las ciudades). |
Corregido el 5 de octubre de 2026. GET /catalogs/ips sin filtros devolvia como maximo 1.000 registros de las 2.161 que hay activas, y no lo decia en ninguna parte: quien pedia el catalogo entero para construir su equivalencia se quedaba con menos de la mitad, y las IPS que faltaban le daban error al crear la afiliacion. Hoy vienen todas y, si algun dia no cupieran, meta.truncated lo avisa. Si tu mapeo de IPS es de antes de esa fecha, vuelve a descargarlo.
Dos avisos sobre el codigo DANE de las ciudades
- El catalogo solo devuelve lo activo. Un municipio que se desactiva desaparece de la lista: si lo tenias mapeado, deja de mandarlo.
- Quedan unas pocas ciudades con un
codeque no es DANE (empieza porCO-). Son variantes mal escritas que arrastra una importacion vieja y las estamos depurando. Si tu cruce porcodeno encuentra alguna, es casi seguro una de esas: cruza ese caso por nombre a mano o preguntanos.
Tipos de documento
Estos 6 son los que acepta document_type_id hoy. La tabla se arma leyendo el catalogo, asi que siempre esta completa.
| id | code | Nombre |
|---|---|---|
1 | CC | CEDULA DE CIUDADANIA |
2 | CE | CEDULA DE EXTRANJERIA |
3 | NIT | NIT |
10 | PT | PERMISO POR PROTECCION TEMPORAL (PPT) |
6 | RC | REGISTRO CIVIL DE NACIMIENTO |
5 | TI | TARJETA DE IDENTIDAD |
ARL
Las 10 administradoras de riesgos laborales autorizadas para operar el ramo en Colombia. Va en arl_id.
| id | code | Nombre |
|---|---|---|
10 | ALFA | ARL ALFA |
7 | AURORA | ARL AURORA |
3 | COLMENA | ARL COLMENA |
6 | COLSANITAS | ARL COLSANITAS |
8 | EQUIDAD | ARL LA EQUIDAD |
9 | MAPFRE | ARL MAPFRE |
5 | BOLIVAR | ARL SEGUROS BOLIVAR |
1 | SURA | ARL SURA |
4 | AXA | AXA COLPATRIA |
2 | POSITIVA | POSITIVA COMPANIA DE SEGUROS |
Fondos de pension
Va en pension_fund_id. Si el afiliado no tiene fondo, envia el que aparece como NINGUNO.
| id | code | Nombre |
|---|---|---|
4 | COLFONDOS | COLFONDOS |
3 | COLPENSIONES | COLPENSIONES |
6 | NINGUNO | NINGUNO |
2 | PORVENIR | PORVENIR |
1 | PROTECCION | PROTECCION |
5 | SKANDIA | SKANDIA |
EPS
| id | code | Nombre | Independientes |
|---|---|---|---|
7 |
CAPITAL_SALUD |
CAPITAL SALUD | Si |
10 |
COMFENALCO_VALLE |
COMFENALCO VALLE | Si |
8 |
COOSALUD |
COOSALUD | No |
4 |
COMPENSAR |
EPS COMPENSAR | No |
2 |
SANITAS |
EPS SANITAS | Si, con contrato |
1 |
SURA |
EPS SURA | No |
6 |
FAMISANAR |
FAMISANAR | Si, con contrato |
3 |
NUEVA_EPS |
NUEVA EPS | Si |
5 |
SALUD_TOTAL |
SALUD TOTAL | Si, con contrato |
Ejemplo: EPS e IPS
GET https://acicolombia.com.co/api/external/v1/catalogs/eps
GET https://acicolombia.com.co/api/external/v1/catalogs/ips?eps_id=2&city_id=477
En todos los campos terminados en _id envia la propiedad id del catalogo, no su code. Por ejemplo, Bogota tiene codigo DANE 11001, pero en esta instalacion su ID de catalogo es 477.
Los campos arl_id y pension_fund_id son datos de la afiliacion de salud. Enviarlos no crea solicitudes separadas de ARL o pension.
Tu identificador externo
Puedes enviar external_id para relacionar la afiliacion con el consecutivo de tu sistema. Es opcional, pero muy recomendado.
- Puede ser texto o numero y admite letras, numeros, punto, guion, guion bajo y dos puntos.
- Longitud maxima: 120 caracteres.
- Es unico dentro de tu usuario.
- Tambien se acepta
idcomo alias, pero recomendamos usarexternal_id. - Si repites el mismo valor, la API responde
409y devuelve la afiliacion existente. No cobra otro credito ni crea un duplicado.
{
"external_id": "VENTA-2026-000184",
"eps_id": 2,
"...": "otros campos"
}
Idempotencia
Si se te cae la red despues de enviar una afiliacion, no sabes si quedo creada. Reintentar a ciegas la crearia dos veces y te cobraria dos creditos. Para eso esta la cabecera Idempotency-Key.
Como se usa
Genera un valor unico por cada solicitud que envias (un UUID sirve) y mandalo en el POST. Si tienes que reintentar, manda el mismo valor.
POST https://acicolombia.com.co/api/external/v1/affiliation-requests
Idempotency-Key: 6f1b7a4e-2c53-4f0b-9a8e-7d1c2b3a4f55
Content-Type: application/json
| Situacion | Que recibes |
|---|---|
| Primer envio | 201 Created con la afiliacion nueva. |
| Reintento con la misma llave y el mismo cuerpo | La misma respuesta del primer envio, con la cabecera Idempotency-Replayed: true. No se crea nada nuevo. |
| Misma llave con un cuerpo distinto | 422. No es un reintento: usa una llave nueva. |
| Dos envios simultaneos con la misma llave | Uno pasa y el otro recibe 409. Reintenta ese en unos segundos. |
| El primer envio fallo por validacion | La llave no queda reservada: corrige y reintenta con la misma. |
Las llaves se recuerdan 24 horas. Pasado ese tiempo, la misma llave vuelve a valer como una solicitud nueva.
El external_id te protege igual pero de otra forma: si repites uno ya usado, la API responde 409 con la afiliacion existente. Puedes usar los dos a la vez; son cinturon y tirantes.
Limites de uso
Hay un tope de peticiones por minuto. No es contra ti: es para que el consumo de un cliente no deje sin servicio a los demas.
| Entorno | Lectura | Crear afiliaciones |
|---|---|---|
| Produccion | 120 por minuto | 30 por minuto |
| Sandbox | 60 por minuto (y 300 entre todos) | 15 por minuto (y 60 entre todos) |
| Sin credenciales validas | 20 por minuto por IP | 10 por minuto por IP |
El limite se cuenta por credencial, no por IP: si sales por la misma IP que otro cliente, su consumo no te afecta.
Cuando te pasas
Recibes 429 Too Many Requests con la cabecera Retry-After en segundos. Espera ese tiempo y reintenta; no insistas en bucle.
HTTP/1.1 429 Too Many Requests
Retry-After: 37
Si tu volumen real necesita mas, escribenos: se sube por cliente. Lo que no funciona es reintentar mas rapido.
Referencia de campos
Datos de la solicitud
| Campo | Tipo | Regla | Descripcion |
|---|---|---|---|
| external_id | string | Opcional | ID propio para seguimiento e idempotencia. |
| request_type | string | Opcional | Si se envia debe ser EPS. La API lo asigna automaticamente. |
| eps_id | integer | Obligatorio | ID del catalogo de EPS. |
| ips_id | integer|null | Segun EPS | IPS elegida para la afiliacion. |
| arl_id | integer | Obligatorio | ARL actual informada en el formulario. |
| pension_fund_id | integer | Obligatorio | Fondo de pension actual. |
| affiliation_type | string | Obligatorio | NUEVO, TRASLADO, INCLUSION o NOVEDAD. |
| contributor_type | string | Obligatorio | DEPENDIENTE o INDEPENDIENTE. |
| salary | number | Obligatorio | Debe cumplir el salario minimo vigente al dia de la solicitud. |
| position | string | Obligatorio | Cargo, maximo 120 caracteres. |
| entry_date | date | Obligatorio | Formato YYYY-MM-DD; entre 60 dias atras y 15 dias adelante. |
| is_sat | boolean | Opcional | Solo para traslados realizados mediante SAT. |
| sat_number | string | Condicional | Obligatorio con SAT, alfanumerico y minimo 16 caracteres. |
Persona principal
| Campo dentro de person | Regla | Descripcion |
|---|---|---|
| document_type_id | Obligatorio | ID de tipo de documento. |
| document_number | Obligatorio | Numero del documento, maximo 40 caracteres. |
| first_name / first_last_name | Obligatorios | Primer nombre y primer apellido. |
| middle_name / second_last_name | Opcionales | Segundo nombre y segundo apellido. |
| sex | Obligatorio | MASCULINO, FEMENINO o NO BINARIO. |
| gender_identity | Obligatorio | MASCULINO, FEMENINO, NO BINARIO, HOMBRE TRANS, MUJER TRANS, GENERO FLUIDO, OTRA o PREFIERE NO INFORMAR. |
| birth_date | Obligatorio | Fecha de nacimiento YYYY-MM-DD. |
| expedition_department_id / expedition_city_id | Obligatorios | Lugar de expedicion; la ciudad debe pertenecer al departamento. |
| expedition_date | Obligatorio | No puede ser posterior al dia actual. |
| birth_country_id | Obligatorio | Pais de nacimiento. |
| birth_department_id / birth_city_id | Condicionales | Obligatorios cuando el pais de nacimiento es Colombia. |
| residence_department_id / residence_city_id | Obligatorios | Lugar de residencia. |
| address / neighborhood | Obligatorios | Direccion y barrio. |
| residence_type | Obligatorio | URBANA o RURAL. |
| email / phone | Obligatorios | Datos de contacto del afiliado. |
Empresa para dependientes
El objeto company es obligatorio cuando contributor_type es DEPENDIENTE y se omite para independientes.
| Campo dentro de company | Regla | Descripcion |
|---|---|---|
| document_type_id | Obligatorio | Tipo de documento de la empresa. |
| document_number | Obligatorio | NIT o documento registrado. |
| business_name | Obligatorio | Razon social. |
| department_id / city_id | Obligatorios | Ubicacion de la empresa. |
| address / phone / email | Obligatorios | Datos de contacto. |
| hr_contact | Obligatorio | Nombre del contacto de talento humano. |
Documentos PDF en base64
Los documentos se envian dentro del objeto documents. Cada archivo debe ser un PDF real, tener una sola pagina y pesar maximo 4 MB una vez decodificado.
"documents": {
"principal": {
"file_name": "formulario-afiliacion.pdf",
"content_base64": "JVBERi0xLjQKJ..."
},
"independent_contract": null,
"withdrawal": null,
"sat_proof": null,
"others": []
}
| Propiedad | Cuando se usa |
|---|---|
| principal | Siempre. Documento principal de la afiliacion. |
| independent_contract | Cuando la EPS exige contrato para independientes. |
| withdrawal | Soporte de retiro de otra EPS cuando aplique. |
| sat_proof | Obligatorio si is_sat es true. |
| others | Hasta 10 documentos adicionales. |
Tambien se acepta el base64 como texto directo o como data URL: data:application/pdf;base64,JVBERi0....
Crear una afiliacion de salud
https://acicolombia.com.co/api/external/v1/affiliation-requestsEjemplo completo para una persona dependiente sin beneficiarios:
{
"external_id": "VENTA-2026-000184",
"request_type": "EPS",
"eps_id": 2,
"ips_id": 1365,
"arl_id": 1,
"pension_fund_id": 1,
"affiliation_type": "NUEVO",
"contributor_type": "DEPENDIENTE",
"is_sat": false,
"salary": 2000000,
"position": "AUXILIAR ADMINISTRATIVO",
"entry_date": "2026-07-25",
"person": {
"document_type_id": 1,
"document_number": "10101010",
"first_name": "ANA",
"middle_name": "MARIA",
"first_last_name": "PEREZ",
"second_last_name": "GOMEZ",
"sex": "FEMENINO",
"gender_identity": "FEMENINO",
"birth_date": "1990-02-10",
"expedition_department_id": 37,
"expedition_city_id": 477,
"expedition_date": "2010-03-15",
"birth_country_id": 1,
"birth_department_id": 37,
"birth_city_id": 477,
"residence_department_id": 37,
"residence_city_id": 477,
"address": "CALLE 1 2 3",
"neighborhood": "CENTRO",
"residence_type": "URBANA",
"email": "ana@example.com",
"phone": "3000000000"
},
"company": {
"document_type_id": 3,
"document_number": "900000000",
"business_name": "EMPRESA DEMO SAS",
"department_id": 37,
"city_id": 477,
"address": "AVENIDA 1 2 3",
"phone": "6010000000",
"email": "rrhh@example.com",
"hr_contact": "TALENTO HUMANO"
},
"documents": {
"principal": {
"file_name": "afiliacion.pdf",
"content_base64": "JVBERi0xLjQKJ..."
},
"others": []
},
"beneficiaries": []
}
Variantes del flujo
Afiliado independiente
Usa contributor_type: INDEPENDIENTE, omite company y envia documents.independent_contract cuando el catalogo de la EPS indique requires_independent_contract: true.
Traslado por SAT
{
"affiliation_type": "TRASLADO",
"is_sat": true,
"sat_number": "SAT202607220000000000000001",
"documents": {
"principal": { "file_name": "afiliacion.pdf", "content_base64": "JVBERi0..." },
"sat_proof": { "file_name": "comprobante-sat.pdf", "content_base64": "JVBERi0..." }
}
}
Inclusion de beneficiarios
Para affiliation_type: INCLUSION debes enviar al menos un beneficiario. Los datos de residencia y contacto se heredan de la persona principal.
"beneficiaries": [
{
"relationship": "HIJO",
"person": {
"document_type_id": 5,
"document_number": "10203040",
"first_name": "JUAN",
"middle_name": "",
"first_last_name": "PEREZ",
"second_last_name": "GOMEZ",
"sex": "MASCULINO",
"gender_identity": "MASCULINO",
"birth_date": "2015-08-20",
"birth_country_id": 1,
"birth_department_id": 37,
"birth_city_id": 477
},
"documents": {
"DOCUMENTO_IDENTIDAD_BENEFICIARIO": {
"file_name": "identidad.pdf",
"content_base64": "JVBERi0..."
},
"REGISTRO_CIVIL_HIJO": {
"file_name": "registro-civil.pdf",
"content_base64": "JVBERi0..."
}
}
}
]
Parentescos aceptados: CONYUGE, ESPOSO, ESPOSA, HIJO, HIJA, PADRE y MADRE. Los soportes requeridos cambian por parentesco y edad.
Respuesta al crear
Una creacion correcta responde 201 Created. Guarda tracking_id, external_id y form_number. El cuerpo viene envuelto en data y el campo de seguimiento es tracking_id, no el id numerico: esta explicado en Forma de las respuestas.
{
"data": {
"id": 394900,
"tracking_id": "AFI-394900",
"external_id": "VENTA-2026-000184",
"request_type": "EPS",
"form_number": "987654324",
"form_number_source": "EPS_SEQUENCE",
"status": {
"id": 8,
"code": "REVISION_DOCUMENTAL",
"name": "REVISION DOCUMENTAL"
},
"eps": { "id": 2, "code": "SANITAS", "name": "EPS SANITAS" },
"filing_date": "2026-07-25",
"links": {
"detail": "https://acicolombia.com.co/api/external/v1/affiliation-requests/VENTA-2026-000184",
"status": "https://acicolombia.com.co/api/external/v1/affiliation-requests/VENTA-2026-000184/status",
"support_pdf": "https://acicolombia.com.co/api/external/v1/affiliation-requests/VENTA-2026-000184/support-pdf"
}
}
}
Consultar estado y detalle
Puedes usar tu external_id, el tracking_id devuelto por ACI o el ID interno numerico.
Respuesta resumida de estado
https://acicolombia.com.co/api/external/v1/affiliation-requests/{referencia}/statusGET https://acicolombia.com.co/api/external/v1/affiliation-requests/VENTA-2026-000184/status
Detalle completo
https://acicolombia.com.co/api/external/v1/affiliation-requests/{referencia}Agrega ?include_documents=1 solamente cuando necesites recibir los documentos en base64. Esto aumenta considerablemente el peso de la respuesta.
El detalle devuelve la persona completa: no solo el nombre y el documento, sino tambien el lugar y la fecha de expedicion, el nacimiento y la residencia. Sirve para comprobar que quedo guardado lo que mandaste.
{
"data": {
"tracking_id": "AFI-394900",
"person": {
"document_type": "CC",
"document_number": "1000383537",
"name": "DANIELA OSPINA ARANGO",
"first_name": "DANIELA",
"middle_name": null,
"first_last_name": "OSPINA",
"second_last_name": "ARANGO",
"birth_date": "2000-06-17",
"sex": "FEMENINO",
"gender_identity": "FEMENINO",
"email": "OSPINARANGO6@GMAIL.COM",
"phone": "3186231240",
"address": "CARRERA 100 # 50B SUR 45",
"neighborhood": "CENTRO",
"expedition_date": "2018-06-17",
"expedition_department": { "id": 37, "code": "11", "name": "BOGOTA" },
"expedition_city": { "id": 477, "code": "11001", "name": "BOGOTA" },
"birth_country": { "id": 1, "code": "CO", "name": "COLOMBIA" },
"birth_department": { "id": 37, "code": "11", "name": "BOGOTA" },
"birth_city": { "id": 477, "code": "11001", "name": "BOGOTA" },
"residence_department": { "id": 37, "code": "11", "name": "BOGOTA" },
"residence_city": { "id": 477, "code": "11001", "name": "BOGOTA" }
}
}
}
Los lugares vienen con la misma forma que eps e ips -id, code y name- o null si no hay dato; nunca a medias. El code de departamentos y ciudades es el DANE, asi que puedes cotejarlo contra tu propio catalogo sin volver a pedir nada.
Anadido el 5 de octubre de 2026. Antes el bloque person del detalle traia solo document_type, document_number y name. Se podia mandar la expedicion al crear (expedition_department_id, expedition_city_id, expedition_date) pero no volver a leerla, asi que no habia forma de comprobar que habia quedado bien. Lo reporto un integrador y tenia razon.
El resumen (/status y el listado) sigue trayendo solo el nombre y el documento, a proposito: son muchas filas y esto son siete relaciones por cada una. Para el detalle de la persona, pide la afiliacion concreta.
GET https://acicolombia.com.co/api/external/v1/affiliation-requests/AFI-394900
GET https://acicolombia.com.co/api/external/v1/affiliation-requests/VENTA-2026-000184?include_documents=1
Webhooks
Consultar el estado en bucle es caro para los dos. Con un webhook, te avisamos nosotros: cuando una afiliacion tuya queda radicada o rechazada, te llega un POST a la URL que registres, con la causal incluida.
Registrar tu URL
Entra a Configuracion > Integracion API, en el mismo sitio donde generas la API key, y pulsa Registrar webhook. Al guardar se genera un secreto que se muestra una sola vez: guardalo, es con lo que verificas que el aviso viene de nosotros.
La URL tiene que ser https y publica. No se aceptan direcciones de red interna ni localhost.
Desde ahi mismo puedes mandarte un aviso de prueba para comprobar tu endpoint sin esperar a que se mueva una afiliacion real.
De que te avisamos
Al registrar el webhook eliges el alcance:
| Alcance | Que recibes |
|---|---|
| Solo radicadas y rechazadas (por defecto) | El desenlace, con la causal y la observacion. Es lo que necesita la mayoria. |
| Todos los cambios de estado | Ademas, cada paso intermedio (revision documental, validado ADRES, cargue devolucion...). Util si quieres reflejar el avance en tu sistema. |
Eventos
| Evento | Cuando llega |
|---|---|
affiliation.filed | La afiliacion quedo radicada en la EPS. Trae el form_number y la fecha de radicacion. |
affiliation.rejected | La afiliacion fue rechazada. Trae el objeto rejection con la causal. |
affiliation.status_changed | Cualquier otro cambio de estado. Solo si elegiste todos los cambios. |
webhook.test | Solo cuando pulsas Enviar prueba desde el portal. |
Todos los avisos traen data.observation: la nota del cambio de estado. Ahi va lo que respondio la EPS, lo que anoto quien lo movio a mano o el detalle del correo de rechazo (quien, que anexo y por que). Y traen data.rejection siempre que la afiliacion tenga causal, no solo en el evento de rechazo.
Que recibes
POST https://tu-sistema.com/webhooks/aci
Content-Type: application/json
X-ACI-Event: affiliation.rejected
X-ACI-Delivery: 4821
X-ACI-Timestamp: 1785443901
X-ACI-Signature: sha256=d6dda0a86b8a7b2eafc7e2b58addd2ee...
{
"event": "affiliation.rejected",
"sent_at": "2026-07-30T15:38:17-05:00",
"environment": "production",
"data": {
"id": 420413,
"external_id": "VENTA-2026-000184",
"status": "RECHAZADA",
"status_name": "RECHAZADA",
"eps": "NUEVA_EPS",
"document_type": "CC",
"document_number": "1010101010",
"form_number": "ACI-420413",
"filing_date": "2026-07-30",
"updated_at": "2026-07-30T15:38:17-05:00",
"rejection": {
"code": "FALTAN_DOCUMENTOS",
"reason": "FALTA(N) DOCUMENTO(S)",
"notes": "Falta el documento de identidad.",
"refunds_credit": false
}
}
}
En affiliation.filed el objeto rejection no viene.
Verificar la firma
No confies en un aviso solo porque llego a tu URL: cualquiera que la conozca puede enviarte uno. La firma es lo que demuestra que es nuestro.
Se calcula asi: HMAC-SHA256( X-ACI-Timestamp + "." + cuerpo_crudo, tu_secreto ). Usa el cuerpo tal como llego, sin volver a serializarlo.
// PHP
$cuerpo = file_get_contents('php://input');
$marca = $_SERVER['HTTP_X_ACI_TIMESTAMP'];
$esperada = 'sha256=' . hash_hmac('sha256', $marca . '.' . $cuerpo, $TU_SECRETO);
if (! hash_equals($esperada, $_SERVER['HTTP_X_ACI_SIGNATURE'])) {
http_response_code(401);
exit;
}
// Rechaza tambien lo que venga con mas de 5 minutos, para que nadie
// pueda reenviarte manana un aviso valido de hoy.
if (abs(time() - (int) $marca) > 300) {
http_response_code(401);
exit;
}
http_response_code(200);
# Python
import hmac, hashlib, time
def valido(cuerpo: bytes, marca: str, firma: str, secreto: str) -> bool:
esperada = 'sha256=' + hmac.new(
secreto.encode(), f'{marca}.'.encode() + cuerpo, hashlib.sha256
).hexdigest()
return hmac.compare_digest(esperada, firma) and abs(time.time() - int(marca)) <= 300
Como respondes tu
| Tu respuesta | Que hacemos |
|---|---|
2xx | Entregado. No se vuelve a enviar. |
4xx | Entendemos que lo rechazaste a proposito. No se reintenta. |
5xx o sin respuesta | Se reintenta hasta 5 veces, esperando 1, 5, 15 y 60 minutos. |
Contesta 200 rapido y procesa despues. Si tardas mas de 15 segundos, el envio cuenta como fallido y te llega repetido.
Un mismo evento puede llegarte mas de una vez (un reintento tras un corte de red que ya habias recibido). Usa X-ACI-Delivery para descartar los repetidos.
El historial de envios, con lo que respondio tu servidor, queda en Configuracion > Integracion API.
Causales de rechazo
Cuando una afiliacion se rechaza, el campo rejection.code trae uno de estos valores. refunds_credit indica si te devolvemos el credito.
| code | Que significa | Devuelve credito |
|---|---|---|
YA_ACTIVO |
YA ACTIVO | No |
ACTIVO_OTRA_EPS |
ACTIVO EN OTRA EPS | No |
ACTIVO_REGIMEN_ESPECIAL |
ACTIVO EN REGIMEN ESPECIAL | No |
COTIZANTE_INACTIVO |
COTIZANTE INACTIVO | No |
COTIZANTE_NO_PERTENECE_EPS |
EL COTIZANTE NO PERTENECE A LA EPS SOLICITADA | Si |
NO_CUMPLE_TIEMPO |
NO CUMPLE TIEMPO | No |
DOCUMENTO_INCOMPLETO |
DOCUMENTO INCOMPLETO: FALTA UNA CARA | No |
FALTAN_DOCUMENTOS |
FALTA(N) DOCUMENTO(S) | No |
DOCUMENTO_ERRADO |
DOCUMENTO ERRADO | No |
DOCUMENTO_ILEGIBLE |
DOCUMENTO ILEGIBLE | No |
DOCUMENTO_PESADO |
DOCUMENTO DEMASIADO PESADO | No |
FIRMA_INVALIDA |
FIRMA NO VALIDA | No |
FIRMAS_MISMA_MANO |
FIRMAS DE COTIZANTE Y EMPLEADOR IGUALES | No |
SOLICITUD_RECIENTE_EN_LA_EPS |
YA HAY UNA SOLICITUD RECIENTE EN LA EPS | No |
EDAD_COTIZANTE_NO_PERMITIDA |
EDAD DEL COTIZANTE NO PERMITIDA | No |
PARENTESCO_INCONSISTENTE |
PARENTESCO DEL BENEFICIARIO INCONSISTENTE | Si |
FECHA_INGRESO_INCONSISTENTE |
FECHA DE INGRESO INCONSISTENTE | Si |
CORREO_INCONSISTENTE |
CORREO DEL AFILIADO INCONSISTENTE | No |
TIPO_AFILIACION_ERRADA |
TIPO DE AFILIACION ERRADA | No |
EMPRESA_NO_CREADA |
EMPRESA NO CREADA | Si |
EMPRESA_EN_MORA |
EMPRESA EN MORA | No |
DATOS_EMPRESA_INCONSISTENTES |
DATOS DE EMPRESA INCONSISTENTES | No |
EPS_NO_IDENTIFICADA |
EPS NO IDENTIFICADA | Si |
ANULADA |
ANULADA | No |
SIN_OBSERVACION |
SIN OBSERVACION | No |
RADICACION_FISICA |
LA EPS EXIGE RADICACION FISICA | Si |
SIN_COBERTURA_MUNICIPIO |
LA EPS NO TIENE COBERTURA EN EL MUNICIPIO | Si |
CELULAR_INCONSISTENTE |
NUMERO DE CELULAR INCONSISTENTE | Si |
La lista sale del catalogo, asi que siempre esta al dia. Tambien la puedes consultar con GET /catalogs/rejection-reasons.
Que hacer con las mas comunes
| Causal | Que la produce | Que hacer |
|---|---|---|
EMPRESA_NO_CREADA | La EPS no tiene registrada esa empresa. | La empresa debe crearse primero ante la EPS. Despues se puede reenviar. |
EMPRESA_EN_MORA | El empleador esta en mora con la EPS. | Se resuelve entre la empresa y la EPS; no se puede reenviar antes. |
YA_ACTIVO | La persona ya esta activa en esa EPS. | No hay nada que radicar. Revisa el estado en ADRES antes de enviar. |
ACTIVO_OTRA_EPS | Esta activo en otra EPS. | Corresponde un traslado, no una afiliacion nueva. |
FALTAN_DOCUMENTOS / DOCUMENTO_ILEGIBLE | Falta un anexo o no se puede leer. | Reenvia con el documento correcto y un external_id nuevo. |
DOCUMENTO_ERRADO | El documento no corresponde a la persona. | Corrige los datos y vuelve a enviar. |
FECHA_INGRESO_INCONSISTENTE | La fecha de ingreso no cuadra con la novedad. | Revisa entry_date y reenvia. |
CORREO_INCONSISTENTE | El correo del afiliado no es valido o no es suyo. | Corrige el correo antes de reenviar. |
NO_CUMPLE_TIEMPO | No se cumplio el tiempo minimo de permanencia para el traslado. | Hay que esperar. La EPS informa desde cuando. |
Descargar el PDF soporte
https://acicolombia.com.co/api/external/v1/affiliation-requests/{referencia}/support-pdfLa respuesta es binaria con Content-Type: application/pdf. No intentes interpretarla como JSON. Guarda el cuerpo de la respuesta directamente como archivo.
curl "https://acicolombia.com.co/api/external/v1/affiliation-requests/VENTA-2026-000184/support-pdf" \
--header "X-ACI-API-Key: aci_live_TU_API_KEY" \
--header "X-ACI-Secret-Key: aci_secret_TU_SECRET_KEY" \
--output soporte-afiliacion.pdf
Listar afiliaciones de salud
https://acicolombia.com.co/api/external/v1/affiliation-requests| Parametro | Descripcion |
|---|---|
| external_id | Coincidencia exacta con tu identificador. |
| document_number | Inicio del documento de la persona. |
| form_number | Inicio del numero de formulario. |
| affiliation_type | NUEVO, TRASLADO, INCLUSION o NOVEDAD. |
| created_from / created_to | Rango de fechas en formato YYYY-MM-DD. |
| per_page | Entre 1 y 100. Valor predeterminado: 25. |
| page | Numero de pagina. |
GET https://acicolombia.com.co/api/external/v1/affiliation-requests?external_id=VENTA-2026-000184
GET https://acicolombia.com.co/api/external/v1/affiliation-requests?created_from=2026-07-01&created_to=2026-07-31&per_page=50
Consecutivo automatico de Sanitas
Cuando la EPS seleccionada es Sanitas, no debes enviar el numero de formulario. ACI reserva automaticamente el siguiente consecutivo y lo incrementa dentro de la misma transaccion.
- Dos solicitudes simultaneas no reciben el mismo formulario.
- Si la radicacion falla y la transaccion se revierte, el consecutivo no se consume.
- El numero asignado aparece en
form_numbery el origen enform_number_source: EPS_SEQUENCE. - El PDF soporte se genera con ese mismo numero.
Ejemplos de codigo
JavaScript en servidor
const response = await fetch('https://acicolombia.com.co/api/external/v1/affiliation-requests/VENTA-2026-000184/status', {
headers: {
'X-ACI-API-Key': process.env.ACI_API_KEY,
'X-ACI-Secret-Key': process.env.ACI_SECRET_KEY,
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`ACI respondio ${response.status}: ${await response.text()}`);
}
const result = await response.json();
console.log(result.data.status);
PHP con cURL
$curl = curl_init('https://acicolombia.com.co/api/external/v1/affiliation-requests/VENTA-2026-000184/status');
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-ACI-API-Key: '.getenv('ACI_API_KEY'),
'X-ACI-Secret-Key: '.getenv('ACI_SECRET_KEY'),
'Accept: application/json',
],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($status !== 200) {
throw new RuntimeException($body);
}
$data = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
Errores y reintentos
| HTTP | Significado | Que hacer |
|---|---|---|
| 400 | Solicitud mal formada. | Revisa JSON, cabeceras y parametros. |
| 401 | Falta una credencial o alguna es invalida. | Envia API key y secret key vigentes. |
| 403 | Usuario inactivo o sin permiso. | Valida el estado y permisos del usuario en ACI. |
| 404 | La referencia no existe o no es visible. | Comprueba external_id o tracking_id. |
| 409 | external_id ya utilizado. | Usa los enlaces de la afiliacion existente incluidos en la respuesta. |
| 422 | Validacion de negocio. | Lee el objeto errors; cada clave identifica el campo. |
| 429 | Limite temporal excedido. | Espera y reintenta con backoff exponencial. |
| 500 | Error inesperado. | No cambies external_id al reintentar; asi evitas duplicados. |
Ejemplo 422
{
"message": "The given data was invalid.",
"errors": {
"person.expedition_city_id": [
"La ciudad de expedicion no pertenece al departamento seleccionado."
],
"salary": [
"El salario no puede ser menor al salario minimo vigente."
]
}
}
Reintenta automaticamente solo errores temporales como 429 o 5xx. Para 4xx, corrige los datos antes de volver a enviar.