El módulo de créditos expone el motor de originación de Compense (reglas, scoring y desembolso) a tu sistema: radicas la solicitud de un afiliado, tu analista o el motor la decide y la desembolsas. Cada paso devuelve un transaction_id rastreable y emite un webhook. Lo que pasa después del desembolso (plan de pagos, recaudo, paz y salvo) vive en Cartera.

Antes de empezar, el afiliado debe existir en el registro de tu entidad: ver Afiliados.

Operaciones y scopes #

OperaciónMétodo SDK (Python)Scope
POST/v1/gateway/credits/application/client.credits.submit_application(...)credits:submit
GET/v1/gateway/credits/applications/client.credits.list_applications(...)credits:read
GET/v1/gateway/credits/applications/{app_id}/client.credits.get_application(app_id)credits:read
POST/v1/gateway/credits/applications/{app_id}/decision/client.credits.decide_application(app_id, ...)credits:decide
POST/v1/gateway/credits/applications/{app_id}/disburse/client.credits.disburse_application(app_id)credits:disburse

Separa las llaves por función: el canal que radica (web, oficina, aliado) solo necesita credits:submit; el back-office que decide, credits:read + credits:decide; y el proceso de tesorería que desembolsa, credits:disburse. Nota que la radicación usa la ruta singular /credits/application/ y el resto la plural /credits/applications/.

El flujo completo #

sequenceDiagram
  autonumber
  participant S as Tu sistema
  participant G as Gateway Compense
  participant M as Motor de créditos
  participant W as Tu receptor de webhooks
  S->>G: POST /credits/application/ (Idempotency-Key)
  G->>M: radicar
  M-->>G: application_id, status
  G-->>S: sobre {transaction_id, data.application_id}
  M--)W: credits.application.received
  S->>G: POST /credits/applications/{id}/decision/ (APPROVE)
  G-->>S: sobre {data.status: APPROVED}
  M--)W: credits.decision
  S->>G: POST /credits/applications/{id}/disburse/
  G-->>S: sobre {data.disbursement_id}
  M--)W: credits.disbursed

Estados de la solicitud #

stateDiagram-v2
  [*] --> SUBMITTED: radicar
  SUBMITTED --> IN_REVIEW: revisión manual
  SUBMITTED --> REQUIRES_DOCS: faltan documentos
  REQUIRES_DOCS --> IN_REVIEW
  SUBMITTED --> APPROVED: APPROVE
  IN_REVIEW --> APPROVED: APPROVE
  SUBMITTED --> REJECTED: REJECT o reglas
  IN_REVIEW --> REJECTED: REJECT
  APPROVED --> DISBURSING: desembolsar
  DISBURSING --> DISBURSED
  DISBURSED --> ACTIVE: crédito vivo (pasa a Cartera)
  SUBMITTED --> CANCELLED
  REJECTED --> [*]
  ACTIVE --> [*]
EstadoSignificado
SUBMITTEDRadicada y aceptada para proceso (en el sandbox se reporta como RECEIVED).
IN_REVIEWEn la bandeja de un analista (revisión manual).
REQUIRES_DOCSEl analista pidió documentos al afiliado.
APPROVEDAprobada: lista para desembolsar.
REJECTEDRechazada por un analista o por las reglas del motor (capacidad, mora, políticas).
DISBURSINGSe envió la orden de desembolso al medio de pago.
DISBURSEDFondos desembolsados.
ACTIVECrédito vivo: desde aquí lo operas en Cartera.
CANCELLEDAnulada.

Solo una solicitud APPROVED se puede desembolsar: si no está aprobada, el motor responde 422, y si ya se está desembolsando o ya se desembolsó, 409 (en el sandbox ambos casos responden 409).

1. Radicar la solicitud #

