Implementation guide
Subsidies and monetary allowance
Submit statutory subsidies by type (e.g. unemployment), work the queue and decide; generate the period’s monetary allowance with dry_run, list it and disburse each one. With the subsidies.* and monetary_allowance.* events.
On this page
This module covers both subsidy fronts of a family compensation fund (caja de compensación):
- Application-based subsidies (e.g. the unemployment subsidy): the affiliate requests them, you submit them, and an analyst or the rules engine decides them.
- Monetary allowance: the cash subsidy per dependent that is settled every period. There is no application here: you generate the month’s settlement, review it and disburse each allowance.
The affiliate must exist in your entity’s registry (see Affiliates).
Operations and scopes #
| Operation | SDK method (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 |
In the other SDKs the monetary allowance resource is client.monetaryAllowance (TypeScript),
$client->monetaryAllowance (PHP), client.MonetaryAllowance (.NET) and client.MonetaryAllowance (Go).
Monetary allowance writes return the envelope with module: "subsidies".
Application-based subsidies #
stateDiagram-v2 [*] --> RECEIVED: submit RECEIVED --> IN_REVIEW: manual review RECEIVED --> APPROVED: APPROVE IN_REVIEW --> APPROVED: APPROVE RECEIVED --> REJECTED: REJECT or rules IN_REVIEW --> REJECTED: REJECT APPROVED --> [*]: payment (subsidies.disbursed event) REJECTED --> [*]
Subsidy types #
The type goes in the path: POST /v1/gateway/subsidies/{subsidy_type}/. The gateway processes
statutory subsidies; the main one is unemployment (the unemployment subsidy; the alias
desempleo is also accepted). Other statutory types your entity has configured are submitted with
their code. The entity’s own subsidies (custom programs) can’t be submitted through the API: they
respond 422 and are managed from the contracted interface.
Submit #
| Field (SDK) | In the body | Required | Notes |
|---|---|---|---|
subsidy_type | (path) | yes | unemployment… |
affiliate | afiliado | yes | {document_type, document_number}; as in credits, the key on the wire is afiliado. |
period | period | no | Settlement period AAAA-MM. |
external_reference | external_reference | no | Your application id. Recommended: it’s your key for reconciling. |
extra | (merged) | no | Fields your engine asks for in the subsidy form. |
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"}
}Same as in credits: store data.application_id next to your external_reference, and if your key
has affiliates:upsert the afiliado block registers the affiliate on the fly (with a notice in
warnings if it fails).
Queue and detail #
The queue (
GET/v1/gateway/subsidies/applications/
) accepts page, page_size (max. 100), search
(document or name) and status; the detail (
GET/v1/gateway/subsidies/applications/{app_id}/
) returns the
engine’s body as is.
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"Decide #
POST/v1/gateway/subsidies/applications/{app_id}/decision/
takes decision (APPROVE or REJECT) and reason. Unlike
credits, there is no approved amount in the contract: the subsidy’s value is set by regulation and
by the program’s configuration. Use the same decision constants each SDK provides
(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="Does not meet the minimum membership period requirement",
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: "Does not meet the minimum membership period requirement",
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: 'Does not meet the minimum membership period requirement',
idempotencyKey: 'horizonte-sub-2026-000045-decision',
);
echo $decision->data['status'], PHP_EOL;Transaction decision = await client.Subsidies.DecideApplicationAsync(
subId, Decisions.Reject, "Does not meet the minimum membership period requirement",
idempotencyKey: "horizonte-sub-2026-000045-decision");
Console.WriteLine(decision.Data["status"]);decision, err := client.Subsidies.DecideApplication(ctx, subID, &compense.SubsidyDecisionParams{
Decision: compense.DecisionReject,
Reason: "Does not meet the minimum membership period requirement",
}, 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": "Does not meet the minimum membership period requirement"}'Paying an approved subsidy has no operation of its own in the gateway: the subsidy engine executes
it and you find out through the subsidies.disbursed event.
The period’s monetary allowance #
flowchart LR
A[generate<br/>dry_run: true] --> B{Does it match<br/>your settlement?}
B -- no --> A
B -- yes --> C[generate<br/>dry_run: false]
C --> D[list period<br/>paginating]
D --> E[disburse<br/>one by one]
E -.-> F((monetary_allowance.disbursed))
C -.-> G((monetary_allowance.generated))Generate (with dry_run) #
POST/v1/gateway/subsidies/monetary-allowance/generate/
evaluates eligibility and generates the period’s allowance.
Body: period (AAAA-MM, required), dry_run (default false: with true it evaluates without
persisting the disbursements) and extra. It’s a write: it carries an Idempotency-Key even when
it’s a dry_run, so use different keys for the simulation and for the actual generation.
sim = client.monetary_allowance.generate(
period="2026-10", dry_run=True, idempotency_key="horizonte-cuota-2026-10-simulacion-1")
print("Simulation:", 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("Simulation:", 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($"Simulation: {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("Simulation:", 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}'The response for an eligible generation carries eligible: true, the amount, the
disbursement_id and the status; an ineligible one carries eligible: false with the reasons.
Before you rely on dry_run in production
dry_run is part of the contract, but in the sandbox it doesn’t change the response (the sandbox
never persists anything). Before you use it as a side-effect-free simulation against your real base,
confirm with your Compense account executive that your entity’s engine honors it.
Generate for a specific affiliate #
The contract accepts additional fields in the body. To evaluate and generate the allowance for
one affiliate (for example, a reprocess or a change in their situation), send their document in
extra as 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"}'List the period’s allowances #
GET/v1/gateway/subsidies/monetary-allowance/
filters by period and status, and paginates with page /
page_size (max. 100). Each row carries at least disbursement_id, period, amount and 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"An allowance’s states in the engine are pending (generated, awaiting payment), processing,
disbursed, failed, reversed and prescribed (the deadline to claim it has passed). To disburse
what’s pending, filter by status=pending. In the sandbox, generated allowances show up as
GENERATED.
Disburse an allowance #
POST/v1/gateway/subsidies/monetary-allowance/{disb_id}/disburse/
has no body and moves money (the allowance is credited to
the affiliate’s payment method, usually their wallet). Give each allowance its own stable
Idempotency-Key, derived from the disbursement_id: the recipe
The period’s monetary allowance shows the full batch with
safe retries.
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 (and subsidies:decide) support the optional X-Compense-Signature HMAC
signature when your key has it enabled; the SDKs don’t implement it. The scheme is in
Credits.
Typical errors #
| HTTP | Exception (Python) | When |
|---|---|---|
403 | AuthError | The scope is missing (subsidies:submit, subsidies:decide, subsidies:disburse…). |
404 | NotFoundError | The application or the allowance doesn’t exist. In the sandbox, also when submitting or generating for 9999999999. |
409 | IdempotencyConflictError | Same Idempotency-Key in flight (title: Idempotency conflict), or disbursing something that isn’t in a disbursable state (title: Request rejected by engine). |
422 | ValidationError | period not in AAAA-MM format, unregistered affiliate, subsidy type not configured or custom, invalid decision. |
400 | CompenseAPIError | The engine couldn’t pay out the allowance (for example, the affiliate has no active payment method); the reason comes in detail. |
502 / 504 | UpstreamError | The engine didn’t respond: retry with the same key. |
Sandbox #
| Document | Subsidy (submit → decide) | Monetary allowance (generate) |
|---|---|---|
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 |
- With no document in the body, the generation is evaluated as eligible.
- Ids are deterministic:
sbx-…for applications andmad-<profile>-<hash>for allowances. Disbursing amad-E-…allowance respondsDISBURSED; one from an ineligible profile,409. - The sandbox doesn’t keep state: the list always returns a sample allowance for the period and a decision doesn’t change the later detail. See Test data.
Webhooks #
Subsidies #
| Event | Since | When it's emitted |
|---|---|---|
subsidies.application.received | 2026-06-01 | A subsidy application was submitted (legacy alias subsidy.application.received). |
subsidies.decision | 2026-06-01 | An advisor or the rules engine decided the subsidy application. |
subsidies.disbursed | 2026-06-01 | An approved subsidy was paid out. |
Monetary allowance #
| Event | Since | When it's emitted |
|---|---|---|
monetary_allowance.generated | 2026-06-01 | An affiliate’s monetary allowance was generated for a period. |
monetary_allowance.disbursed | 2026-06-01 | A generated monetary allowance was paid out. |
{
"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_idis thedisbursement_idreturned by the generation and the list: your key for marking the allowance as paid in your system.subsidies.decisioncarriesdecision(approvedorrejected), who decided, the amounts (ornull) and the reasons.- The disbursement’s
event_idis derived from theIdempotency-Keyyou used: retrying with the same key never produces a second event.
Verify the signature and deduplicate by X-Webhook-Id as explained in
Verification.