Receta
Cita médica y recaudo EPS
IPS Horizonte Salud de punta a punta: disponibilidad, agendar en nombre del paciente, recordatorio, cancelar o reprogramar, asistencia, factura con XML y RIPS, pago de la EPS con llave natural y webhooks appointment.* y health_billing.payment.registered.
En esta página
IPS Horizonte Salud agenda citas desde su call center y su portal, y concilia los giros de las EPS desde su sistema de cartera. En esta receta conectas ese recorrido con Compense: buscas un cupo, agendas a nombre del paciente, recuerdas la cita, manejas cancelaciones y reprogramaciones, registras la asistencia, ubicas la factura de la atención y registras el pago de la EPS sin duplicarlo. Los detalles de cada operación están en la guía de Salud.
sequenceDiagram
autonumber
participant S as Sistema de IPS Horizonte Salud
participant G as Gateway Compense
participant P as Panel de la IPS
S->>G: availability (especialidad, fecha)
S->>G: book {document, slot_id} + Idempotency-Key
G-)S: appointment.booked
S->>G: appointments.list(when=upcoming) → recordatorio 24 h antes
opt el paciente no puede
S->>G: reschedule / cancel
G-)S: appointment.rescheduled / appointment.cancelled
end
S->>G: status completed / no_show
G-)S: appointment.status_changed
P->>P: facturación emite la factura (DIAN) y la radica ante la EPS
S->>G: billing.list_invoices(document, status=CUV_OK,RADICADA) + XML + RIPS
S->>G: billing.list_glosas(invoice)
S->>G: register_payment (método + referencia del giro)
G-)S: health_billing.payment.registeredRequisitos #
- Una llave con
appointments:read,appointments:write,health_billing:readyhealth_billing:collect. - Un endpoint de webhooks suscrito a
appointment.booked,appointment.cancelled,appointment.rescheduled,appointment.status_changedyhealth_billing.payment.registered.
Los ejemplos crean el cliente en el paso 1 y lo reutilizan; en Go asumen ctx y client creados
como en el quickstart.
1. Busca un cupo y agenda #
Consulta la disponibilidad del día (o asap=true para los primeros cupos libres) y agenda el que
elija el paciente. Entre tu consulta y el agendamiento otro canal puede tomar el cupo: en ese caso la
respuesta es 409 con SLOT_NOT_AVAILABLE (type …/upstream-error) y pasas al siguiente. Usa
una Idempotency-Key por solicitud y cupo, para que un reintento por red del mismo intento no
agende dos veces.
Si el paciente no existe en la IPS, la respuesta es 404 HEALTH_PATIENT_NOT_FOUND: reenvía con
new_patient (ver Agendar en nombre del paciente).
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" # id de la solicitud en tu sistema
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="Dolor lumbar",
idempotency_key=f"{SOLICITUD}-{cupo['id']}",
)
cita = tx.data
break
except IdempotencyConflictError as e:
if e.problem.type.endswith("/idempotency-conflict"):
raise # tu intento anterior sigue en curso
continue # SLOT_NOT_AVAILABLE: siguiente cupo
print(cita["id"], cita["appointment_code"], cita["date"], cita["start_time"])import { CompenseClient, IdempotencyConflictError } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
const PACIENTE = "1000000001";
const SOLICITUD = "HORIZONTE-SAL-2026-00311"; // id de la solicitud en tu sistema
const cupos = await client.health.availability({ date: "2026-11-03" });
let cita: Record<string, any> | undefined;
for (const cupo of cupos.results) {
try {
const tx = await client.health.appointments.book({
document: PACIENTE,
slotId: String(cupo.id),
appointmentType: "first_time",
consultationType: "presencial",
channel: "telefono",
reason: "Dolor lumbar",
idempotencyKey: `${SOLICITUD}-${cupo.id}`,
});
cita = tx.data;
break;
} catch (err) {
if (err instanceof IdempotencyConflictError && !err.problem.type.endsWith("/idempotency-conflict")) continue; // SLOT_NOT_AVAILABLE
throw err;
}
}
console.log(cita?.id, cita?.appointment_code, cita?.date, cita?.start_time);<?php
require __DIR__ . '/vendor/autoload.php';
use Compense\Sdk\CompenseClient;
use Compense\Sdk\Exception\IdempotencyConflictException;
$client = new CompenseClient(apiKey: getenv('COMPENSE_API_KEY'), environment: 'sandbox');
$paciente = '1000000001';
$solicitud = 'HORIZONTE-SAL-2026-00311'; // id de la solicitud en tu sistema
$cupos = $client->health->availability(date: '2026-11-03');
$cita = null;
foreach ($cupos->results as $cupo) {
try {
$tx = $client->health->appointments->book(
document: $paciente, slotId: $cupo['id'], reason: 'Dolor lumbar',
appointmentType: 'first_time', consultationType: 'presencial', channel: 'telefono',
idempotencyKey: "{$solicitud}-{$cupo['id']}",
);
$cita = $tx->data;
break;
} catch (IdempotencyConflictException $e) {
if (str_ends_with($e->problem->type, '/idempotency-conflict')) {
throw $e; // tu intento anterior sigue en curso
}
// SLOT_NOT_AVAILABLE: siguiente cupo
}
}
echo $cita['id'], ' ', $cita['appointment_code'], ' ', $cita['date'], 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);
const string Paciente = "1000000001";
const string Solicitud = "HORIZONTE-SAL-2026-00311"; // id de la solicitud en tu sistema
Page cupos = await client.Health.AvailabilityAsync(date: "2026-11-03");
JsonObject? cita = null;
foreach (JsonObject cupo in cupos.Results)
{
string slot = cupo["id"]!.GetValue<string>();
try
{
Transaction tx = await client.Health.Appointments.BookAsync(
Paciente, slot, reason: "Dolor lumbar", appointmentType: "first_time",
consultationType: "presencial", channel: "telefono", idempotencyKey: $"{Solicitud}-{slot}");
cita = tx.Data;
break;
}
catch (IdempotencyConflictException e) when (!e.Problem.Type.EndsWith("/idempotency-conflict", StringComparison.Ordinal))
{
// SLOT_NOT_AVAILABLE: siguiente cupo
}
}
Console.WriteLine($"{cita?["id"]} {cita?["appointment_code"]} {cita?["date"]}");const paciente = "1000000001"
const solicitud = "HORIZONTE-SAL-2026-00311" // id de la solicitud en tu sistema
cupos, err := client.Health.Availability(ctx, &compense.AvailabilityParams{Date: "2026-11-03"})
if err != nil {
log.Fatal(err)
}
var cita map[string]any
for _, cupo := range cupos.Results {
slot, _ := cupo["id"].(string)
tx, err := client.Health.Appointments.Book(ctx, &compense.BookAppointmentParams{
Document: paciente, SlotID: slot, Reason: "Dolor lumbar",
AppointmentType: "first_time", ConsultationType: "presencial", Channel: "telefono",
}, compense.WithIdempotencyKey(solicitud+"-"+slot))
var apiErr *compense.APIError
if compense.IsIdempotencyConflict(err) && errors.As(err, &apiErr) &&
!strings.HasSuffix(apiErr.Problem.Type, "/idempotency-conflict") {
continue // SLOT_NOT_AVAILABLE: siguiente cupo
}
if err != nil {
log.Fatal(err)
}
cita = tx.Data
break
}
fmt.Println(cita["id"], cita["appointment_code"], cita["date"])curl "https://sandbox.dev.compense.co/gw/v1/gateway/health-services/availability/?date=2026-11-03" \
-H "X-API-Key: $COMPENSE_API_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-SAL-2026-00311-slt-D1-20261103-0800" \
-H "Content-Type: application/json" \
-d '{"document": "1000000001", "slot_id": "slt-D1-20261103-0800", "appointment_type": "first_time",
"consultation_type": "presencial", "channel": "telefono", "reason": "Dolor lumbar"}'Guarda en tu sistema el id de la cita, su appointment_code (CITA-AAAA-NNNNNN, el que le dictas
al paciente) y el patient.id (identificador estable del paciente; el documento y el nombre vuelven
enmascarados).
2. Recuerda, cancela o reprograma #
Compense no envía recordatorios por API: tu sistema los programa. Una vez al día, lista las citas
próximas de tus pacientes con appointments.list(document, when="upcoming") (o mantén tu agenda al
día con los webhooks appointment.*) y envía el recordatorio por tu canal.
Si el paciente no puede asistir:
- Reprogramar exige al menos 2 horas de anticipación sobre la cita actual y un cupo nuevo libre;
el cupo original se libera si faltaba más de una hora. La cita reprogramada puede cambiar de
id: actualiza el que guardaste. - Cancelar libera el cupo y exige un motivo de al menos 5 caracteres. Si la cancelación nace del
paciente (por ejemplo en tu portal), envía
on_behalf_of_patient: truepara aplicar sus reglas (incluida la de 2 horas). Repetir una cancelación responde200conalready_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"]) # tu canal
tx = client.health.appointments.reschedule(
cita["id"], new_slot_id="slt-D2-20261104-1000",
idempotency_key=f"{SOLICITUD}-reprograma-1",
)
cita = tx.data # el id puede cambiar
# O cancelar a pedido del paciente desde el portal:
# client.health.appointments.cancel(cita["id"], reason="Viaje del paciente",
# on_behalf_of_patient=True, idempotency_key=f"{SOLICITUD}-cancela")const proximas = await client.health.appointments.list(PACIENTE, { when: "upcoming" });
for (const c of proximas.results) enviarRecordatorio(PACIENTE, c.appointment_code, c.date, c.start_time); // tu canal
const rep = await client.health.appointments.reschedule(String(cita!.id), {
newSlotId: "slt-D2-20261104-1000",
idempotencyKey: `${SOLICITUD}-reprograma-1`,
});
cita = rep.data; // el id puede cambiar
// O cancelar a pedido del paciente desde el portal:
// await client.health.appointments.cancel(String(cita.id), { reason: "Viaje del paciente", onBehalfOfPatient: true, idempotencyKey: `${SOLICITUD}-cancela` });
$proximas = $client->health->appointments->list($paciente, when: 'upcoming');
foreach ($proximas->results as $c) {
enviarRecordatorio($paciente, $c['appointment_code'], $c['date'], $c['start_time']); // tu canal
}
$rep = $client->health->appointments->reschedule(
$cita['id'], newSlotId: 'slt-D2-20261104-1000', idempotencyKey: "{$solicitud}-reprograma-1",
);
$cita = $rep->data; // el id puede cambiar
// O cancelar a pedido del paciente desde el portal:
// $client->health->appointments->cancel($cita['id'], reason: 'Viaje del paciente',
// onBehalfOfPatient: true, idempotencyKey: "{$solicitud}-cancela");
Page proximas = await client.Health.Appointments.ListAsync(Paciente, when: "upcoming");
foreach (JsonObject c in proximas.Results)
EnviarRecordatorio(Paciente, c["appointment_code"]!.ToString(), c["date"]!.ToString(), c["start_time"]!.ToString());
Transaction rep = await client.Health.Appointments.RescheduleAsync(
cita!["id"]!.GetValue<string>(), "slt-D2-20261104-1000", idempotencyKey: $"{Solicitud}-reprograma-1");
cita = rep.Data; // el id puede cambiar
// O cancelar a pedido del paciente desde el portal:
// await client.Health.Appointments.CancelAsync(cita["id"]!.GetValue<string>(), "Viaje del paciente",
// onBehalfOfPatient: true, idempotencyKey: $"{Solicitud}-cancela");proximas, err := client.Health.Appointments.List(ctx, paciente, &compense.AppointmentListParams{When: "upcoming"})
if err != nil {
log.Fatal(err)
}
for _, c := range proximas.Results {
enviarRecordatorio(paciente, c["appointment_code"], c["date"], c["start_time"]) // tu canal
}
citaID, _ := cita["id"].(string)
rep, err := client.Health.Appointments.Reschedule(ctx, citaID, &compense.RescheduleAppointmentParams{
NewSlotID: "slt-D2-20261104-1000",
}, compense.WithIdempotencyKey(solicitud+"-reprograma-1"))
if err != nil {
log.Fatal(err)
}
cita = rep.Data // el id puede cambiar
// O cancelar a pedido del paciente desde el portal:
// client.Health.Appointments.Cancel(ctx, citaID, &compense.CancelAppointmentParams{
// Reason: "Viaje del paciente", OnBehalfOfPatient: compense.Bool(true)},
// compense.WithIdempotencyKey(solicitud+"-cancela"))curl "https://sandbox.dev.compense.co/gw/v1/gateway/health-services/appointments/?document=1000000001&when=upcoming" \
-H "X-API-Key: $COMPENSE_API_KEY"
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-SAL-2026-00311-reprograma-1" \
-H "Content-Type: application/json" \
-d '{"new_slot_id": "slt-D2-20261104-1000"}'3. Registra la asistencia #
Desde la hora de la cita, tu sistema de admisiones marca si el paciente asistió (completed) o no
(no_show). Antes de esa hora la respuesta es 422. La asistencia es la base para facturar la
atención (y, si no asistió, para tus indicadores de inasistencia).
client.health.appointments.set_attendance(
cita["id"], status="completed", idempotency_key=f"{SOLICITUD}-asistencia",
)await client.health.appointments.setAttendance(String(cita!.id), {
status: "completed",
idempotencyKey: `${SOLICITUD}-asistencia`,
});$client->health->appointments->setAttendance(
$cita['id'], status: 'completed', idempotencyKey: "{$solicitud}-asistencia",
);await client.Health.Appointments.SetAttendanceAsync(
cita!["id"]!.GetValue<string>(), "completed", idempotencyKey: $"{Solicitud}-asistencia");citaID, _ = cita["id"].(string)
if _, err := client.Health.Appointments.SetAttendance(ctx, citaID, &compense.AttendanceParams{
Status: "completed",
}, compense.WithIdempotencyKey(solicitud+"-asistencia")); 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-SAL-2026-00311-asistencia" \
-H "Content-Type: application/json" \
-d '{"status": "completed"}'4. Ubica la factura y sus soportes #
Emitir la factura electrónica, anularla y radicarla ante la EPS se hace en el panel de la IPS (envían documentos a la DIAN y exigen revisión humana). Por API ubicas la factura del paciente, su estado y descargas el XML y los RIPS para tu archivo o para la radicación. Antes de cobrar, revisa las glosas: lo glosado no se paga hasta que la EPS levante la glosa.
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"]) - glosadoconst facturas = await client.health.billing.listInvoices({ document: PACIENTE, status: "CUV_OK,RADICADA" });
const factura = facturas.results[0];
const facturaId = String(factura.id);
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 });
const glosado = glosas.results.reduce((s, g) => s + Number(g.amount), 0);
const cobrable = Number(factura.balance) - glosado;$facturas = $client->health->billing->listInvoices(status: 'CUV_OK,RADICADA', document: $paciente);
$factura = $facturas->results[0];
$xml = $client->health->billing->getInvoiceXml($factura['id']);
$rips = $client->health->billing->getInvoiceRips($factura['id']);
$glosas = $client->health->billing->listGlosas(invoice: $factura['id']);
$glosado = array_sum(array_map(fn ($g) => (float) $g['amount'], $glosas->results));
$cobrable = (float) $factura['balance'] - $glosado;Page facturas = await client.Health.Billing.ListInvoicesAsync(status: "CUV_OK,RADICADA", document: Paciente);
JsonObject factura = facturas.Results[0];
string facturaId = factura["id"]!.GetValue<string>();
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);
decimal glosado = glosas.Results.Sum(g => decimal.Parse(g["amount"]!.GetValue<string>(), System.Globalization.CultureInfo.InvariantCulture));
decimal cobrable = decimal.Parse(factura["balance"]!.GetValue<string>(), System.Globalization.CultureInfo.InvariantCulture) - glosado;facturas, err := client.Health.Billing.ListInvoices(ctx, &compense.InvoiceListParams{
Document: paciente, Status: "CUV_OK,RADICADA",
})
if err != nil {
log.Fatal(err)
}
factura := facturas.Results[0]
facturaID, _ := factura["id"].(string)
xml, err := client.Health.Billing.GetInvoiceXML(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(factura["number"], factura["balance"], xml["filename"], glosas.Count)curl "https://sandbox.dev.compense.co/gw/v1/gateway/health-services/billing/invoices/?document=1000000001&status=CUV_OK,RADICADA" \
-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"5. Registra el pago de la EPS #
Cuando llega el giro de la EPS (lo ves en tu extracto o en la relación de pagos del pagador), regístralo sobre la factura. El registro está protegido dos veces contra duplicados:
- por la
Idempotency-Keyen el gateway, y - por la llave natural del motor: método + referencia. El mismo giro registrado otra vez
(aunque llegue con otra llave, desde otro proceso o desde el panel) responde
200concreated: falsey no suma dos veces.
Por eso conviene que la Idempotency-Key también salga de la llave natural
(<método>-<referencia>), y que reference sea el número de giro tal como lo reporta la EPS.
method es TRANSFERENCIA, CONSIGNACION, GIRO_DIRECTO o RETENCION; el monto nunca puede
superar el saldo no glosado (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("Ese giro ya estaba registrado")
print(r["invoice"]["status"], r["invoice"]["paid_amount"], r["invoice"]["balance"])const giro = { metodo: "TRANSFERENCIA", referencia: "GIRO-778899", valor: "53500", fecha: "2026-10-27" };
const tx = await client.health.billing.registerPayment(facturaId, {
amount: giro.valor,
method: giro.metodo,
reference: giro.referencia,
paidAt: giro.fecha,
idempotencyKey: `horizonte-eps-${giro.metodo}-${giro.referencia}`,
});
if (!tx.data.created) console.log("Ese giro ya estaba registrado");
console.log(tx.data.invoice);$giro = ['metodo' => 'TRANSFERENCIA', 'referencia' => 'GIRO-778899', 'valor' => '53500', 'fecha' => '2026-10-27'];
$tx = $client->health->billing->registerPayment(
$factura['id'],
amount: $giro['valor'],
method: $giro['metodo'],
reference: $giro['referencia'],
paidAt: $giro['fecha'],
idempotencyKey: "horizonte-eps-{$giro['metodo']}-{$giro['referencia']}",
);
if (!$tx->data['created']) {
echo "Ese giro ya estaba registrado\n";
}
echo $tx->data['invoice']['status'], ' ', $tx->data['invoice']['balance'], PHP_EOL;Transaction pago = await client.Health.Billing.RegisterPaymentAsync(
facturaId, "53500", "TRANSFERENCIA", "GIRO-778899", paidAt: "2026-10-27",
idempotencyKey: "horizonte-eps-TRANSFERENCIA-GIRO-778899");
if (pago.Data["created"]?.GetValue<bool>() == false) Console.WriteLine("Ese giro ya estaba registrado");
Console.WriteLine($"{pago.Data["invoice"]!["status"]} {pago.Data["invoice"]!["balance"]}");pago, err := client.Health.Billing.RegisterPayment(ctx, facturaID, &compense.HealthPaymentParams{
Amount: "53500", Method: "TRANSFERENCIA", Reference: "GIRO-778899", PaidAt: "2026-10-27",
}, compense.WithIdempotencyKey("horizonte-eps-TRANSFERENCIA-GIRO-778899"))
if err != nil {
log.Fatal(err)
}
if creado, _ := pago.Data["created"].(bool); !creado {
fmt.Println("Ese giro ya estaba registrado")
}
fmt.Println(pago.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-eps-TRANSFERENCIA-GIRO-778899" \
-H "Content-Type: application/json" \
-d '{"amount": "53500", "method": "TRANSFERENCIA", "reference": "GIRO-778899", "paid_at": "2026-10-27"}'Un giro de la EPS suele cubrir varias facturas. Regístralo factura por factura con la misma
reference y el valor aplicado a cada una; la llave natural evita que un reproceso del archivo de
pagos duplique alguna.
6. Escucha los webhooks #
Con los eventos mantienes tu agenda y tu cartera sincronizadas aunque los cambios vengan del panel o de otro canal. Ninguno trae el documento completo ni el nombre del paciente: cruza por ids.
| Evento | Qué hacer en tu sistema |
|---|---|
appointment.booked | Registrar la cita (también las agendadas desde otros canales) |
appointment.rescheduled | Mover la cita; previous trae la fecha y hora anteriores |
appointment.cancelled | Liberar la cita y cancelar el recordatorio |
appointment.status_changed | Marcar asistencia (completed) o inasistencia (no_show) |
health_billing.payment.registered | Aplicar el recaudo en tu cartera |
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) # tu lógica
elif ev.event == "health_billing.payment.registered":
aplicar_recaudo(ev.resource) # tu lógica
registrar_procesado(ev.event_id)
return 200import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@compense/sdk/webhooks";
const app = express();
app.post("/compense/webhooks", express.raw({ type: "*/*" }), (req, res) => {
let ev;
try {
ev = verifyWebhook(req.body, req.headers, process.env.COMPENSE_WEBHOOK_SECRET!);
} catch (err) {
if (err instanceof WebhookVerificationError) return res.sendStatus(401);
throw err;
}
if (!ev.isTest && !yaProcesado(ev.eventId)) {
if (ev.event.startsWith("appointment.")) sincronizarCita(ev.event, ev.resource);
else if (ev.event === "health_billing.payment.registered") aplicarRecaudo(ev.resource);
registrarProcesado(ev.eventId);
}
res.sendStatus(200);
});use Compense\Sdk\Webhook\Webhook;
use Compense\Sdk\Webhook\WebhookVerificationException;
try {
$ev = Webhook::verify(file_get_contents('php://input'), getallheaders(), getenv('COMPENSE_WEBHOOK_SECRET'));
} catch (WebhookVerificationException) {
http_response_code(401);
exit;
}
if (!$ev->isTest() && !yaProcesado($ev->eventId)) {
if (str_starts_with($ev->event(), 'appointment.')) {
sincronizarCita($ev->event(), $ev->resource());
} elseif ($ev->event() === 'health_billing.payment.registered') {
aplicarRecaudo($ev->resource());
}
registrarProcesado($ev->eventId);
}
http_response_code(200);using Compense.Sdk.Webhooks;
app.MapPost("/compense/webhooks", async (HttpRequest req) =>
{
using var cuerpo = new MemoryStream();
await req.Body.CopyToAsync(cuerpo);
WebhookEvent ev;
try { ev = CompenseWebhooks.Verify(cuerpo.ToArray(), req.Headers, Environment.GetEnvironmentVariable("COMPENSE_WEBHOOK_SECRET")!); }
catch (WebhookVerificationException) { return Results.Unauthorized(); }
if (!ev.IsTest && !YaProcesado(ev.EventId))
{
if (ev.Event.StartsWith("appointment.", StringComparison.Ordinal)) SincronizarCita(ev.Event, ev.Resource);
else if (ev.Event == "health_billing.payment.registered") AplicarRecaudo(ev.Resource);
RegistrarProcesado(ev.EventId);
}
return Results.Ok();
});import "github.com/Appiasoft/compense-saas-sdk-go/webhook"
secrets := []string{os.Getenv("COMPENSE_WEBHOOK_SECRET")}
http.Handle("/compense/webhooks", webhook.Handler(secrets, func(ctx context.Context, ev *webhook.Event) error {
if ev.IsTest() || yaProcesado(ev.ID) {
return nil
}
switch {
case strings.HasPrefix(ev.Event(), "appointment."):
sincronizarCita(ev.Event(), ev.Resource())
case ev.Event() == "health_billing.payment.registered":
aplicarRecaudo(ev.Resource())
}
registrarProcesado(ev.ID)
return nil
}))curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/endpoints/$ENDPOINT_ID/test/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event_type": "health_billing.payment.registered"}'Pruébalo en el sandbox #
| Para probar | Haz esto |
|---|---|
| Agendar | Cupos slt-D1-<AAAAMMDD>-0800, -0820, -1000… de la disponibilidad |
| Cupo tomado | Un cupo con minuto 40 (por ejemplo slt-D1-20261103-0840) → 409 SLOT_NOT_AVAILABLE |
| Paciente nuevo | Documento 9999999999 → 404; reenvía con new_patient |
| Asistencia | Sobre una cita futura → 422; sobre una pasada → 200 |
| Facturas | Una RADICADA de $120.000 (con una glosa de $12.000) y una CUV_OK de $53.500 |
| Pago que excede | Más que el neto de la factura → 422 PAYMENT_EXCEEDS_BALANCE |
| Medio inválido | method que no sea de pagador (por ejemplo PSE) → 422 |
El sandbox no guarda estado (una cita reprogramada vuelve a su estado inicial al consultarla) y no emite los webhooks reales: valida tu receptor con eventos de prueba (Pruebas).
Lista de verificación #
- Ante
409 SLOT_NOT_AVAILABLEofreces otro cupo; ante404 HEALTH_PATIENT_NOT_FOUNDregistras connew_patient. - Guardas el
id, elappointment_codey elpatient.id, y actualizas el id tras reprogramar. - Los recordatorios salen de tu sistema con
appointments.list(when="upcoming")o los webhooks. - Registras la asistencia solo desde la hora de la cita.
- Revisas glosas antes de cobrar y nunca registras más que el saldo no glosado.
- El pago de la EPS usa
reference= número de giro y una llave derivada de método + referencia.