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 #

ScopeAllows
appointments:readCatalog, availability, a patient’s appointments and appointment details
appointments:writeBook, cancel, reschedule and record attendance
health_billing:readInvoices, details, XML, RIPS and glosas
health_billing:collectRecord the payer’s payment (collection) against an invoice
vaccination:readA 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 #

OperationRouteScope
CatalogGET/v1/gateway/health-services/catalog/appointments:read
AvailabilityGET/v1/gateway/health-services/availability/appointments:read
Patient appointmentsGET/v1/gateway/health-services/appointments/appointments:read
BookPOST/v1/gateway/health-services/appointments/appointments:write
Appointment detailsGET/v1/gateway/health-services/appointments/{appointment_id}/appointments:read
CancelPOST/v1/gateway/health-services/appointments/{appointment_id}/cancel/appointments:write
ReschedulePOST/v1/gateway/health-services/appointments/{appointment_id}/reschedule/appointments:write
AttendancePOST/v1/gateway/health-services/appointments/{appointment_id}/status/appointments:write
InvoicesGET/v1/gateway/health-services/billing/invoices/health_billing:read
Invoice detailsGET/v1/gateway/health-services/billing/invoices/{invoice_id}/health_billing:read
Invoice XMLGET/v1/gateway/health-services/billing/invoices/{invoice_id}/xml/health_billing:read
RIPSGET/v1/gateway/health-services/billing/invoices/{invoice_id}/rips/health_billing:read
Record payer paymentPOST/v1/gateway/health-services/billing/invoices/{invoice_id}/payments/health_billing:collect
GlosasGET/v1/gateway/health-services/billing/glosas/health_billing:read
Vaccination cardGET/v1/gateway/health-services/vaccination/card/vaccination:read

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"])
JSON
{"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
    end

The 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"])

Response (data, abridged):

JSON
{"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"])

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}",
)

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}",
)

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_OKElectronic invoice validated, with its unique validation code (CUV)
RADICADASubmitted to the payer; includes radication_number
PAGADAThe 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"])
  • 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, amount and 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"])
JSON
{"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"])

Common errors #

HTTPEngine codeWhen
401—Missing X-API-Key
403—Insufficient scope, IP not on the allowlist, entity without health, or vaccination disabled
404HEALTH_PATIENT_NOT_FOUNDThe document isn’t a patient of the IPS (resend with new_patient)
404HEALTH_SLOT_NOT_FOUND / HEALTH_APPOINTMENT_NOT_FOUND / HEALTH_INVOICE_NOT_FOUNDSlot, appointment or invoice doesn’t exist in the entity
409SLOT_NOT_AVAILABLEThe slot has already been taken
409—The same Idempotency-Key is still in progress
400 / 422HEALTH_REASON_REQUIRED, HEALTH_STATUS_NOT_ALLOWED…Past date, reason too short, attendance before the appointment time
422HEALTH_PAYMENT_METHOD / PAYMENT_EXCEEDS_BALANCENot 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 #

EventSinceWhen it's emitted
appointment.booked2026-10-27An appointment was booked through the gateway.
appointment.cancelled2026-10-27An appointment was cancelled and its slot was released.
appointment.rescheduled2026-10-27An appointment was moved to another slot (previous carries the previous date and time).
appointment.status_changed2026-10-27Attendance was recorded (completed or no_show).
health_billing.payment.registered2026-10-27A payer (EPS) payment was applied to a health invoice.
  • appointment.rescheduled includes the previous date and time in previous.
  • appointment.status_changed arrives when attendance is recorded (completed or no_show).
  • health_billing.payment.registered arrives when a payer payment is applied to an invoice. Reconcile by the remittance reference and deduplicate by X-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> (doctors D1 and D2); appointments aps-…; invoices inv-<R|C>-<net>-<hash>: one RADICADA for $120,000 and one CUV_OK for $53,500.
  • Document 9999999999 → nonexistent patient (404); with new_patient it gets “registered”.
  • A slot with minute 40 (for example slt-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 D1 and D2.

What’s next #