IPS Horizonte Salud, a healthcare provider (IPS), books appointments from its call center and its portal, and reconciles the transfers it receives from each EPS (health insurer) in its accounts receivable system. In this recipe you connect that journey to Compense: you find a slot, book on the patient’s behalf, send a reminder, handle cancellations and reschedules, record attendance, locate the invoice for the visit and register the EPS payment without duplicating it. The details of each operation are in the Health guide.

sequenceDiagram
    autonumber
    participant S as IPS Horizonte Salud system
    participant G as Compense Gateway
    participant P as IPS panel
    S->>G: availability (specialty, date)
    S->>G: book {document, slot_id} + Idempotency-Key
    G-)S: appointment.booked
    S->>G: appointments.list(when=upcoming) → reminder 24 h before
    opt the patient can't make it
        S->>G: reschedule / cancel
        G-)S: appointment.rescheduled / appointment.cancelled
    end
    S->>G: status completed / no_show
    G-)S: appointment.status_changed
    P->>P: billing issues the invoice (DIAN) and submits it to the EPS
    S->>G: billing.list_invoices(document, status=CUV_OK,RADICADA) + XML + RIPS
    S->>G: billing.list_glosas(invoice)
    S->>G: register_payment (method + transfer reference)
    G-)S: health_billing.payment.registered

Requirements #

  • An API key with appointments:read, appointments:write, health_billing:read and health_billing:collect.
  • A webhook endpoint subscribed to appointment.booked, appointment.cancelled, appointment.rescheduled, appointment.status_changed and health_billing.payment.registered.

The examples create the client in step 1 and reuse it; in Go they assume ctx and client created as in the quickstart.

1. Find a slot and book #

Check the day’s availability (or asap=true for the first open slots) and book the one the patient picks. Between your query and the booking, another channel may take the slot: in that case the response is 409 with SLOT_NOT_AVAILABLE (type …/upstream-error) and you move on to the next one. Use one Idempotency-Key per request and slot, so that a network retry of the same attempt doesn’t book twice.

If the patient doesn’t exist at the IPS, the response is 404 HEALTH_PATIENT_NOT_FOUND: resend with new_patient (see Book on the patient’s behalf).

import os
from compense_gateway import CompenseClient, IdempotencyConflictError

client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
PACIENTE = "1000000001"
SOLICITUD = "HORIZONTE-SAL-2026-00311"      # request id in your system

cupos = client.health.availability(date="2026-11-03")
cita = None
for cupo in cupos.results:
    try:
        tx = client.health.appointments.book(
            document=PACIENTE, slot_id=cupo["id"], appointment_type="first_time",
            consultation_type="presencial", channel="telefono", reason="Lower back pain",
            idempotency_key=f"{SOLICITUD}-{cupo['id']}",
        )
        cita = tx.data
        break
    except IdempotencyConflictError as e:
        if e.problem.type.endswith("/idempotency-conflict"):
            raise                                   # your previous attempt is still in progress
        continue                                    # SLOT_NOT_AVAILABLE: next slot

print(cita["id"], cita["appointment_code"], cita["date"], cita["start_time"])

Store in your system the appointment’s id, its appointment_code (CITA-AAAA-NNNNNN, the one you read out to the patient) and the patient.id (the patient’s stable identifier; the document and the name come back masked).

2. Remind, cancel or reschedule #

Compense doesn’t send reminders through the API: your system schedules them. Once a day, list your patients’ upcoming appointments with appointments.list(document, when="upcoming") (or keep your calendar up to date with the appointment.* webhooks) and send the reminder through your own channel.

If the patient can’t attend:

  • Rescheduling requires at least 2 hours’ notice before the current appointment and a new open slot; the original slot is released if there was more than an hour to go. The rescheduled appointment may get a new id: update the one you stored.
  • Cancelling releases the slot and requires a reason of at least 5 characters. If the cancellation comes from the patient (for example in your portal), send on_behalf_of_patient: true to apply the patient rules (including the 2-hour one). Repeating a cancellation responds 200 with already_cancelled: true.
proximas = client.health.appointments.list(PACIENTE, when="upcoming")
for c in proximas.results:
    enviar_recordatorio(PACIENTE, c["appointment_code"], c["date"], c["start_time"])  # your channel

tx = client.health.appointments.reschedule(
    cita["id"], new_slot_id="slt-D2-20261104-1000",
    idempotency_key=f"{SOLICITUD}-reprograma-1",
)
cita = tx.data                                     # the id may change

# Or cancel at the patient's request from the portal:
# client.health.appointments.cancel(cita["id"], reason="Patient is traveling",
#                                   on_behalf_of_patient=True, idempotency_key=f"{SOLICITUD}-cancela")

3. Record attendance #

From the appointment time onward, your admissions system marks whether the patient attended (completed) or not (no_show). Before that time the response is 422. Attendance is the basis for billing the visit (and, if the patient didn’t show up, for your no-show metrics).

client.health.appointments.set_attendance(
    cita["id"], status="completed", idempotency_key=f"{SOLICITUD}-asistencia",
)