Campo (SDK)En el cuerpoObligatorioNotas
affiliateafiliadosí{document_type, document_number}. En el cable la clave es afiliado: los SDK la llenan desde affiliate / Affiliate.
product_codeproduct_codesíCódigo del producto de crédito de tu entidad (p. ej. LIBRANZA_12M).
amountamountsíMonto solicitado en COP como texto decimal ("3500000"). Ver Montos.
term_monthsterm_monthssíPlazo en meses, de 1 a 120.
external_referenceexternal_referencesíTu id de la solicitud; vuelve en las respuestas y sirve para conciliar.
purposepurposenoDestino del crédito.
extra(se fusiona en el cuerpo)noCampos adicionales que tu motor exija y el SDK no modela.
from compense_gateway import AffiliateRef

tx = client.credits.submit_application(
    affiliate=AffiliateRef("CC", "1000000001"),
    product_code="LIBRANZA_12M",
    amount="3500000",
    term_months=12,
    external_reference="HORIZONTE-CRED-2026-000123",
    purpose="Compra de cartera",
    idempotency_key="horizonte-cred-2026-000123",
)
app_id = tx.data["application_id"]          # el id del motor: guárdalo junto a tu referencia
print(tx.transaction_id, app_id, tx.data["status"], tx.warnings)
JSON
{
  "status": "success",
  "transaction_id": "a1b2c3d4-0000-4a1b-9c2d-000000000001",
  "module": "credits",
  "data": {
    "application_id": "sbx-E-2cf1b69c533d",
    "status": "RECEIVED",
    "profile": "eligible",
    "environment": "sandbox"
  }
}

Hay tres identificadores y conviene guardar los tres en tu sistema:

IdQuién lo generaPara qué
external_referenceTúTu llave de negocio; la ves en tu ERP.
data.application_idEl motorEl id de la solicitud: lo usan decidir, desembolsar, el detalle y los webhooks (resource.application_id).
transaction_idEl gatewayEl registro de esa llamada: soporte y transactions.get. Cada paso (radicar, decidir, desembolsar) tiene el suyo.

Si tu llave tiene affiliates:upsert, el bloque afiliado también registra al afiliado al vuelo; si ese registro falla, la radicación sigue y llega un aviso en warnings (ver Afiliados).

2. Trabajar la bandeja #

GET/v1/gateway/credits/applications/ devuelve una página estilo DRF (count, next, previous, results). Filtros: page, page_size (máx. 100, por defecto 25), search (documento o nombre) y status. Más en Paginación.

page_no = 1
while True:
    page = client.credits.list_applications(status="IN_REVIEW", page=page_no, page_size=50)
    for app in page:                       # Page itera sobre results
        print(app["application_id"], app.get("document_number"), app["status"])
    if not page.next:
        break
    page_no += 1

3. Consultar el detalle #

GET/v1/gateway/credits/applications/{app_id}/ devuelve el cuerpo del motor tal cual (en los SDK, un diccionario/objeto JSON sin tipar: dict, JsonObject, array, map[string]any).

detail = client.credits.get_application(app_id)
print(detail["status"], detail.get("reason_code"))

4. Decidir: aprobar o rechazar #

POST/v1/gateway/credits/applications/{app_id}/decision/ recibe decision (APPROVE o REJECT), approved_amount (obligatorio al aprobar; puede ser menor que el solicitado) y reason (texto libre que queda en la auditoría). Cada SDK trae las constantes:

PythonTypeScriptPHP.NETGo
DECISION_APPROVE / DECISION_REJECTDECISION_APPROVE / DECISION_REJECTDecision::APPROVE / Decision::REJECTDecisions.Approve / Decisions.Rejectcompense.DecisionApprove / compense.DecisionReject
from compense_gateway import DECISION_APPROVE

decision = client.credits.decide_application(
    app_id,
    decision=DECISION_APPROVE,
    approved_amount="3000000",
    reason="Capacidad de pago verificada",
    idempotency_key="horizonte-cred-2026-000123-decision",
)
print(decision.data["status"])             # APPROVED

# Rechazar:
# client.credits.decide_application(app_id, decision=DECISION_REJECT, reason="Score insuficiente")

5. Desembolsar #

POST/v1/gateway/credits/applications/{app_id}/disburse/ no lleva cuerpo y mueve dinero. Usa una Idempotency-Key estable derivada de tu solicitud (no la generada al azar por el SDK): si la respuesta se pierde por un corte de red, reintentas con la misma llave y el gateway te devuelve el desembolso original en vez de ordenar otro.

