Guía de implementación
Salud (IPS)
Para IPS: catálogo y disponibilidad, agendar, cancelar, reprogramar y registrar asistencia en nombre del paciente; facturas de salud con estado DIAN, XML, RIPS y glosas; pago del pagador (EPS) y carné de vacunación.
En esta página
Esta guía es para el equipo técnico de una IPS (o de una entidad con el módulo de salud contratado) que integra su sistema con Compense server-to-server: agenda de citas, facturación de salud y carné de vacunación. Es el mismo gateway y el mismo modelo de seguridad que créditos o billetera. Los ejemplos usan a IPS Horizonte Salud, que agenda desde su call center y concilia los giros de las EPS desde su sistema de cartera.
client.health no es client.system.health()
client.health (en .NET client.Health, en Go client.Health) es este módulo de servicios de
salud: rutas /v1/gateway/health-services/.... client.system.health() es otra cosa: verifica tu
llave y devuelve sus scopes (GET /v1/gateway/health/). No los confundas al leer logs ni al dar
permisos.
Antes de empezar #
Quién puede usarlo #
Solo entidades tipo IPS o con el módulo salud contratado. Cualquier otra recibe 403 con
«La entidad no tiene habilitados los servicios de salud…». El carné de vacunación exige además que
la vacunación esté habilitada en la configuración de la IPS (si no, 403 «La vacunación no está
habilitada en la entidad.»).
Scopes #
| Scope | Permite |
|---|---|
appointments:read | Catálogo, disponibilidad, citas de un paciente y detalle de una cita |
appointments:write | Agendar, cancelar, reprogramar y registrar asistencia |
health_billing:read | Facturas, detalle, XML, RIPS y glosas |
health_billing:collect | Registrar el pago (recaudo) del pagador sobre una factura |
vaccination:read | Carné y esquema de vacunación de un paciente |
health_billing:collect está en la lista de scopes que exigirán firma de la petición cuando se
active esa protección en tu llave.
El paciente #
El paciente se identifica con document: el número de documento del titular o de un
beneficiario de su grupo familiar (query en lecturas, cuerpo en escrituras). En las respuestas el
nombre y el documento salen enmascarados (L***, 103****89); el identificador estable para
cruzar con tu sistema es patient.id, y el número de historia clínica, medical_record_number.
La historia clínica no se expone por este gateway: el intercambio clínico entre instituciones va por el canal FHIR B2B de Compense, con su propio consentimiento y credenciales.
Operaciones #
Las lecturas devuelven el cuerpo del motor; las escrituras, el sobre estándar
({"status": "success", "transaction_id": "…", "module": "health", "data": {…}}). Envía siempre
Idempotency-Key en las escrituras: un reintento con la misma llave devuelve el resultado original y
nunca agenda ni cobra dos veces.
Catálogo y disponibilidad #
El catálogo trae especialidades, servicios, sedes, profesionales, tipos de cita
(first_time, follow_up, control), tipos de consulta (presencial, telemedicina) y canales
(recepcion, telefono). Con esos ids consultas los cupos libres de un día (date), de un rango
(start_date/end_date) o los más próximos (asap=true), filtrando por doctor_id, location_id,
specialty_id o service_id. La disponibilidad pagina por número de página.
import os
from compense_gateway import CompenseClient
client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
catalogo = client.health.catalog()
medico = catalogo["doctors"][0]["id"]
cupos = client.health.availability(date="2026-11-03", doctor_id=medico)
for cupo in cupos.results:
print(cupo["id"], cupo["date"], cupo["start_time"], cupo["doctor"]["name"], cupo["location"]["name"])import { CompenseClient } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
const catalogo = await client.health.catalog();
const medico = String((catalogo.doctors as Array<Record<string, unknown>>)[0].id);
const cupos = await client.health.availability({ date: "2026-11-03", doctorId: medico });
for (const cupo of cupos.results) {
console.log(cupo.id, cupo.date, cupo.start_time);
}<?php
require __DIR__ . '/vendor/autoload.php';
use Compense\Sdk\CompenseClient;
$client = new CompenseClient(apiKey: getenv('COMPENSE_API_KEY'), environment: 'sandbox');
$catalogo = $client->health->catalog();
$medico = $catalogo['doctors'][0]['id'];
$cupos = $client->health->availability(date: '2026-11-03', doctorId: $medico);
foreach ($cupos->results as $cupo) {
echo $cupo['id'], ' ', $cupo['date'], ' ', $cupo['start_time'], PHP_EOL;
}using System.Text.Json.Nodes;
using Compense.Sdk;
using Compense.Sdk.Models;
using var client = new CompenseClient(
Environment.GetEnvironmentVariable("COMPENSE_API_KEY")!, CompenseEnvironment.Sandbox);
JsonObject catalogo = await client.Health.CatalogAsync();
string medico = catalogo["doctors"]![0]!["id"]!.GetValue<string>();
Page cupos = await client.Health.AvailabilityAsync(date: "2026-11-03", doctorId: medico);
foreach (JsonObject cupo in cupos.Results)
{
Console.WriteLine($"{cupo["id"]} {cupo["date"]} {cupo["start_time"]}");
}package main
import (
"context"
"fmt"
"log"
"os"
compense "github.com/Appiasoft/compense-saas-sdk-go"
)
func main() {
ctx := context.Background()
client, err := compense.NewClient(os.Getenv("COMPENSE_API_KEY"),
compense.WithEnvironment(compense.Sandbox))
if err != nil {
log.Fatal(err)
}
catalogo, err := client.Health.Catalog(ctx)
if err != nil {
log.Fatal(err)
}
medicos, _ := catalogo["doctors"].([]any)
medico, _ := medicos[0].(map[string]any)["id"].(string)
cupos, err := client.Health.Availability(ctx, &compense.AvailabilityParams{Date: "2026-11-03", DoctorID: medico})
if err != nil {
log.Fatal(err)
}
for _, cupo := range cupos.Results {
fmt.Println(cupo["id"], cupo["date"], cupo["start_time"])
}
}curl https://sandbox.dev.compense.co/gw/v1/gateway/health-services/catalog/ \
-H "X-API-Key: $COMPENSE_API_KEY"
curl "https://sandbox.dev.compense.co/gw/v1/gateway/health-services/availability/?date=2026-11-03&doctor_id=$MEDICO" \
-H "X-API-Key: $COMPENSE_API_KEY"{"count": 5, "page": 1, "page_size": 25, "results": [
{"id": "slt-D1-20261103-0800", "date": "2026-11-03", "start_time": "08:00", "end_time": "08:20",
"doctor": {"id": "…", "name": "Dra. Sandbox Medicina General"},
"location": {"id": "…", "name": "Sede Sandbox Centro"}}]}No se puede consultar ni agendar en una fecha pasada (400).
Agendar en nombre del paciente #
sequenceDiagram
autonumber
participant CC as Call center de IPS Horizonte Salud
participant G as Gateway Compense
CC->>G: GET availability (date, doctor_id)
G-->>CC: cupos libres (slot_id)
CC->>G: POST appointments {document, slot_id} + Idempotency-Key
alt paciente nuevo
G-->>CC: 404 HEALTH_PATIENT_NOT_FOUND
CC->>G: POST appointments {document, slot_id, new_patient} + otra llave
end
alt cupo tomado entre la consulta y el agendamiento
G-->>CC: 409 SLOT_NOT_AVAILABLE → vuelve a consultar disponibilidad
else
G-->>CC: 201 cita confirmada (id, appointment_code)
G-)CC: webhook appointment.booked
endLa cita queda creada por el usuario de servicio del gateway y con las mismas reglas de la recepción:
el cupo se bloquea, solo hay una cita vigente por cupo, el paciente no puede tener choques de horario
y nunca se agenda en un cupo pasado. channel es recepcion (por defecto) o telefono.
Paciente nuevo. Si el documento no existe en la IPS, la respuesta es 404
(HEALTH_PATIENT_NOT_FOUND). Reenvía con new_patient y el paciente se registra sin cuenta de app:
document_type, first_name, last_name, birth_date, gender (M, F u O), phone y,
opcional, email. Usa otra Idempotency-Key para ese segundo intento: el cuerpo es distinto.
from compense_gateway import NotFoundError
PACIENTE = "1000000001"
cupo_id = cupos.results[0]["id"]
base = dict(document=PACIENTE, slot_id=cupo_id, appointment_type="first_time",
consultation_type="presencial", channel="telefono", reason="Control")
try:
tx = client.health.appointments.book(**base, idempotency_key=f"horizonte-salud-{cupo_id}-{PACIENTE}")
except NotFoundError as e:
if "new_patient" not in (e.detail or ""):
raise # otro 404: el cupo no existe
tx = client.health.appointments.book(
**base,
new_patient={"document_type": "CC", "first_name": "Laura", "last_name": "Gómez",
"birth_date": "1990-04-12", "gender": "F", "phone": "3001234567"},
idempotency_key=f"horizonte-salud-{cupo_id}-{PACIENTE}-nuevo",
)
cita = tx.data
print(cita["id"], cita["appointment_code"], cita["status"], cita["patient"]["medical_record_number"])import { NotFoundError } from "@compense/sdk";
const PACIENTE = "1000000001";
const cupoId = String(cupos.results[0].id);
const base = {
document: PACIENTE,
slotId: cupoId,
appointmentType: "first_time",
consultationType: "presencial",
channel: "telefono",
reason: "Control",
};
let tx;
try {
tx = await client.health.appointments.book({ ...base, idempotencyKey: `horizonte-salud-${cupoId}-${PACIENTE}` });
} catch (err) {
if (!(err instanceof NotFoundError) || !(err.detail ?? "").includes("new_patient")) throw err;
tx = await client.health.appointments.book({
...base,
newPatient: {
document_type: "CC", first_name: "Laura", last_name: "Gómez",
birth_date: "1990-04-12", gender: "F", phone: "3001234567",
},
idempotencyKey: `horizonte-salud-${cupoId}-${PACIENTE}-nuevo`,
});
}
console.log(tx.data.id, tx.data.appointment_code, tx.data.status);use Compense\Sdk\Exception\NotFoundException;
$paciente = '1000000001';
$cupoId = $cupos->results[0]['id'];
try {
$tx = $client->health->appointments->book(
document: $paciente, slotId: $cupoId, reason: 'Control',
appointmentType: 'first_time', consultationType: 'presencial', channel: 'telefono',
idempotencyKey: "horizonte-salud-{$cupoId}-{$paciente}",
);
} catch (NotFoundException $e) {
if (!str_contains($e->detail ?? '', 'new_patient')) {
throw $e;
}
$tx = $client->health->appointments->book(
document: $paciente, slotId: $cupoId, reason: 'Control',
appointmentType: 'first_time', consultationType: 'presencial', channel: 'telefono',
newPatient: [
'document_type' => 'CC', 'first_name' => 'Laura', 'last_name' => 'Gómez',
'birth_date' => '1990-04-12', 'gender' => 'F', 'phone' => '3001234567',
],
idempotencyKey: "horizonte-salud-{$cupoId}-{$paciente}-nuevo",
);
}
echo $tx->data['id'], ' ', $tx->data['appointment_code'], ' ', $tx->data['status'], PHP_EOL;const string Paciente = "1000000001";
string cupoId = cupos.Results[0]["id"]!.GetValue<string>();
Transaction tx;
try
{
tx = await client.Health.Appointments.BookAsync(
Paciente, cupoId, reason: "Control", appointmentType: "first_time",
consultationType: "presencial", channel: "telefono",
idempotencyKey: $"horizonte-salud-{cupoId}-{Paciente}");
}
catch (NotFoundException e) when ((e.Detail ?? "").Contains("new_patient"))
{
tx = await client.Health.Appointments.BookAsync(
Paciente, cupoId, reason: "Control", appointmentType: "first_time",
consultationType: "presencial", channel: "telefono",
newPatient: new Dictionary<string, object?>
{
["document_type"] = "CC", ["first_name"] = "Laura", ["last_name"] = "Gómez",
["birth_date"] = "1990-04-12", ["gender"] = "F", ["phone"] = "3001234567",
},
idempotencyKey: $"horizonte-salud-{cupoId}-{Paciente}-nuevo");
}
Console.WriteLine($"{tx.Data["id"]} {tx.Data["appointment_code"]} {tx.Data["status"]}");const paciente = "1000000001"
cupoID, _ := cupos.Results[0]["id"].(string)
params := &compense.BookAppointmentParams{
Document: paciente, SlotID: cupoID, Reason: "Control",
AppointmentType: "first_time", ConsultationType: "presencial", Channel: "telefono",
}
tx, err := client.Health.Appointments.Book(ctx, params,
compense.WithIdempotencyKey("horizonte-salud-"+cupoID+"-"+paciente))
var apiErr *compense.APIError
if compense.IsNotFound(err) && errors.As(err, &apiErr) && strings.Contains(apiErr.Detail, "new_patient") {
params.NewPatient = map[string]any{
"document_type": "CC", "first_name": "Laura", "last_name": "Gómez",
"birth_date": "1990-04-12", "gender": "F", "phone": "3001234567",
}
tx, err = client.Health.Appointments.Book(ctx, params,
compense.WithIdempotencyKey("horizonte-salud-"+cupoID+"-"+paciente+"-nuevo"))
}
if err != nil {
log.Fatal(err)
}
fmt.Println(tx.Data["id"], tx.Data["appointment_code"], tx.Data["status"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/health-services/appointments/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-salud-slt-D1-20261103-0800-1000000001" \
-H "Content-Type: application/json" \
-d '{"document": "1000000001", "slot_id": "slt-D1-20261103-0800", "appointment_type": "first_time",
"consultation_type": "presencial", "channel": "telefono", "reason": "Control"}'
# Paciente nuevo: el mismo cuerpo con new_patient y otra Idempotency-Key
curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/health-services/appointments/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-salud-slt-D1-20261103-0800-9999999999-nuevo" \
-H "Content-Type: application/json" \
-d '{"document": "9999999999", "slot_id": "slt-D1-20261103-0800", "reason": "Control",
"new_patient": {"document_type": "CC", "first_name": "Laura", "last_name": "Gómez",
"birth_date": "1990-04-12", "gender": "F", "phone": "3001234567"}}'Respuesta (data, resumida):
{"id": "aps-D1-20261103-0800-…", "appointment_code": "CITA-SBX-1A2B3C", "status": "confirmed",
"status_label": "Confirmada", "date": "2026-11-03", "start_time": "08:00:00", "end_time": "08:20:00",
"appointment_type": "first_time", "consultation_type": "presencial", "channel": "recepcion",
"doctor": {"id": "…", "name": "Dra. Sandbox Medicina General", "specialty": "Medicina general"},
"location": {"id": "…", "name": "Sede Sandbox Centro", "address": "Cra 1 # 2-3"},
"patient": {"id": "…", "medical_record_number": "HC-SBX-000001", "name": "S***", "document": "100****01"}}Si el cupo se tomó entre tu consulta y el agendamiento, la respuesta es 409 con
SLOT_NOT_AVAILABLE: consulta de nuevo la disponibilidad y ofrece otro horario.
Citas del paciente y detalle #
appointments.list trae las citas del paciente (when: upcoming, past o all; status, por
ejemplo confirmed,cancelled). El detalle acepta el id de la cita o su código CITA-AAAA-NNNNNN.
proximas = client.health.appointments.list(PACIENTE, when="upcoming")
for c in proximas.results:
print(c["appointment_code"], c["date"], c["start_time"], c["status"])
cita = client.health.appointments.get(proximas.results[0]["id"])const proximas = await client.health.appointments.list(PACIENTE, { when: "upcoming" });
for (const c of proximas.results) console.log(c.appointment_code, c.date, c.start_time, c.status);
const cita = await client.health.appointments.get(String(proximas.results[0].id));$proximas = $client->health->appointments->list($paciente, when: 'upcoming');
foreach ($proximas->results as $c) {
echo $c['appointment_code'], ' ', $c['date'], ' ', $c['start_time'], ' ', $c['status'], PHP_EOL;
}
$cita = $client->health->appointments->get($proximas->results[0]['id']);Page proximas = await client.Health.Appointments.ListAsync(Paciente, when: "upcoming");
foreach (JsonObject c in proximas.Results)
Console.WriteLine($"{c["appointment_code"]} {c["date"]} {c["start_time"]} {c["status"]}");
JsonObject cita = await client.Health.Appointments.GetAsync(proximas.Results[0]["id"]!.GetValue<string>());proximas, err := client.Health.Appointments.List(ctx, paciente, &compense.AppointmentListParams{When: "upcoming"})
if err != nil {
log.Fatal(err)
}
for _, c := range proximas.Results {
fmt.Println(c["appointment_code"], c["date"], c["start_time"], c["status"])
}
citaID, _ := proximas.Results[0]["id"].(string)
cita, err := client.Health.Appointments.Get(ctx, citaID)
if err != nil {
log.Fatal(err)
}
fmt.Println(cita["status_label"])curl "https://sandbox.dev.compense.co/gw/v1/gateway/health-services/appointments/?document=1000000001&when=upcoming" \
-H "X-API-Key: $COMPENSE_API_KEY"
curl https://sandbox.dev.compense.co/gw/v1/gateway/health-services/appointments/$CITA/ \
-H "X-API-Key: $COMPENSE_API_KEY"Cancelar y reprogramar #
Cancelar libera el cupo. El motivo es obligatorio (mínimo 5 caracteres). La IPS cancela como su
personal, sin la regla de 2 horas del afiliado; si tu sistema cancela a pedido del paciente (por
ejemplo desde tu propio portal), envía on_behalf_of_patient: true y se aplican las reglas del
paciente (estado y 2 horas de anticipación). Cancelar una cita ya cancelada responde 200 con
already_cancelled: true.
Reprogramar mueve la cita a otro cupo libre con las reglas de la app: al menos 2 horas antes de la cita actual, el cupo nuevo libre y no pasado; el cupo original se libera si faltaba más de una hora. La cita reprogramada puede tener otro id: guarda el que devuelve la respuesta.
tx = client.health.appointments.reschedule(
cita["id"], new_slot_id="slt-D2-20261104-1000",
idempotency_key=f"horizonte-salud-reprograma-{cita['id']}",
)
cita_id = tx.data["id"] # puede cambiar
client.health.appointments.cancel(
cita_id, reason="El paciente no puede asistir",
idempotency_key=f"horizonte-salud-cancela-{cita_id}",
)const rep = await client.health.appointments.reschedule(String(cita.id), {
newSlotId: "slt-D2-20261104-1000",
idempotencyKey: `horizonte-salud-reprograma-${cita.id}`,
});
const citaId = String(rep.data.id); // puede cambiar
await client.health.appointments.cancel(citaId, {
reason: "El paciente no puede asistir",
idempotencyKey: `horizonte-salud-cancela-${citaId}`,
});$rep = $client->health->appointments->reschedule(
$cita['id'], newSlotId: 'slt-D2-20261104-1000',
idempotencyKey: "horizonte-salud-reprograma-{$cita['id']}",
);
$citaId = $rep->data['id']; // puede cambiar
$client->health->appointments->cancel(
$citaId, reason: 'El paciente no puede asistir',
idempotencyKey: "horizonte-salud-cancela-{$citaId}",
);string idActual = cita["id"]!.GetValue<string>();
Transaction rep = await client.Health.Appointments.RescheduleAsync(
idActual, "slt-D2-20261104-1000", idempotencyKey: $"horizonte-salud-reprograma-{idActual}");
string citaId = rep.Data["id"]!.GetValue<string>(); // puede cambiar
await client.Health.Appointments.CancelAsync(
citaId, "El paciente no puede asistir", idempotencyKey: $"horizonte-salud-cancela-{citaId}");rep, err := client.Health.Appointments.Reschedule(ctx, citaID, &compense.RescheduleAppointmentParams{
NewSlotID: "slt-D2-20261104-1000",
}, compense.WithIdempotencyKey("horizonte-salud-reprograma-"+citaID))
if err != nil {
log.Fatal(err)
}
nuevoID, _ := rep.Data["id"].(string) // puede cambiar
if _, err := client.Health.Appointments.Cancel(ctx, nuevoID, &compense.CancelAppointmentParams{
Reason: "El paciente no puede asistir",
}, compense.WithIdempotencyKey("horizonte-salud-cancela-"+nuevoID)); err != nil {
log.Fatal(err)
}curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/health-services/appointments/$CITA/reschedule/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-salud-reprograma-$CITA" \
-H "Content-Type: application/json" \
-d '{"new_slot_id": "slt-D2-20261104-1000"}'
curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/health-services/appointments/$CITA/cancel/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-salud-cancela-$CITA" \
-H "Content-Type: application/json" \
-d '{"reason": "El paciente no puede asistir"}'Registrar asistencia #
Desde la hora de la cita, registra si el paciente asistió (completed) o no (no_show). Solo se
puede desde confirmed o pending, y el cambio queda en el historial de estados de la cita con el
motivo.
stateDiagram-v2
[*] --> confirmed: book
confirmed --> confirmed: reschedule (otro cupo)
confirmed --> cancelled: cancel
confirmed --> completed: status completed
confirmed --> no_show: status no_show
cancelled --> [*]
completed --> [*]
no_show --> [*]client.health.appointments.set_attendance(
cita_id, status="no_show", reason="No se presentó",
idempotency_key=f"horizonte-salud-asistencia-{cita_id}",
)await client.health.appointments.setAttendance(citaId, {
status: "no_show",
reason: "No se presentó",
idempotencyKey: `horizonte-salud-asistencia-${citaId}`,
});$client->health->appointments->setAttendance(
$citaId, status: 'no_show', reason: 'No se presentó',
idempotencyKey: "horizonte-salud-asistencia-{$citaId}",
);await client.Health.Appointments.SetAttendanceAsync(
citaId, "no_show", reason: "No se presentó", idempotencyKey: $"horizonte-salud-asistencia-{citaId}");if _, err := client.Health.Appointments.SetAttendance(ctx, nuevoID, &compense.AttendanceParams{
Status: "no_show", Reason: "No se presentó",
}, compense.WithIdempotencyKey("horizonte-salud-asistencia-"+nuevoID)); err != nil {
log.Fatal(err)
}curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/health-services/appointments/$CITA/status/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-salud-asistencia-$CITA" \
-H "Content-Type: application/json" \
-d '{"status": "no_show", "reason": "No se presentó"}'Registrar la asistencia de una cita futura responde 422 («La asistencia se registra a partir de la
hora de la cita.»).
Facturación de salud #
Por API la facturación es de lectura, más una acción segura: registrar el pago del pagador. Emitir, anular, radicar y responder glosas no se exponen en esta fase: emitir y anular envían documentos a la DIAN (factura o nota crédito) y la anulación con nota crédito exige revisión humana; se hacen desde el panel de la IPS.
Estado (status) | Qué significa |
|---|---|
CUV_OK | Factura electrónica validada, con su código único de validación (CUV) |
RADICADA | Radicada ante el pagador; trae radication_number |
PAGADA | El pagador cubrió el saldo |
El filtro status acepta una lista separada por comas (CUV_OK,RADICADA); además hay payer, q,
document, date_from y date_to.
facturas = client.health.billing.list_invoices(status="CUV_OK,RADICADA", date_from="2026-10-01")
factura = facturas.results[0]
print(factura["number"], factura["status"], factura["net_amount"], factura["balance"])
detalle = client.health.billing.get_invoice(factura["id"]) # líneas, pagos, glosas, radicaciones, notas
xml = client.health.billing.get_invoice_xml(factura["id"]) # {"xml", "filename", "content_type", "xml_hash"}
rips = client.health.billing.get_invoice_rips(factura["id"]) # RIPS JSON ("preview": true si no se han generado)
glosas = client.health.billing.list_glosas(invoice=factura["id"])const facturas = await client.health.billing.listInvoices({ status: "CUV_OK,RADICADA", dateFrom: "2026-10-01" });
const factura = facturas.results[0];
const facturaId = String(factura.id);
const detalle = await client.health.billing.getInvoice(facturaId);
const xml = await client.health.billing.getInvoiceXml(facturaId);
const rips = await client.health.billing.getInvoiceRips(facturaId);
const glosas = await client.health.billing.listGlosas({ invoice: facturaId });$facturas = $client->health->billing->listInvoices(status: 'CUV_OK,RADICADA', dateFrom: '2026-10-01');
$factura = $facturas->results[0];
$detalle = $client->health->billing->getInvoice($factura['id']);
$xml = $client->health->billing->getInvoiceXml($factura['id']);
$rips = $client->health->billing->getInvoiceRips($factura['id']);
$glosas = $client->health->billing->listGlosas(invoice: $factura['id']);
file_put_contents($xml['filename'], $xml['xml']);Page facturas = await client.Health.Billing.ListInvoicesAsync(status: "CUV_OK,RADICADA", dateFrom: "2026-10-01");
string facturaId = facturas.Results[0]["id"]!.GetValue<string>();
JsonObject detalle = await client.Health.Billing.GetInvoiceAsync(facturaId);
JsonObject xml = await client.Health.Billing.GetInvoiceXmlAsync(facturaId);
JsonObject rips = await client.Health.Billing.GetInvoiceRipsAsync(facturaId);
Page glosas = await client.Health.Billing.ListGlosasAsync(invoice: facturaId);facturas, err := client.Health.Billing.ListInvoices(ctx, &compense.InvoiceListParams{
Status: "CUV_OK,RADICADA", DateFrom: "2026-10-01",
})
if err != nil {
log.Fatal(err)
}
facturaID, _ := facturas.Results[0]["id"].(string)
detalle, err := client.Health.Billing.GetInvoice(ctx, facturaID)
if err != nil {
log.Fatal(err)
}
xml, err := client.Health.Billing.GetInvoiceXML(ctx, facturaID)
if err != nil {
log.Fatal(err)
}
rips, err := client.Health.Billing.GetInvoiceRIPS(ctx, facturaID)
if err != nil {
log.Fatal(err)
}
glosas, err := client.Health.Billing.ListGlosas(ctx, &compense.GlosaListParams{Invoice: facturaID})
if err != nil {
log.Fatal(err)
}
fmt.Println(detalle["status"], xml["filename"], rips["preview"], glosas.Count)curl "https://sandbox.dev.compense.co/gw/v1/gateway/health-services/billing/invoices/?status=CUV_OK,RADICADA&date_from=2026-10-01" \
-H "X-API-Key: $COMPENSE_API_KEY"
curl https://sandbox.dev.compense.co/gw/v1/gateway/health-services/billing/invoices/$FACTURA/xml/ \
-H "X-API-Key: $COMPENSE_API_KEY"
curl https://sandbox.dev.compense.co/gw/v1/gateway/health-services/billing/invoices/$FACTURA/rips/ \
-H "X-API-Key: $COMPENSE_API_KEY"
curl "https://sandbox.dev.compense.co/gw/v1/gateway/health-services/billing/glosas/?invoice=$FACTURA" \
-H "X-API-Key: $COMPENSE_API_KEY"- El XML viaja dentro de un JSON (
xml,filename,content_type,xml_hash). Si la factura aún no tiene documento electrónico,422. - Los RIPS salen como vista previa (
preview: true) mientras no se hayan generado; sin número DIAN,422. - Una glosa trae
glosa_type,concept_code,amounty su estado. Lo glosado no se puede cobrar hasta que el pagador levante la glosa.
Registrar el pago del pagador (EPS) #
Cuando la EPS gira, registra el pago sobre la factura. method es uno de los medios del pagador:
TRANSFERENCIA, CONSIGNACION, GIRO_DIRECTO o RETENCION (los medios del paciente —billetera,
PSE, efectivo— no van por aquí). Solo se aceptan pagos sobre facturas validadas (CUV_OK) o
radicadas/glosadas, y nunca por encima del saldo no glosado.
Este registro es idempotente dos veces: por Idempotency-Key en el gateway y, aunque no la
envíes, por la llave natural medio + referencia en el motor. El mismo giro registrado dos veces
responde 200 con created: false y no duplica el recaudo. Usa como reference el número de giro o
de transferencia que trae el soporte del pagador.
tx = client.health.billing.register_payment(
"inv-C-53500-…", # id de la factura
amount="53500",
method="TRANSFERENCIA",
reference="GIRO-778899",
paid_at="2026-10-27",
idempotency_key="horizonte-salud-giro-TRANSFERENCIA-GIRO-778899",
)
pago = tx.data
print(pago["created"], pago["invoice"]["status"], pago["invoice"]["balance"])const tx = await client.health.billing.registerPayment("inv-C-53500-…", {
amount: "53500",
method: "TRANSFERENCIA",
reference: "GIRO-778899",
paidAt: "2026-10-27",
idempotencyKey: "horizonte-salud-giro-TRANSFERENCIA-GIRO-778899",
});
console.log(tx.data.created, tx.data.invoice);$tx = $client->health->billing->registerPayment(
'inv-C-53500-…',
amount: '53500',
method: 'TRANSFERENCIA',
reference: 'GIRO-778899',
paidAt: '2026-10-27',
idempotencyKey: 'horizonte-salud-giro-TRANSFERENCIA-GIRO-778899',
);
echo var_export($tx->data['created'], true), ' ', $tx->data['invoice']['balance'], PHP_EOL;Transaction tx = await client.Health.Billing.RegisterPaymentAsync(
"inv-C-53500-…", "53500", "TRANSFERENCIA", "GIRO-778899", paidAt: "2026-10-27",
idempotencyKey: "horizonte-salud-giro-TRANSFERENCIA-GIRO-778899");
Console.WriteLine($"{tx.Data["created"]} {tx.Data["invoice"]!["balance"]}");tx, err := client.Health.Billing.RegisterPayment(ctx, "inv-C-53500-…", &compense.HealthPaymentParams{
Amount: "53500", Method: "TRANSFERENCIA", Reference: "GIRO-778899", PaidAt: "2026-10-27",
}, compense.WithIdempotencyKey("horizonte-salud-giro-TRANSFERENCIA-GIRO-778899"))
if err != nil {
log.Fatal(err)
}
fmt.Println(tx.Data["created"], tx.Data["invoice"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/health-services/billing/invoices/$FACTURA/payments/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-salud-giro-TRANSFERENCIA-GIRO-778899" \
-H "Content-Type: application/json" \
-d '{"amount": "53500", "method": "TRANSFERENCIA", "reference": "GIRO-778899", "paid_at": "2026-10-27"}'{"id": "hpy-E-…", "payment": {"id": "hpy-E-…", "amount": "53500.00", "method": "TRANSFERENCIA",
"reference": "GIRO-778899"}, "created": true,
"invoice": {"id": "inv-C-53500-…", "number": "FES4821", "status": "CUV_OK",
"paid_amount": "53500.00", "balance": "0.00"}}Para el flujo de punta a punta (de la cita al recaudo), sigue la receta Cita médica y recaudo EPS.
Carné de vacunación #
Lectura del esquema del paciente: patient (con fecha de nacimiento y edad), summary, applied
(dosis aplicadas o antecedentes), overdue (atrasadas), upcoming (próximas), pending y los
schemes vigentes de la IPS. Cada consulta queda en la auditoría clínica con el canal gateway.
carne = client.health.vaccination.card("1000000001")
print(carne["summary"])
for dosis in carne["overdue"]:
print("Atrasada:", dosis["biological_name"], dosis["label"], dosis["suggested_date"])const carne = await client.health.vaccination.card("1000000001");
console.log(carne.summary, carne.overdue);$carne = $client->health->vaccination->card('1000000001');
foreach ($carne['overdue'] as $dosis) {
echo 'Atrasada: ', $dosis['biological_name'], ' ', $dosis['label'], PHP_EOL;
}JsonObject carne = await client.Health.Vaccination.CardAsync("1000000001");
Console.WriteLine(carne["summary"]);carne, err := client.Health.Vaccination.Card(ctx, "1000000001")
if err != nil {
log.Fatal(err)
}
fmt.Println(carne["summary"], carne["overdue"])curl "https://sandbox.dev.compense.co/gw/v1/gateway/health-services/vaccination/card/?document=1000000001" \
-H "X-API-Key: $COMPENSE_API_KEY"Errores frecuentes #
| HTTP | Código del motor | Cuándo |
|---|---|---|
401 | — | Falta X-API-Key |
403 | — | Scope insuficiente, IP fuera de la lista, entidad sin salud o vacunación deshabilitada |
404 | HEALTH_PATIENT_NOT_FOUND | El documento no es paciente de la IPS (reenvía con new_patient) |
404 | HEALTH_SLOT_NOT_FOUND / HEALTH_APPOINTMENT_NOT_FOUND / HEALTH_INVOICE_NOT_FOUND | Cupo, cita o factura inexistente en la entidad |
409 | SLOT_NOT_AVAILABLE | El cupo ya fue tomado |
409 | — | La misma Idempotency-Key sigue en curso |
400 / 422 | HEALTH_REASON_REQUIRED, HEALTH_STATUS_NOT_ALLOWED… | Fecha pasada, motivo corto, asistencia antes de la hora |
422 | HEALTH_PAYMENT_METHOD / PAYMENT_EXCEEDS_BALANCE | Medio que no es de pagador, o pago por encima del saldo no glosado |
429 | — | Límite por minuto o cuota mensual (Retry-After) |
502 / 504 | — | El motor falló o no respondió: reintenta con la misma Idempotency-Key |
En los rechazos, detail trae el mensaje del motor en español (por ejemplo «Ese cupo ya no está
disponible: alguien lo acaba de tomar. Elija otro.»). Ver Errores.
Eventos #
Se emiten en la misma transacción que el cambio, con el sobre {event, version, tenant_id, occurred_at, resource}. resource nunca lleva el documento completo ni el nombre del paciente.
Servicios de salud #
| Evento | Desde | Cuándo se emite |
|---|---|---|
appointment.booked | 2026-10-27 | Se agendó una cita por el gateway. |
appointment.cancelled | 2026-10-27 | Se canceló una cita y se liberó el cupo. |
appointment.rescheduled | 2026-10-27 | Se movió una cita a otro cupo (previous trae la fecha y hora anteriores). |
appointment.status_changed | 2026-10-27 | Se registró la asistencia (completed o no_show). |
health_billing.payment.registered | 2026-10-27 | Se aplicó un pago del pagador (EPS) a una factura de salud. |
appointment.rescheduledtrae enpreviousla fecha y hora anteriores.appointment.status_changedllega al registrar la asistencia (completedono_show).health_billing.payment.registeredllega cuando un pago del pagador se aplica a una factura. Concilia por la referencia del giro y deduplica porX-Webhook-Id(Reintentos de webhooks).
Sandbox #
Con una llave de sandbox las respuestas son simuladas y deterministas (nunca tocan la agenda ni la cartera reales). El estado viaja en el id:
- Cupos
slt-<médico>-<AAAAMMDD>-<HHMM>(médicosD1yD2); citasaps-…; facturasinv-<R|C>-<neto>-<hash>: unaRADICADAde $120.000 y unaCUV_OKde $53.500. - Documento
9999999999→ paciente inexistente (404); connew_patientse «registra». - Un cupo con minuto
40(por ejemploslt-D1-20261103-0840) →409 SLOT_NOT_AVAILABLE. - Pago mayor que el neto →
422 PAYMENT_EXCEEDS_BALANCE; medio que no es de pagador →422. - Asistencia sobre una cita futura →
422; sobre una pasada →200. - La disponibilidad devuelve cupos del día pedido (o de mañana) para
D1yD2.