Guía de implementación
Subsidios y cuota monetaria
Radica subsidios de ley por tipo (p. ej. unemployment), trabaja la bandeja y decide; genera la cuota monetaria del período con dry_run, lístala y desembolsa cada una. Con los eventos subsidies.* y monetary_allowance.*.
En esta página
Este módulo cubre los dos frentes de subsidio de una caja de compensación:
- Subsidios por solicitud (p. ej. el subsidio al desempleo): el afiliado los pide, tú los radicas, un analista o el motor de reglas los decide.
- Cuota monetaria: el subsidio en dinero por persona a cargo que se liquida cada período. Aquí no hay solicitud: generas la liquidación del mes, la revisas y desembolsas cada cuota.
El afiliado debe existir en el registro de tu entidad (ver Afiliados).
Operaciones y scopes #
| Operación | Método SDK (Python) | Scope |
|---|---|---|
POST/v1/gateway/subsidies/{subsidy_type}/ | client.subsidies.submit_application(subsidy_type, ...) | subsidies:submit |
GET/v1/gateway/subsidies/applications/ | client.subsidies.list_applications(...) | subsidies:read |
GET/v1/gateway/subsidies/applications/{app_id}/ | client.subsidies.get_application(app_id) | subsidies:read |
POST/v1/gateway/subsidies/applications/{app_id}/decision/ | client.subsidies.decide_application(app_id, ...) | subsidies:decide |
POST/v1/gateway/subsidies/monetary-allowance/generate/ | client.monetary_allowance.generate(...) | subsidies:submit |
GET/v1/gateway/subsidies/monetary-allowance/ | client.monetary_allowance.list(...) | subsidies:read |
POST/v1/gateway/subsidies/monetary-allowance/{disb_id}/disburse/ | client.monetary_allowance.disburse(disbursement_id) | subsidies:disburse |
En los demás SDK el recurso de cuota monetaria es client.monetaryAllowance (TypeScript),
$client->monetaryAllowance (PHP), client.MonetaryAllowance (.NET) y client.MonetaryAllowance (Go).
Las escrituras de cuota monetaria devuelven el sobre con module: "subsidies".
Subsidios por solicitud #
stateDiagram-v2 [*] --> RECEIVED: radicar RECEIVED --> IN_REVIEW: revisión manual RECEIVED --> APPROVED: APPROVE IN_REVIEW --> APPROVED: APPROVE RECEIVED --> REJECTED: REJECT o reglas IN_REVIEW --> REJECTED: REJECT APPROVED --> [*]: pago (evento subsidies.disbursed) REJECTED --> [*]
Tipos de subsidio #
El tipo va en la ruta: POST /v1/gateway/subsidies/{subsidy_type}/. Por el gateway se procesan
los subsidios de ley; el principal es unemployment (el subsidio al desempleo; también se acepta
el alias desempleo). Otros tipos de ley que tu entidad tenga configurados se radican con su código.
Los subsidios propios de la entidad (programas custom) no se radican por API: responden 422 y se
gestionan desde la interfaz contratada.
Radicar #
| Campo (SDK) | En el cuerpo | Obligatorio | Notas |
|---|---|---|---|
subsidy_type | (ruta) | sí | unemployment… |
affiliate | afiliado | sí | {document_type, document_number}; como en créditos, la clave en el cable es afiliado. |
period | period | no | Período de liquidación AAAA-MM. |
external_reference | external_reference | no | Tu id de la solicitud. Recomendado: es tu llave para conciliar. |
extra | (se fusiona) | no | Campos que tu motor pida en el formulario del subsidio. |
from compense_gateway import AffiliateRef
tx = client.subsidies.submit_application(
"unemployment",
affiliate=AffiliateRef("CC", "1000000001"),
period="2026-10",
external_reference="HORIZONTE-SUB-2026-000045",
idempotency_key="horizonte-sub-2026-000045",
)
sub_id = tx.data["application_id"]
print(tx.transaction_id, sub_id, tx.data["status"])import { AffiliateRef } from "@compense/sdk";
const tx = await client.subsidies.submitApplication("unemployment", {
affiliate: new AffiliateRef("CC", "1000000001"),
period: "2026-10",
externalReference: "HORIZONTE-SUB-2026-000045",
idempotencyKey: "horizonte-sub-2026-000045",
});
const subId = String(tx.data.application_id);
console.log(tx.transactionId, subId, tx.data.status);use Compense\Sdk\Model\AffiliateRef;
$tx = $client->subsidies->submitApplication(
'unemployment',
affiliate: new AffiliateRef('CC', '1000000001'),
period: '2026-10',
externalReference: 'HORIZONTE-SUB-2026-000045',
idempotencyKey: 'horizonte-sub-2026-000045',
);
$subId = $tx->data['application_id'];
echo $tx->transactionId, ' ', $subId, ' ', $tx->data['status'], PHP_EOL;Transaction tx = await client.Subsidies.SubmitApplicationAsync(
"unemployment",
new AffiliateRef("CC", "1000000001"),
period: "2026-10",
externalReference: "HORIZONTE-SUB-2026-000045",
idempotencyKey: "horizonte-sub-2026-000045");
string subId = tx.Data["application_id"]!.GetValue<string>();
Console.WriteLine($"{tx.TransactionId} {subId} {tx.Data["status"]}");tx, err := client.Subsidies.SubmitApplication(ctx, "unemployment", &compense.SubsidyApplicationParams{
Affiliate: compense.AffiliateRef{DocumentType: "CC", DocumentNumber: "1000000001"},
Period: "2026-10",
ExternalReference: "HORIZONTE-SUB-2026-000045",
}, compense.WithIdempotencyKey("horizonte-sub-2026-000045"))
if err != nil {
log.Fatal(err)
}
subID, _ := tx.Data["application_id"].(string)
fmt.Println(tx.TransactionID, subID, tx.Data["status"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/subsidies/unemployment/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-sub-2026-000045" \
-H "Content-Type: application/json" \
-d '{
"afiliado": {"document_type": "CC", "document_number": "1000000001"},
"period": "2026-10",
"external_reference": "HORIZONTE-SUB-2026-000045"
}'{
"status": "success",
"transaction_id": "0d9f7a10-3c2b-4e5f-8a71-000000000045",
"module": "subsidies",
"data": {"application_id": "sbx-E-fa4046ec0a84", "status": "RECEIVED", "subsidy_type": "unemployment"}
}Igual que en créditos: guarda data.application_id junto a tu external_reference, y si tu llave
tiene affiliates:upsert el bloque afiliado registra al afiliado al vuelo (con aviso en
warnings si falla).
Bandeja y detalle #
La bandeja (
GET/v1/gateway/subsidies/applications/
) acepta page, page_size (máx. 100), search
(documento o nombre) y status; el detalle (
GET/v1/gateway/subsidies/applications/{app_id}/
) devuelve el cuerpo
del motor tal cual.
page = client.subsidies.list_applications(status="IN_REVIEW", search="1000000003", page_size=25)
print(page.count)
for app in page:
print(app["application_id"], app["status"])
detail = client.subsidies.get_application(sub_id)
print(detail["status"])const page = await client.subsidies.listApplications({ status: "IN_REVIEW", search: "1000000003", pageSize: 25 });
console.log(page.count);
for (const app of page) console.log(app.application_id, app.status);
const detail = await client.subsidies.getApplication(subId);
console.log(detail.status);$page = $client->subsidies->listApplications(search: '1000000003', status: 'IN_REVIEW', pageSize: 25);
echo $page->count, PHP_EOL;
foreach ($page as $app) {
echo $app['application_id'], ' ', $app['status'], PHP_EOL;
}
$detail = $client->subsidies->getApplication($subId);
echo $detail['status'], PHP_EOL;Page page = await client.Subsidies.ListApplicationsAsync(pageSize: 25, search: "1000000003", status: "IN_REVIEW");
Console.WriteLine(page.Count);
foreach (var app in page) Console.WriteLine($"{app["application_id"]} {app["status"]}");
var detail = await client.Subsidies.GetApplicationAsync(subId);
Console.WriteLine(detail["status"]);page, err := client.Subsidies.ListApplications(ctx, &compense.ListApplicationsParams{
Status: "IN_REVIEW", Search: "1000000003", PageSize: 25,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(page.Count)
for _, app := range page.Results {
fmt.Println(app["application_id"], app["status"])
}
detail, err := client.Subsidies.GetApplication(ctx, subID)
if err != nil {
log.Fatal(err)
}
fmt.Println(detail["status"])curl "https://sandbox.dev.compense.co/gw/v1/gateway/subsidies/applications/?status=IN_REVIEW&search=1000000003&page_size=25" \
-H "X-API-Key: $COMPENSE_API_KEY"
curl https://sandbox.dev.compense.co/gw/v1/gateway/subsidies/applications/$SUB_ID/ \
-H "X-API-Key: $COMPENSE_API_KEY"Decidir #
POST/v1/gateway/subsidies/applications/{app_id}/decision/
recibe decision (APPROVE o REJECT) y reason. A
diferencia de créditos, no hay monto aprobado en el contrato: el valor del subsidio lo fija la norma
y la configuración del programa. Usa las mismas constantes de decisión de cada SDK
(DECISION_REJECT, Decision::REJECT, Decisions.Reject, compense.DecisionReject).
from compense_gateway import DECISION_REJECT
tx = client.subsidies.decide_application(
sub_id,
decision=DECISION_REJECT,
reason="No cumple el requisito de permanencia",
idempotency_key="horizonte-sub-2026-000045-decision",
)
print(tx.data["status"])import { DECISION_REJECT } from "@compense/sdk";
const decision = await client.subsidies.decideApplication(subId, {
decision: DECISION_REJECT,
reason: "No cumple el requisito de permanencia",
idempotencyKey: "horizonte-sub-2026-000045-decision",
});
console.log(decision.data.status);use Compense\Sdk\Model\Decision;
$decision = $client->subsidies->decideApplication(
$subId,
decision: Decision::REJECT,
reason: 'No cumple el requisito de permanencia',
idempotencyKey: 'horizonte-sub-2026-000045-decision',
);
echo $decision->data['status'], PHP_EOL;Transaction decision = await client.Subsidies.DecideApplicationAsync(
subId, Decisions.Reject, "No cumple el requisito de permanencia",
idempotencyKey: "horizonte-sub-2026-000045-decision");
Console.WriteLine(decision.Data["status"]);decision, err := client.Subsidies.DecideApplication(ctx, subID, &compense.SubsidyDecisionParams{
Decision: compense.DecisionReject,
Reason: "No cumple el requisito de permanencia",
}, compense.WithIdempotencyKey("horizonte-sub-2026-000045-decision"))
if err != nil {
log.Fatal(err)
}
fmt.Println(decision.Data["status"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/subsidies/applications/$SUB_ID/decision/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-sub-2026-000045-decision" \
-H "Content-Type: application/json" \
-d '{"decision": "REJECT", "reason": "No cumple el requisito de permanencia"}'El pago de un subsidio aprobado no tiene operación propia en el gateway: lo ejecuta el motor de
subsidios y te enteras por el evento subsidies.disbursed.
Cuota monetaria del período #
flowchart LR
A[generate<br/>dry_run: true] --> B{¿Cuadra con<br/>tu liquidación?}
B -- no --> A
B -- sí --> C[generate<br/>dry_run: false]
C --> D[list period<br/>paginando]
D --> E[disburse<br/>una por una]
E -.-> F((monetary_allowance.disbursed))
C -.-> G((monetary_allowance.generated))Generar (con dry_run) #
POST/v1/gateway/subsidies/monetary-allowance/generate/
evalúa la elegibilidad y genera la cuota del período.
Cuerpo: period (AAAA-MM, obligatorio), dry_run (por defecto false: con true evalúa sin
persistir los desembolsos) y extra. Es una escritura: lleva Idempotency-Key aunque sea un
dry_run, así que usa llaves distintas para la simulación y para la generación en firme.
sim = client.monetary_allowance.generate(
period="2026-10", dry_run=True, idempotency_key="horizonte-cuota-2026-10-simulacion-1")
print("Simulación:", sim.data)
gen = client.monetary_allowance.generate(
period="2026-10", idempotency_key="horizonte-cuota-2026-10-generacion")
print(gen.transaction_id, gen.data.get("disbursement_id"), gen.data.get("status"))const sim = await client.monetaryAllowance.generate({
period: "2026-10", dryRun: true, idempotencyKey: "horizonte-cuota-2026-10-simulacion-1",
});
console.log("Simulación:", sim.data);
const gen = await client.monetaryAllowance.generate({
period: "2026-10", idempotencyKey: "horizonte-cuota-2026-10-generacion",
});
console.log(gen.transactionId, gen.data.disbursement_id, gen.data.status);$sim = $client->monetaryAllowance->generate(
period: '2026-10', dryRun: true, idempotencyKey: 'horizonte-cuota-2026-10-simulacion-1');
print_r($sim->data);
$gen = $client->monetaryAllowance->generate(
period: '2026-10', idempotencyKey: 'horizonte-cuota-2026-10-generacion');
echo $gen->transactionId, ' ', $gen->data['status'] ?? '-', PHP_EOL;Transaction sim = await client.MonetaryAllowance.GenerateAsync(
"2026-10", dryRun: true, idempotencyKey: "horizonte-cuota-2026-10-simulacion-1");
Console.WriteLine($"Simulación: {sim.Data.ToJsonString()}");
Transaction gen = await client.MonetaryAllowance.GenerateAsync(
"2026-10", idempotencyKey: "horizonte-cuota-2026-10-generacion");
Console.WriteLine($"{gen.TransactionId} {gen.Data["disbursement_id"]} {gen.Data["status"]}");sim, err := client.MonetaryAllowance.Generate(ctx,
&compense.MonetaryAllowanceGenerateParams{Period: "2026-10", DryRun: true},
compense.WithIdempotencyKey("horizonte-cuota-2026-10-simulacion-1"))
if err != nil {
log.Fatal(err)
}
fmt.Println("Simulación:", sim.Data)
gen, err := client.MonetaryAllowance.Generate(ctx,
&compense.MonetaryAllowanceGenerateParams{Period: "2026-10"},
compense.WithIdempotencyKey("horizonte-cuota-2026-10-generacion"))
if err != nil {
log.Fatal(err)
}
fmt.Println(gen.TransactionID, gen.Data["disbursement_id"], gen.Data["status"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/subsidies/monetary-allowance/generate/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-cuota-2026-10-simulacion-1" \
-H "Content-Type: application/json" \
-d '{"period": "2026-10", "dry_run": true}'La respuesta de una generación elegible trae eligible: true, el amount, el disbursement_id y el
status; una no elegible, eligible: false con los motivos.
Antes de confiar en dry_run en producción
dry_run es parte del contrato, pero en el sandbox no cambia la respuesta (el sandbox nunca
persiste nada). Antes de usarlo como simulación sin efectos sobre tu base real, confirma con tu
ejecutivo de Compense que el motor de tu entidad lo honra.
Generar para un afiliado concreto #
El contrato acepta campos adicionales en el cuerpo. Para evaluar y generar la cuota de un
afiliado (por ejemplo, un reproceso o una novedad), envía su documento en extra como
document_number:
tx = client.monetary_allowance.generate(
period="2026-10",
extra={"document_number": "1000000002"},
idempotency_key="horizonte-cuota-2026-10-1000000002",
)
print(tx.data.get("eligible"), tx.data.get("reason_code") or tx.data.get("reasons"))const tx = await client.monetaryAllowance.generate({
period: "2026-10",
extra: { document_number: "1000000002" },
idempotencyKey: "horizonte-cuota-2026-10-1000000002",
});
console.log(tx.data.eligible, tx.data.reason_code ?? tx.data.reasons);$tx = $client->monetaryAllowance->generate(
period: '2026-10',
extra: ['document_number' => '1000000002'],
idempotencyKey: 'horizonte-cuota-2026-10-1000000002',
);
var_dump($tx->data['eligible'] ?? null);Transaction tx = await client.MonetaryAllowance.GenerateAsync(
"2026-10",
extra: new Dictionary<string, object?> { ["document_number"] = "1000000002" },
idempotencyKey: "horizonte-cuota-2026-10-1000000002");
Console.WriteLine($"{tx.Data["eligible"]} {tx.Data["reason_code"]}");tx, err := client.MonetaryAllowance.Generate(ctx, &compense.MonetaryAllowanceGenerateParams{
Period: "2026-10",
Extra: map[string]any{"document_number": "1000000002"},
}, compense.WithIdempotencyKey("horizonte-cuota-2026-10-1000000002"))
if err != nil {
log.Fatal(err)
}
fmt.Println(tx.Data["eligible"], tx.Data["reason_code"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/subsidies/monetary-allowance/generate/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-cuota-2026-10-1000000002" \
-H "Content-Type: application/json" \
-d '{"period": "2026-10", "dry_run": false, "document_number": "1000000002"}'Listar las cuotas del período #
GET/v1/gateway/subsidies/monetary-allowance/
filtra por period y status, y pagina con page / page_size
(máx. 100). Cada fila trae al menos disbursement_id, period, amount y status.
page = client.monetary_allowance.list(period="2026-10", page=1, page_size=100)
print(page.count)
for row in page:
print(row["disbursement_id"], row["amount"], row["status"])const page = await client.monetaryAllowance.list({ period: "2026-10", pageSize: 100 });
console.log(page.count);
for (const row of page) console.log(row.disbursement_id, row.amount, row.status);$page = $client->monetaryAllowance->list(period: '2026-10', page: 1, pageSize: 100);
echo $page->count, PHP_EOL;
foreach ($page as $row) {
echo $row['disbursement_id'], ' ', $row['amount'], ' ', $row['status'], PHP_EOL;
}Page page = await client.MonetaryAllowance.ListAsync("2026-10", page: 1, pageSize: 100);
Console.WriteLine(page.Count);
foreach (var row in page) Console.WriteLine($"{row["disbursement_id"]} {row["amount"]} {row["status"]}");page, err := client.MonetaryAllowance.List(ctx, &compense.MonetaryAllowanceListParams{
Period: "2026-10", Page: 1, PageSize: 100,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(page.Count)
for _, row := range page.Results {
fmt.Println(row["disbursement_id"], row["amount"], row["status"])
}curl "https://sandbox.dev.compense.co/gw/v1/gateway/subsidies/monetary-allowance/?period=2026-10&page=1&page_size=100" \
-H "X-API-Key: $COMPENSE_API_KEY"Los estados de una cuota en el motor son pending (generada, por pagar), processing, disbursed,
failed, reversed y prescribed (venció el plazo para cobrarla). Para desembolsar lo pendiente,
filtra status=pending. En el sandbox las cuotas generadas aparecen como GENERATED.
Desembolsar una cuota #
POST/v1/gateway/subsidies/monetary-allowance/{disb_id}/disburse/
no lleva cuerpo y mueve dinero (la cuota se abona al medio
de pago del afiliado, normalmente su billetera). Dale a cada cuota su propia Idempotency-Key
estable, derivada del disbursement_id: la receta
Cuota monetaria del período muestra el lote completo con
reintentos seguros.
out = client.monetary_allowance.disburse(
"mad-E-fc874ab08f97", idempotency_key="horizonte-cuota-desembolso-mad-E-fc874ab08f97")
print(out.transaction_id, out.data.get("status"), out.idempotent_replay)const out = await client.monetaryAllowance.disburse("mad-E-fc874ab08f97", {
idempotencyKey: "horizonte-cuota-desembolso-mad-E-fc874ab08f97",
});
console.log(out.transactionId, out.data.status, out.idempotentReplay);$out = $client->monetaryAllowance->disburse(
'mad-E-fc874ab08f97', idempotencyKey: 'horizonte-cuota-desembolso-mad-E-fc874ab08f97');
echo $out->transactionId, ' ', $out->data['status'] ?? '-', PHP_EOL;Transaction outTx = await client.MonetaryAllowance.DisburseAsync(
"mad-E-fc874ab08f97", idempotencyKey: "horizonte-cuota-desembolso-mad-E-fc874ab08f97");
Console.WriteLine($"{outTx.TransactionId} {outTx.Data["status"]} {outTx.IdempotentReplay}");out, err := client.MonetaryAllowance.Disburse(ctx, "mad-E-fc874ab08f97",
compense.WithIdempotencyKey("horizonte-cuota-desembolso-mad-E-fc874ab08f97"))
if err != nil {
log.Fatal(err)
}
fmt.Println(out.TransactionID, out.Data["status"], out.IdempotentReplay)curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/subsidies/monetary-allowance/mad-E-fc874ab08f97/disburse/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-cuota-desembolso-mad-E-fc874ab08f97"subsidies:disburse (y subsidies:decide) admiten la firma HMAC opcional X-Compense-Signature
cuando tu llave la tiene activa; los SDK no la implementan. El esquema está en
Créditos.
Errores típicos #
| HTTP | Excepción (Python) | Cuándo |
|---|---|---|
403 | AuthError | Falta el scope (subsidies:submit, subsidies:decide, subsidies:disburse…). |
404 | NotFoundError | La solicitud o la cuota no existen. En el sandbox, también al radicar o generar para 9999999999. |
409 | IdempotencyConflictError | Misma Idempotency-Key en vuelo (title: Idempotency conflict), o desembolso de algo que no está en estado desembolsable (title: Request rejected by engine). |
422 | ValidationError | period sin formato AAAA-MM, afiliado no registrado, tipo de subsidio no configurado o custom, decision inválida. |
400 | CompenseAPIError | El motor no pudo dispersar la cuota (por ejemplo, el afiliado no tiene medio de pago activo); el motivo viene en detail. |
502 / 504 | UpstreamError | El motor no respondió: reintenta con la misma llave. |
Sandbox #
| Documento | Subsidio (radicar → decidir) | Cuota monetaria (generar) |
|---|---|---|
1000000001 | RECEIVED → APPROVED | eligible: true, amount: 50000, status: GENERATED |
1000000002 | REJECTED (NOT_ELIGIBLE) | eligible: false, NOT_ELIGIBLE |
1000000003 | RECEIVED → IN_REVIEW (MANUAL_REVIEW) | eligible: true |
1000000004 | REJECTED (OUTSTANDING_DEBT) | eligible: false, OUTSTANDING_DEBT |
9999999999 | 404 | 404 |
- Sin documento en el cuerpo, la generación se evalúa como elegible.
- Los ids son deterministas:
sbx-…para solicitudes ymad-<perfil>-<hash>para cuotas. Desembolsar una cuotamad-E-…respondeDISBURSED; una de un perfil no elegible,409. - El sandbox no guarda estado: el listado devuelve siempre una cuota de muestra del período y una decisión no cambia el detalle posterior. Ver Datos de prueba.
Webhooks #
Subsidios #
| Evento | Desde | Cuándo se emite |
|---|---|---|
subsidies.application.received | 2026-06-01 | Se radicó una solicitud de subsidio (alias legado subsidy.application.received). |
subsidies.decision | 2026-06-01 | Un asesor o el motor de reglas resolvió la solicitud de subsidio. |
subsidies.disbursed | 2026-06-01 | Se pagó un subsidio aprobado. |
Cuota monetaria #
| Evento | Desde | Cuándo se emite |
|---|---|---|
monetary_allowance.generated | 2026-06-01 | Se generó la cuota monetaria de un afiliado para un período. |
monetary_allowance.disbursed | 2026-06-01 | Se pagó una cuota monetaria generada. |
{
"event": "monetary_allowance.disbursed",
"version": "1",
"tenant_id": "horizonte",
"occurred_at": "2026-11-05T09:00:00Z",
"resource": {
"allowance_id": "5b2e9c1d-7f3a-4d8b-a6c2-9e0f1a2b3c4d",
"disbursement_id": "5b2e9c1d-7f3a-4d8b-a6c2-9e0f1a2b3c4d",
"amount": "47000.00",
"currency": "COP",
"method": "wallet",
"disbursed_at": "2026-11-05T09:00:00Z",
"status": "disbursed"
}
}resource.allowance_ides eldisbursement_idque devolvió la generación y el listado: tu llave para marcar la cuota como pagada en tu sistema.subsidies.decisiontraedecision(approvedorejected), quién decidió, los montos (onull) y los motivos.- El
event_iddel desembolso se deriva de laIdempotency-Keyque usaste: reintentar con la misma llave nunca produce un segundo evento.
Verifica la firma y deduplica por X-Webhook-Id como se explica en
Verificación.