4. Locate the invoice and its supporting documents #

Issuing the electronic invoice, voiding it and submitting it to the EPS are done in the IPS panel (they send documents to the DIAN and require human review). Through the API you locate the patient’s invoice and its status, and download the XML and the RIPS for your records or for the submission. Before you collect, check the glosas (billing objections): any amount under a glosa isn’t paid until the EPS lifts it.

facturas = client.health.billing.list_invoices(document=PACIENTE, status="CUV_OK,RADICADA")
factura = facturas.results[0]
print(factura["number"], factura["status"], factura["net_amount"], factura["balance"],
      factura.get("radication_number"))

xml = client.health.billing.get_invoice_xml(factura["id"])
rips = client.health.billing.get_invoice_rips(factura["id"])
glosas = client.health.billing.list_glosas(invoice=factura["id"])
glosado = sum(float(g["amount"]) for g in glosas.results)
cobrable = float(factura["balance"]) - glosado

5. Register the EPS payment #

When the EPS transfer arrives (you see it in your bank statement or in the payer’s remittance report), register it against the invoice. The registration is protected against duplicates twice:

  • by the Idempotency-Key at the gateway, and
  • by the engine’s natural key: method + reference. The same transfer registered again (even if it arrives with another key, from another process or from the panel) responds 200 with created: false and isn’t counted twice.

That’s why the Idempotency-Key should also come from the natural key (<method>-<reference>), and reference should be the transfer number exactly as the EPS reports it. method is TRANSFERENCIA, CONSIGNACION, GIRO_DIRECTO or RETENCION; the amount can never exceed the balance not under a glosa (422).

giro = {"metodo": "TRANSFERENCIA", "referencia": "GIRO-778899", "valor": "53500", "fecha": "2026-10-27"}

tx = client.health.billing.register_payment(
    factura["id"], amount=giro["valor"], method=giro["metodo"],
    reference=giro["referencia"], paid_at=giro["fecha"],
    idempotency_key=f"horizonte-eps-{giro['metodo']}-{giro['referencia']}",
)
r = tx.data
if not r["created"]:
    print("That transfer was already registered")
print(r["invoice"]["status"], r["invoice"]["paid_amount"], r["invoice"]["balance"])

A single EPS transfer usually covers several invoices. Register it invoice by invoice with the same reference and the amount applied to each one; the natural key keeps a reprocessing of the payments file from duplicating any of them.

6. Listen to the webhooks #

With these events you keep your calendar and your accounts receivable in sync even when changes come from the panel or from another channel. None of them carries the full document or the patient’s name: match by ids.

EventWhat to do in your system
appointment.bookedRecord the appointment (including those booked from other channels)
appointment.rescheduledMove the appointment; previous carries the previous date and time
appointment.cancelledRelease the appointment and cancel the reminder
appointment.status_changedMark attendance (completed) or no-show (no_show)
health_billing.payment.registeredApply the collection in your accounts receivable
Webhook receiver
from compense_gateway.webhooks import verify_webhook, WebhookVerificationError

def recibir(raw_body: bytes, headers) -> int:
    try:
        ev = verify_webhook(raw_body, headers, secret=os.environ["COMPENSE_WEBHOOK_SECRET"])
    except WebhookVerificationError:
        return 401
    if ev.is_test or ya_procesado(ev.event_id):
        return 200
    if ev.event.startswith("appointment."):
        sincronizar_cita(ev.event, ev.resource)            # your logic
    elif ev.event == "health_billing.payment.registered":
        aplicar_recaudo(ev.resource)                        # your logic
    registrar_procesado(ev.event_id)
    return 200

Try it in the sandbox #

To testDo this
BookingSlots slt-D1-<AAAAMMDD>-0800, -0820, -1000… from the availability
Slot already takenA slot with minute 40 (for example slt-D1-20261103-0840) → 409 SLOT_NOT_AVAILABLE
New patientDocument 9999999999 → 404; resend with new_patient
AttendanceOn a future appointment → 422; on a past one → 200
InvoicesOne RADICADA for $120,000 (with a $12,000 glosa) and one CUV_OK for $53,500
Payment that exceeds the balanceMore than the invoice’s net amount → 422 PAYMENT_EXCEEDS_BALANCE
Invalid methodA method that isn’t a payer method (for example PSE) → 422

The sandbox keeps no state (a rescheduled appointment goes back to its initial state when you look it up) and doesn’t emit the real webhooks: validate your receiver with test events (Testing).

Checklist #

  • On 409 SLOT_NOT_AVAILABLE you offer another slot; on 404 HEALTH_PATIENT_NOT_FOUND you register the patient with new_patient.
  • You store the id, the appointment_code and the patient.id, and update the id after a reschedule.
  • Reminders come from your system using appointments.list(when="upcoming") or the webhooks.
  • You record attendance only from the appointment time onward.
  • You check glosas before collecting and never register more than the balance not under a glosa.
  • The EPS payment uses reference = transfer number and a key derived from method + reference.