disb = client.credits.disburse_application(
    app_id, idempotency_key="horizonte-cred-2026-000123-desembolso")
print(disb.transaction_id, disb.data["status"], disb.data.get("disbursement_id"), disb.idempotent_replay)
JSON
{
  "status": "success",
  "transaction_id": "b7e1c0d2-5a3f-4c9e-8d21-000000000003",
  "module": "credits",
  "data": {"application_id": "sbx-E-2cf1b69c533d", "disbursement_id": "dsb-E-506a3cd0df6b", "status": "DISBURSED"}
}

La confirmación definitiva llega por el webhook credits.disbursed. La receta Desembolsar un crédito y conciliar muestra el ciclo completo con tu ERP.

Firma de alto valor (opcional) #

credits:decide y credits:disburse están entre los scopes que admiten firma HMAC de la petición. Solo aplica si Compense activó la firma en tu llave (te lo indica al emitirla y te entrega un secreto de firma); con la firma activa, toda decisión o desembolso sin una cabecera X-Compense-Signature válida responde 401.

Los SDK no firman las peticiones

Ninguno de los cinco SDK implementa X-Compense-Signature. Si tu llave tiene la firma activa, haz esas dos llamadas con tu propio cliente HTTP siguiendo el esquema de abajo, o pide una llave sin firma para ellas.

El formato es X-Compense-Signature: t=<unix>,v2=<hex>, donde v2 es el HMAC-SHA256 (en hexadecimal) con tu secreto de firma sobre estas cinco líneas unidas por \n: el timestamp, el método en mayúsculas, la ruta tal como la recibe el gateway (empieza por /v1/gateway/), la Idempotency-Key (vacía si no envías) y el cuerpo crudo. El timestamp no puede diferir más de 5 minutos del reloj del gateway.

Shell
APP_ID="sbx-E-2cf1b69c533d"
IDEM="horizonte-cred-2026-000123-desembolso"
RUTA="/v1/gateway/credits/applications/$APP_ID/disburse/"
T=$(date +%s)
CUERPO=""   # disburse no lleva cuerpo
FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$T" "POST" "$RUTA" "$IDEM" "$CUERPO" \
  | openssl dgst -sha256 -hmac "$COMPENSE_SIGNING_SECRET" -hex | sed 's/^.* //')

curl -X POST "https://sandbox.dev.compense.co/gw$RUTA" \
  -H "X-API-Key: $COMPENSE_API_KEY" \
  -H "Idempotency-Key: $IDEM" \
  -H "X-Compense-Signature: t=$T,v2=$FIRMA"

Errores de negocio típicos #

HTTPExcepción (Python)titleCuándo
401 / 403AuthError—Llave inválida, de otro entorno o sin el scope del paso (credits:decide, credits:disburse…).
404NotFoundError—El app_id no existe. En el sandbox, también al radicar para el documento 9999999999.
409IdempotencyConflictErrorIdempotency conflictLa misma Idempotency-Key sigue en proceso: reintenta con la misma llave.
409IdempotencyConflictErrorRequest rejected by engineLa solicitud ya se está desembolsando o ya se desembolsó (en el sandbox, también cuando no está aprobada). No reintentes con otra llave: consulta el detalle.
422ValidationError—Cuerpo inválido: falta un obligatorio, amount mal formado, term_months fuera de 1–120, APPROVE sin approved_amount; desembolsar una solicitud que no está APPROVED; o el afiliado no está en el registro de tu entidad (sincronízalo primero con Afiliados). Revisa detail y errors.
502 / 504UpstreamError—El motor falló o no respondió. Reintenta con la misma Idempotency-Key.

Un 409 no siempre es un conflicto de idempotencia: distingue los dos casos por title (y lee detail). Todas las excepciones traen trace_id, igual a la cabecera X-Request-ID: cítalo a soporte. Más en Errores y Reintentos.

Un rechazo de negocio (afiliado con mora, sin capacidad, monto fuera de política) no es un error HTTP: la radicación o la decisión responden 200 con data.status: "REJECTED" y un reason_code.

