Implementation guide
Health (IPS)
For healthcare providers (IPS): catalog and availability; book, cancel, reschedule and record attendance on the patient’s behalf; health invoices with DIAN status, XML, RIPS and glosas (payer objections); payer (EPS) payments and vaccination card.
On this page
This guide is for the technical team of a healthcare provider (IPS) (or an entity (organization) that has the health module enabled) integrating its system with Compense server-to-server: appointment scheduling, health billing and the vaccination card. It’s the same gateway and the same security model as loans or the wallet. The examples use IPS Horizonte Salud, which books appointments from its call center and reconciles EPS (health insurer) remittances from its accounts receivable system.
client.health is not client.system.health()
client.health (client.Health in .NET and in Go) is this health services module: routes under
/v1/gateway/health-services/.... client.system.health() is something else: it checks your API key
and returns its scopes (GET /v1/gateway/health/). Don’t mix them up when reading logs or granting
permissions.
Before you start #
Who can use it #
Only IPS-type entities or entities with the health module enabled. Any other entity gets a
403 with «La entidad no tiene habilitados los servicios de salud…» (“The entity does not have
health services enabled…”). The vaccination card additionally requires vaccination to be enabled in
the IPS settings (otherwise, 403 «La vacunación no está habilitada en la entidad.» — “Vaccination
is not enabled for the entity.”).
Scopes #
| Scope | Allows |
|---|---|
appointments:read | Catalog, availability, a patient’s appointments and appointment details |
appointments:write | Book, cancel, reschedule and record attendance |
health_billing:read | Invoices, details, XML, RIPS and glosas |
health_billing:collect | Record the payer’s payment (collection) against an invoice |
vaccination:read | A patient’s vaccination card and schedule |
health_billing:collect is on the list of scopes that will require request signing once that
protection is turned on for your API key.
The patient #
The patient is identified by document: the ID number of the primary member or a beneficiary in
their family group (query parameter on reads, request body on writes). In responses, the name and the
document are masked (L***, 103****89); the stable identifier for matching against your system
is patient.id, and the medical record number is medical_record_number.
The medical record is not exposed through this gateway: clinical data exchange between institutions goes through Compense’s B2B FHIR channel, with its own consent and credentials.
Operations #
Reads return the engine’s response body; writes return the standard envelope
({"status": "success", "transaction_id": "…", "module": "health", "data": {…}}). Always send an
Idempotency-Key on writes: a retry with the same key returns the original result and never books
or charges twice.
Catalog and availability #
The catalog returns specialties, services, locations, practitioners, appointment types
(first_time, follow_up, control), consultation types (presencial, telemedicina) and channels
(recepcion, telefono). With those ids you query the open slots for a single day (date), for a
range (start_date/end_date) or the soonest ones (asap=true), filtering by doctor_id,
location_id, specialty_id or service_id. Availability is paginated by page number.
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"}}]}You can’t query or book a past date (400).
Booking on the patient’s behalf #
sequenceDiagram
autonumber
participant CC as IPS Horizonte Salud call center
participant G as Compense Gateway
CC->>G: GET availability (date, doctor_id)
G-->>CC: open slots (slot_id)
CC->>G: POST appointments {document, slot_id} + Idempotency-Key
alt new patient
G-->>CC: 404 HEALTH_PATIENT_NOT_FOUND
CC->>G: POST appointments {document, slot_id, new_patient} + a different key
end
alt slot taken between the query and the booking
G-->>CC: 409 SLOT_NOT_AVAILABLE → query availability again
else
G-->>CC: 201 appointment confirmed (id, appointment_code)
G-)CC: webhook appointment.booked
endThe appointment is created by the gateway’s service user and follows the same rules as the front
desk: the slot is locked, there’s only one active appointment per slot, the patient can’t have
overlapping appointments, and nothing is ever booked in a past slot. channel is recepcion
(the default) or telefono.
New patient. If the document doesn’t exist at the IPS, the response is 404
(HEALTH_PATIENT_NOT_FOUND). Resend with new_patient and the patient is registered without an app
account: document_type, first_name, last_name, birth_date, gender (M, F or O),
phone and, optionally, email. Use a different Idempotency-Key for that second attempt: the
body is different.
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="Check-up")
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 # a different 404: the slot doesn't exist
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: "Check-up",
};
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: 'Check-up',
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: 'Check-up',
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: "Check-up", 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: "Check-up", 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: "Check-up",
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": "Check-up"}'
# New patient: the same body with new_patient and a different 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": "Check-up",
"new_patient": {"document_type": "CC", "first_name": "Laura", "last_name": "Gómez",
"birth_date": "1990-04-12", "gender": "F", "phone": "3001234567"}}'Response (data, abridged):
{"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"}}If the slot was taken between your query and the booking, the response is 409 with
SLOT_NOT_AVAILABLE: query availability again and offer another time.
Patient appointments and details #
appointments.list returns the patient’s appointments (when: upcoming, past or all;
status, for example confirmed,cancelled). The details endpoint accepts the appointment id or its
CITA-AAAA-NNNNNN code.
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"Canceling and rescheduling #
Canceling frees the slot. A reason is required (at least 5 characters). The IPS cancels as its
own staff, without the affiliate’s 2-hour rule; if your system cancels at the patient’s request
(for example, from your own portal), send on_behalf_of_patient: true and the patient’s rules apply
(status and 2 hours’ notice). Canceling an appointment that’s already canceled returns 200 with
already_cancelled: true.
Rescheduling moves the appointment to another open slot under the app’s rules: at least 2 hours before the current appointment, with the new slot open and not in the past; the original slot is freed if it was more than one hour away. The rescheduled appointment may have a different id: store the one returned in the response.
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"] # may change
client.health.appointments.cancel(
cita_id, reason="The patient cannot attend",
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); // may change
await client.health.appointments.cancel(citaId, {
reason: "The patient cannot attend",
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']; // may change
$client->health->appointments->cancel(
$citaId, reason: 'The patient cannot attend',
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>(); // may change
await client.Health.Appointments.CancelAsync(
citaId, "The patient cannot attend", 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) // may change
if _, err := client.Health.Appointments.Cancel(ctx, nuevoID, &compense.CancelAppointmentParams{
Reason: "The patient cannot attend",
}, 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": "The patient cannot attend"}'Recording attendance #
Starting at the appointment time, record whether the patient attended (completed) or not
(no_show). This is only allowed from confirmed or pending, and the change is logged in the
appointment’s status history along with the reason.
stateDiagram-v2
[*] --> confirmed: book
confirmed --> confirmed: reschedule (another slot)
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="Did not show up",
idempotency_key=f"horizonte-salud-asistencia-{cita_id}",
)await client.health.appointments.setAttendance(citaId, {
status: "no_show",
reason: "Did not show up",
idempotencyKey: `horizonte-salud-asistencia-${citaId}`,
});$client->health->appointments->setAttendance(
$citaId, status: 'no_show', reason: 'Did not show up',
idempotencyKey: "horizonte-salud-asistencia-{$citaId}",
);await client.Health.Appointments.SetAttendanceAsync(
citaId, "no_show", reason: "Did not show up", idempotencyKey: $"horizonte-salud-asistencia-{citaId}");if _, err := client.Health.Appointments.SetAttendance(ctx, nuevoID, &compense.AttendanceParams{
Status: "no_show", Reason: "Did not show up",
}, 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": "Did not show up"}'Recording attendance for a future appointment returns 422 («La asistencia se registra a partir de
la hora de la cita.» — “Attendance can be recorded starting at the appointment time.”).
Health billing #
Through the API, billing is read-only, plus one safe action: recording the payer’s payment. Issuing, voiding, submitting and responding to glosas are not exposed in this phase: issuing and voiding send documents to the DIAN (an invoice or a credit note), and voiding with a credit note requires human review; those are done from the IPS dashboard.
Status (status) | What it means |
|---|---|
CUV_OK | Electronic invoice validated, with its unique validation code (CUV) |
RADICADA | Submitted to the payer; includes radication_number |
PAGADA | The payer covered the balance |
The status filter accepts a comma-separated list (CUV_OK,RADICADA); there are also payer, q,
document, date_from and 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"]) # lines, payments, glosas, submissions, notes
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 if not generated yet)
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"- The XML is delivered inside a JSON object (
xml,filename,content_type,xml_hash). If the invoice doesn’t have an electronic document yet,422. - RIPS are returned as a preview (
preview: true) until they’ve been generated; without a DIAN number,422. - A glosa (payer objection) includes
glosa_type,concept_code,amountand its status. The disputed amount can’t be collected until the payer lifts the glosa.
Recording the payer’s (EPS) payment #
When the EPS sends a remittance, record the payment against the invoice. method is one of the
payer’s payment methods: TRANSFERENCIA, CONSIGNACION, GIRO_DIRECTO or RETENCION (patient
payment methods—wallet, PSE, cash—don’t go through here). Payments are only accepted on validated
(CUV_OK) or submitted/objected invoices, and never above the balance not under glosa.
This operation is idempotent twice over: by Idempotency-Key at the gateway and, even if you
don’t send one, by the natural key method + reference in the engine. The same remittance recorded
twice returns 200 with created: false and doesn’t duplicate the collection. Use the remittance or
transfer number from the payer’s remittance advice as the reference.
tx = client.health.billing.register_payment(
"inv-C-53500-…", # invoice id
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"}}For the end-to-end flow (from appointment to collection), follow the recipe Medical appointment and EPS collection.
Vaccination card #
Reads the patient’s vaccination schedule: patient (with date of birth and age), summary,
applied (doses administered or prior history), overdue, upcoming, pending and the IPS’s
current schemes. Every query is logged in the clinical audit trail with the gateway channel.
carne = client.health.vaccination.card("1000000001")
print(carne["summary"])
for dosis in carne["overdue"]:
print("Overdue:", 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 'Overdue: ', $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"Common errors #
| HTTP | Engine code | When |
|---|---|---|
401 | — | Missing X-API-Key |
403 | — | Insufficient scope, IP not on the allowlist, entity without health, or vaccination disabled |
404 | HEALTH_PATIENT_NOT_FOUND | The document isn’t a patient of the IPS (resend with new_patient) |
404 | HEALTH_SLOT_NOT_FOUND / HEALTH_APPOINTMENT_NOT_FOUND / HEALTH_INVOICE_NOT_FOUND | Slot, appointment or invoice doesn’t exist in the entity |
409 | SLOT_NOT_AVAILABLE | The slot has already been taken |
409 | — | The same Idempotency-Key is still in progress |
400 / 422 | HEALTH_REASON_REQUIRED, HEALTH_STATUS_NOT_ALLOWED… | Past date, reason too short, attendance before the appointment time |
422 | HEALTH_PAYMENT_METHOD / PAYMENT_EXCEEDS_BALANCE | Not a payer payment method, or payment above the balance not under glosa |
429 | — | Per-minute limit or monthly quota (Retry-After) |
502 / 504 | — | The engine failed or didn’t respond: retry with the same Idempotency-Key |
On rejections, detail carries the engine’s message in Spanish (for example «Ese cupo ya no está
disponible: alguien lo acaba de tomar. Elija otro.» — “That slot is no longer available: someone just
took it. Choose another one.”). See Errors.
Events #
Events are emitted in the same transaction as the change, with the envelope {event, version, tenant_id, occurred_at, resource}. resource never includes the full document number or the
patient’s name.
Health services #
| Event | Since | When it's emitted |
|---|---|---|
appointment.booked | 2026-10-27 | An appointment was booked through the gateway. |
appointment.cancelled | 2026-10-27 | An appointment was cancelled and its slot was released. |
appointment.rescheduled | 2026-10-27 | An appointment was moved to another slot (previous carries the previous date and time). |
appointment.status_changed | 2026-10-27 | Attendance was recorded (completed or no_show). |
health_billing.payment.registered | 2026-10-27 | A payer (EPS) payment was applied to a health invoice. |
appointment.rescheduledincludes the previous date and time inprevious.appointment.status_changedarrives when attendance is recorded (completedorno_show).health_billing.payment.registeredarrives when a payer payment is applied to an invoice. Reconcile by the remittance reference and deduplicate byX-Webhook-Id(Webhook retries).
Sandbox #
With a sandbox API key, responses are simulated and deterministic (they never touch the real schedule or accounts receivable). The state travels in the id:
- Slots
slt-<doctor>-<AAAAMMDD>-<HHMM>(doctorsD1andD2); appointmentsaps-…; invoicesinv-<R|C>-<net>-<hash>: oneRADICADAfor $120,000 and oneCUV_OKfor $53,500. - Document
9999999999→ nonexistent patient (404); withnew_patientit gets “registered”. - A slot with minute
40(for exampleslt-D1-20261103-0840) →409 SLOT_NOT_AVAILABLE. - Payment greater than the net amount →
422 PAYMENT_EXCEEDS_BALANCE; non-payer payment method →422. - Attendance on a future appointment →
422; on a past one →200. - Availability returns slots for the requested day (or tomorrow) for
D1andD2.