Implementation guide
Transactions and system
Check the asynchronous state of any write with client.transactions.get (status, history, reference) and validate the key and its scopes with client.system.health, which also works as a monitoring health check. With trace_id and X-Request-ID for support.
On this page
Two cross-cutting operations that don’t belong to any business module, but that every integration uses:
| Operation | SDK method (Python) | Scope | Used for |
|---|---|---|---|
GET/v1/gateway/transactions/{tx_id}/ | client.transactions.get(tx_id) | none (any valid key) | State and trace of a write you made through the gateway. |
GET/v1/gateway/health/ | client.system.health() | none | Validate the key: tenant and scopes. Health check for your monitoring. |
In the other SDKs: client.transactions.get / client.system.health() (TypeScript),
$client->transactions->get / $client->system->health() (PHP), client.Transactions.GetAsync /
client.System.HealthAsync() (.NET) and client.Transactions.Get / client.System.Health (Go).
Transaction state #
Every write that goes through the gateway (submitting, deciding, disbursing, recording a collection,
loading affiliates…) leaves a transaction record and returns its transaction_id in the
response envelope.
GET/v1/gateway/transactions/{tx_id}/
reads that record.
| Field | SDK model | What it is |
|---|---|---|
transaction_id | yes | The id the write returned to you. |
module | yes | credits, subsidies, affiliates, loans, wallet… |
status | yes | PROCESSING, COMPLETED or FAILED (see below). |
reference | yes | The business reference that was associated with it: the application_id in credits and subsidies, the disbursement_id in monetary allowance, the receipt_id in loan portfolio. |
history | yes | The milestones the call went through, in order (e.g. ["received", "submitted"]). |
operation | in raw | The internal operation (application, decision:<id>, disbursement:<id>…). |
data | in raw | The response that was stored (the same data as in the envelope). |
error | in raw | The reason, if status is FAILED. |
created_at, updated_at | in raw | ISO 8601 timestamps. |
The typed models (TransactionStatus) expose the first five; the rest is in raw
(status.raw, $status->raw, status.Raw).
What each state means #
stateDiagram-v2 [*] --> PROCESSING: the gateway accepts the write PROCESSING --> COMPLETED: the engine completed it PROCESSING --> FAILED: the engine rejected it or failed PROCESSING --> PROCESSING: the business operation is still in progress
PROCESSING: the write was accepted and the business operation is still open. It’s the normal state of a credit or subsidy submission (the application is now in the hands of the engine and the analyst) and of an affiliate load with rejected rows.COMPLETED: the operation finished (a decision, a disbursement, an applied collection, a load with no rejections).FAILED: the engine rejected the request or didn’t respond;raw.errorcarries the reason.
The record describes the call to the gateway, not the full life of the business object: the
state of a credit application (approved, disbursed…) is queried in its module
(credits.get_application) and arrives through
webhooks. Use transactions.get for support, auditing and reconciliation of
your own calls.
status = client.transactions.get("a1b2c3d4-0000-4a1b-9c2d-000000000001")
print(status.module, status.status, status.reference, status.history)
print(status.raw.get("operation"), status.raw.get("error"))const status = await client.transactions.get("a1b2c3d4-0000-4a1b-9c2d-000000000001");
console.log(status.module, status.status, status.reference, status.history);
console.log(status.raw.operation, status.raw.error);$status = $client->transactions->get('a1b2c3d4-0000-4a1b-9c2d-000000000001');
echo $status->module, ' ', $status->status, ' ', $status->reference ?? '-', ' ', implode(' → ', $status->history), PHP_EOL;
echo $status->raw['operation'] ?? '-', PHP_EOL;TransactionStatus status = await client.Transactions.GetAsync("a1b2c3d4-0000-4a1b-9c2d-000000000001");
Console.WriteLine($"{status.Module} {status.Status} {status.Reference} {string.Join(" → ", status.History)}");
Console.WriteLine($"{status.Raw["operation"]} {status.Raw["error"]}");status, err := client.Transactions.Get(ctx, "a1b2c3d4-0000-4a1b-9c2d-000000000001")
if err != nil {
log.Fatal(err)
}
fmt.Println(status.Module, status.Status, status.History, status.Raw["operation"])
if status.Reference != nil {
fmt.Println("Reference:", *status.Reference)
}curl https://sandbox.dev.compense.co/gw/v1/gateway/transactions/a1b2c3d4-0000-4a1b-9c2d-000000000001/ \
-H "X-API-Key: $COMPENSE_API_KEY"{
"transaction_id": "a1b2c3d4-0000-4a1b-9c2d-000000000001",
"module": "credits",
"operation": "application",
"status": "PROCESSING",
"reference": "sbx-E-2cf1b69c533d",
"history": ["received", "submitted"],
"data": {"application_id": "sbx-E-2cf1b69c533d", "status": "RECEIVED"},
"error": null,
"created_at": "2026-11-02T14:03:11.204Z",
"updated_at": "2026-11-02T14:03:11.612Z"
}You only see your entity’s transactions: a transaction_id from another tenant (or one that
doesn’t exist) responds 404.
Wait for a result without webhooks #
The recommended approach is to react to webhooks, but if a batch process needs to
confirm its own writes you can poll with increasing backoff until the state is no longer
PROCESSING. Keep in mind that a submission stays in PROCESSING on purpose: only wait this way
for operations that finish (decisions, disbursements, collections, loads).
import time
def esperar(tx_id: str, intentos: int = 6) -> str:
espera = 1.0
for _ in range(intentos):
st = client.transactions.get(tx_id)
if st.status != "PROCESSING":
return st.status
time.sleep(espera)
espera *= 2
return "PROCESSING"async function esperar(txId: string, intentos = 6): Promise<string> {
let espera = 1000;
for (let i = 0; i < intentos; i++) {
const st = await client.transactions.get(txId);
if (st.status !== "PROCESSING") return st.status;
await new Promise((r) => setTimeout(r, espera));
espera *= 2;
}
return "PROCESSING";
}function esperar(\Compense\Sdk\CompenseClient $client, string $txId, int $intentos = 6): string
{
$espera = 1;
for ($i = 0; $i < $intentos; $i++) {
$st = $client->transactions->get($txId);
if ($st->status !== 'PROCESSING') {
return $st->status;
}
sleep($espera);
$espera *= 2;
}
return 'PROCESSING';
}async Task<string> EsperarAsync(CompenseClient client, string txId, int intentos = 6)
{
var espera = TimeSpan.FromSeconds(1);
for (int i = 0; i < intentos; i++)
{
TransactionStatus st = await client.Transactions.GetAsync(txId);
if (st.Status != "PROCESSING") return st.Status;
await Task.Delay(espera);
espera *= 2;
}
return "PROCESSING";
}func esperar(ctx context.Context, client *compense.Client, txID string, intentos int) (string, error) {
espera := time.Second
for i := 0; i < intentos; i++ {
st, err := client.Transactions.Get(ctx, txID)
if err != nil {
return "", err
}
if st.Status != "PROCESSING" {
return st.Status, nil
}
time.Sleep(espera)
espera *= 2
}
return "PROCESSING", nil
}for espera in 1 2 4 8 16 32; do
ESTADO=$(curl -s https://sandbox.dev.compense.co/gw/v1/gateway/transactions/$TX_ID/ \
-H "X-API-Key: $COMPENSE_API_KEY" | jq -r .status)
[ "$ESTADO" != "PROCESSING" ] && break
sleep $espera
done
echo "$ESTADO"What if you never received the transaction_id?
If the write was cut off before the response arrived, you have no transaction_id to query.
Resend the same request with the same Idempotency-Key: the gateway returns the original
response (with its transaction_id and idempotent_replay: true) or, if it’s still in progress, a
409 you can retry. See Idempotency.
Validate the key and its scopes #
GET/v1/gateway/health/
doesn’t touch the engine: it confirms the key is valid and returns the
tenant it belongs to and the list of scopes. Use it at two moments:
- When your service starts: check that the key has every scope your integration needs and fail
fast (with a clear message) if any is missing, instead of finding out through a
403on an affiliate’s first operation. - As a monitoring health check: a
200tells you the gateway is responding and that your key is still valid (it hasn’t been revoked or rotated).
from compense_gateway import AuthError
REQUERIDOS = {"credits:submit", "credits:read", "loans:read", "loans:collect"}
try:
health = client.system.health()
except AuthError as e:
raise SystemExit(f"Invalid or revoked key ({e.status}, trace_id={e.trace_id})")
faltan = REQUERIDOS - set(health.scopes)
if faltan:
raise SystemExit(f"The key for {health.tenant} is missing scopes: {sorted(faltan)}")
print("OK", health.tenant, health.status)import { AuthError } from "@compense/sdk";
const REQUERIDOS = ["credits:submit", "credits:read", "loans:read", "loans:collect"];
try {
const health = await client.system.health();
const faltan = REQUERIDOS.filter((s) => !health.scopes.includes(s));
if (faltan.length) throw new Error(`The key for ${health.tenant} is missing scopes: ${faltan.join(", ")}`);
console.log("OK", health.tenant, health.status);
} catch (err) {
if (err instanceof AuthError) throw new Error(`Invalid or revoked key (${err.status}, trace_id=${err.traceId})`);
throw err;
}use Compense\Sdk\Exception\AuthException;
$requeridos = ['credits:submit', 'credits:read', 'loans:read', 'loans:collect'];
try {
$health = $client->system->health();
} catch (AuthException $e) {
exit("Invalid or revoked key ({$e->status}, trace_id={$e->traceId})\n");
}
$faltan = array_diff($requeridos, $health->scopes);
if ($faltan) {
exit("The key for {$health->tenant} is missing scopes: " . implode(', ', $faltan) . "\n");
}
echo 'OK ', $health->tenant, PHP_EOL;string[] requeridos = { "credits:submit", "credits:read", "loans:read", "loans:collect" };
GatewayHealth health;
try
{
health = await client.System.HealthAsync();
}
catch (AuthException e)
{
throw new InvalidOperationException($"Invalid or revoked key ({e.Status}, trace_id={e.TraceId})");
}
var faltan = requeridos.Except(health.Scopes).ToList();
if (faltan.Count > 0)
{
throw new InvalidOperationException($"The key for {health.Tenant} is missing scopes: {string.Join(", ", faltan)}");
}
Console.WriteLine($"OK {health.Tenant} {health.Status}");requeridos := []string{"credits:submit", "credits:read", "loans:read", "loans:collect"}
health, err := client.System.Health(ctx)
if compense.IsAuthError(err) {
log.Fatalf("invalid or revoked key: %v", err)
} else if err != nil {
log.Fatal(err)
}
tiene := map[string]bool{}
for _, s := range health.Scopes {
tiene[s] = true
}
for _, s := range requeridos {
if !tiene[s] {
log.Fatalf("the key for %s is missing the scope %s", health.Tenant, s)
}
}
fmt.Println("OK", health.Tenant, health.Status)curl -s https://sandbox.dev.compense.co/gw/v1/gateway/health/ \
-H "X-API-Key: $COMPENSE_API_KEY" | jq '{tenant, scopes}'
# {"tenant": "horizonte", "scopes": ["affiliates:read", "credits:read", "credits:submit", ...]}| Response | Meaning |
|---|---|
200 | Valid key: check tenant and scopes. |
401 | The X-API-Key header is missing. |
403 | Invalid key, revoked key, key from another environment (a sandbox key against production or vice versa) or an IP outside the key’s allowlist (title: IP not allowed). |
For monitoring, one request per minute is enough. A 200 doesn’t guarantee that a module’s engine
is up (this endpoint doesn’t touch it): for that, watch the 502/504 responses of your real
operations and the rate limits on your writes.
trace_id and X-Request-ID #
Every gateway response carries the X-Request-ID header. On errors, the
application/problem+json body carries the same value as trace_id: that’s what you should quote
to support, because it lets them find the exact cause in the gateway’s logs (which are never leaked
in the response).
- The SDKs generate an
X-Request-IDfor each logical operation and keep it the same across all of their automatic retries; in exceptions they expose it astrace_id(Python),traceId(TypeScript, PHP),TraceId(.NET) andTraceID(Go). - With cURL or your own HTTP client you can send your own
X-Request-ID(for example, the request id in your system) and the gateway returns it. - In a successful response, your correlation key is the envelope’s
transaction_id.
Always log transaction_id, trace_id (if there was an error), your external_reference and the
Idempotency-Key together: with those four pieces of data any support case gets resolved in
minutes. More in Errors and Support.