Sandbox: perfiles deterministas #

Con una llave de sandbox el motor es simulado: el resultado depende solo del documento del afiliado y del monto, sin estado ni dinero real.

DocumentoPerfilAl radicarAl decidirAl desembolsar
1000000001ElegibleRECEIVEDAPPROVEDDISBURSED
1000000002No elegibleREJECTED · NOT_ELIGIBLEREJECTED409
1000000003Revisión manualRECEIVEDIN_REVIEW · MANUAL_REVIEW409
1000000004Con deudasREJECTED · OUTSTANDING_DEBTREJECTED409
9999999999No existe404——
cualquier otroElegiblecomo 1000000001
  • Umbral de monto: un afiliado elegible con amount mayor a 50.000.000 COP se rechaza al radicar con AMOUNT_EXCEEDS_LIMIT, y sigue rechazado en la decisión y el desembolso. Úsalo para probar tu caso de “monto demasiado alto”.
  • El resultado de la decisión lo fija el perfil, no el decision que envíes: decidir REJECT sobre 1000000001 también responde APPROVED.
  • Los application_id del sandbox tienen la forma sbx-<perfil>-<hash> y son deterministas por external_reference: radicar dos veces con la misma referencia devuelve el mismo id.
  • El sandbox no guarda estado: el detalle de una solicitud ya desembolsada sigue mostrando APPROVED. Para recorrer el ciclo del desembolso (PENDING → PROCESSING → DISBURSED, o FAILED con el monto mágico 22.22) usa client.sandbox.simulate.credit_disbursement; ver Simuladores.

Webhooks #

Los eventos namespaced de créditos llegan con el sobre {event, version, tenant_id, occurred_at, resource}:

Créditos #

EventoDesdeCuándo se emite
credits.application.received2026-06-01Se radicó una solicitud de crédito en el gateway y quedó aceptada para proceso.
credits.decision2026-06-01Un asesor o el motor resolvió la solicitud (decision, approved o rejected).
credits.disbursed2026-06-01Se desembolsaron los fondos del crédito aprobado.
JSON
{
  "event": "credits.disbursed",
  "version": "1",
  "tenant_id": "horizonte",
  "occurred_at": "2026-11-02T15:00:00Z",
  "resource": {
    "application_id": "6f1d2c3b-8a9e-4b7c-9d0e-1a2b3c4d5e6f",
    "disbursement_id": "DSB-2026-000777",
    "amount": "3000000.00",
    "currency": "COP",
    "method": "ach_credit",
    "disbursed_at": "2026-11-02T15:00:00Z",
    "status": "DISBURSED"
  }
}

credits.decision trae en resource el decision (approved o rejected), quién decidió, los montos aprobados (null si se rechazó) y los motivos.

Además existen los eventos clásicos del ciclo de crédito (categoría general). Son anteriores al sobre: sus campos vienen en el nivel superior del cuerpo, no en resource (léelos de payload en tu SDK).

Ciclo de crédito clásico (categoría general, sin sobre namespaced) #

EventoDesdeCuándo se emite
application.submitted—El solicitante terminó y envió la solicitud.
application.scored—Resultados de buró, scoring y reglas del motor de decisión.
application.approved—Aprobada por un analista o automáticamente por el motor de reglas.
application.rejected—Rechazada por el motor de riesgo, cumplimiento o un analista.
application.manual_review_required—Enviada a la bandeja de revisión humana.
credit.activated—El crédito quedó activo tras el desembolso.
credit.disbursed—El proveedor confirmó la transferencia de los fondos.
credit.restructured—Se aprobó un cambio de condiciones del crédito.
credit.written_off—El crédito se castigó como pérdida irrecuperable.
installment.due_soon—Una cuota vence en los próximos 5 días.
compliance.exception_raised—Alerta SARLAFT, UIAF u otra excepción regulatoria.
compliance.ros_submitted—Se envió un reporte de operación sospechosa (ROS) a la UIAF.

Suscríbete a unos u otros, no a ambos para el mismo hecho, y deduplica por X-Webhook-Id. Ver Webhooks y Verificación.

Lo que sigue #