Implementation guide
Administrative (ERP)
Integrate your ERP with Administrative Management: HR employees, contracts and HR adjustments, payroll adjustments and payslips, draft journal entries, third parties, taxes, and treasury with bank reconciliation.
On this page
This guide is for the technical team at an entity (a family compensation fund (caja de
compensación), fintech or healthcare provider (IPS)) that integrates its ERP with Compense’s
Administrative Management (Gestión Administrativa) module: HR, payroll, accounting and treasury.
Everything is server-to-server with the entity’s key; no end user is involved. The examples use
Caja Horizonte’s ERP, which identifies its documents with HORIZONTE-ERP-… references.
The company's surface, not the affiliate's
Here the resources belong to the entity: employees, adjustments, journal entries, third parties,
collections and bank statements. The paths hang from /v1/gateway/admin/… and, in the SDKs, from
client.admin (in .NET client.Admin, in Go client.Admin) with four groups: hr, payroll,
accounting and treasury (in .NET Hr, Payroll, Accounting, Treasury; in Go HR,
Payroll, Accounting, Treasury). The entity must have contracted the module; otherwise every
call returns 403 with «La entidad no tiene contratado el módulo Gestión Administrativa.» (“The
entity has not contracted the Administrative Management module.”).
What goes through the API and what stays in the panel #
| Area | Through the API | Stays in the panel (person with a role) |
|---|---|---|
| HR | Search and read employees and contracts; create or update employees (upsert by document); record sick leave, leaves of absence, permissions and vacations | Approve or reject payroll adjustments and contracts, link panel users, change an employee’s bank account |
| Payroll | Load payroll adjustments (overtime, surcharges, bonuses, deductions), one at a time or in batches; read periods, payslips and the electronic payroll status | Calculate, approve, reverse and post the payroll; generate and transmit the electronic payroll to the DIAN |
| Accounting | Create draft journal entries; read journal entries; create or update third parties; simulate taxes and withholdings | Post, reverse or void journal entries; close periods; chart of accounts; third parties’ bank details |
| Treasury | Record draft collections, disbursements and receivables; load bank statements and trigger the auto-match | Post or void the documents; close the reconciliation; bank accounts |
Why the line is drawn here. An API key isn’t a person: it has no role, no segregation of duties
and no second factor. Anything that approves, settles or posts—and therefore moves money or
closes the books—requires someone with a role in the panel. What your ERP sends comes in as a
request (HR adjustments, in SOLICITADA status) or as a draft (journal entries and treasury
documents, in DRAFT status), and is audited with the integration user and the gateway channel.
flowchart LR
ERP[Caja Horizonte ERP] -->|POST via API| B[Request / draft<br/>SOLICITADA · DRAFT]
B --> P{Compense panel<br/>person with a role}
P -->|approves / posts| OK[Approved · posted]
P -->|rejects / voids| X[Rejected · voided]
B -.->|webhook admin.*| ERPBank accounts. Changing a bank account is the classic vector for payment-diversion fraud. The API
accepts an employee’s payroll account only when the employee is created; any later change, or any
bank detail for a third party, returns 422 and must be done in the panel, which leaves an alert in
the audit log.
Sensitive data (Law 1581 of 2012). You can send the diagnosis (ICD-10) for a sick leave, but it
is never returned through the API: health-related adjustments read as «Ausencia justificada»
(“Justified absence”), with category OTRA.
Scopes #
| Scope | Allows |
|---|---|
hr:read | Employees, contracts and HR adjustments (read) |
hr:write | Create or update employees; record HR adjustments |
payroll:read | Payroll adjustments, periods, payslips and electronic payroll (read) |
payroll:write | Load payroll adjustments |
accounting:read | Journal entries, third parties and the tax simulator |
accounting:write | Draft journal entries; create or update third parties |
treasury:write | Draft collections, disbursements and receivables; bank statements and auto-match |
There’s an admin-erp preset with all seven. The read-only and full presets do not include
them (this is employee and salary data): they’re granted explicitly. payroll:write and
treasury:write are on the list of scopes that will require a request signature once that protection
is turned on.
Conventions #
- Send
Idempotency-Keyon everyPOST(except the tax simulator, which is a lookup). A retry with the same key returns the original response withidempotent_replay: true. - Employees and third parties are identified by id (UUID) or by document number; treasury bank accounts, by id or by account number.
- Amounts in COP as decimal strings (
"1500000.00"); dates asYYYY-MM-DD. - Reads return the engine’s body; writes return the standard envelope with
"module": "admin".
Free-form fields in each SDK #
The module’s bodies are broad (they reuse the panel’s forms), so the SDKs take only the required
fields as arguments and accept the rest as free-form fields, using the contract’s snake_case
names:
| SDK | How free-form fields are passed |
|---|---|
| Python | Additional keyword arguments (**fields): upsert_employee(id_number="…", first_name="Laura") |
| JavaScript / TS | A fields object: upsertEmployee({ idNumber: "…", fields: { first_name: "Laura" } }) |
| PHP | Named arguments in snake_case (variadic): upsertEmployee(idNumber: '…', first_name: 'Laura') |
| .NET | A fields dictionary: new Dictionary<string, object?> { ["first_name"] = "Laura" } |
| Go | A Fields map: Fields: map[string]any{"first_name": "Laura"} |
Null values are dropped, and the SDKs convert the amounts in the amount, subtotal,
opening_balance and closing_balance fields to decimal strings.
HR #
Create or update employees #
POST/v1/gateway/admin/hr/employees/
performs an upsert by id_number: if the employee doesn’t exist
it creates them (201, created: true); if they exist, it updates only the fields you send (200,
created: false). Fields: those on the employee record (first_name, middle_name, last_name,
second_last_name, birth_date, sex, email_personal, email_work, phone, address, city,
department, area_id, position_id, manager_id, status, hire_date, eps_code,
pension_fund_code, severance_fund_code…). user_id is rejected (NOT_ALLOWED_BY_API), as is
changing the bank details of an existing employee (BANK_CHANGE_NOT_ALLOWED).
import os
from compense_gateway import CompenseClient
client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
tx = client.admin.hr.upsert_employee(
id_number="1032456789",
id_type="CC",
first_name="Laura",
last_name="Pérez",
hire_date="2026-11-01",
email_work="laura.perez@horizonte.example",
phone="3001234567",
bank_name="Banco Uno", bank_account_type="AHORROS", bank_account_number="123456789", # only on creation
idempotency_key="horizonte-erp-emp-1032456789-v1",
)
print(tx.data["id"], tx.data["created"])import { CompenseClient } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
const tx = await client.admin.hr.upsertEmployee({
idNumber: "1032456789",
fields: {
id_type: "CC",
first_name: "Laura",
last_name: "Pérez",
hire_date: "2026-11-01",
email_work: "laura.perez@horizonte.example",
phone: "3001234567",
bank_name: "Banco Uno", bank_account_type: "AHORROS", bank_account_number: "123456789", // only on creation
},
idempotencyKey: "horizonte-erp-emp-1032456789-v1",
});
console.log(tx.data.id, tx.data.created);<?php
require __DIR__ . '/vendor/autoload.php';
use Compense\Sdk\CompenseClient;
$client = new CompenseClient(apiKey: getenv('COMPENSE_API_KEY'), environment: 'sandbox');
$tx = $client->admin->hr->upsertEmployee(
idNumber: '1032456789',
idempotencyKey: 'horizonte-erp-emp-1032456789-v1',
id_type: 'CC',
first_name: 'Laura',
last_name: 'Pérez',
hire_date: '2026-11-01',
email_work: 'laura.perez@horizonte.example',
phone: '3001234567',
bank_name: 'Banco Uno', bank_account_type: 'AHORROS', bank_account_number: '123456789', // only on creation
);
echo $tx->data['id'], ' ', var_export($tx->data['created'], true), 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);
Transaction tx = await client.Admin.Hr.UpsertEmployeeAsync(
"1032456789",
new Dictionary<string, object?>
{
["id_type"] = "CC",
["first_name"] = "Laura",
["last_name"] = "Pérez",
["hire_date"] = "2026-11-01",
["email_work"] = "laura.perez@horizonte.example",
["phone"] = "3001234567",
["bank_name"] = "Banco Uno", ["bank_account_type"] = "AHORROS", ["bank_account_number"] = "123456789",
},
idempotencyKey: "horizonte-erp-emp-1032456789-v1");
Console.WriteLine($"{tx.Data["id"]} {tx.Data["created"]}");package main
import (
"context"
"fmt"
"log"
"os"
compense "github.com/Appiasoft/compense-saas-sdk-go"
)
func main() {
ctx := context.Background()
client, err := compense.NewClient(os.Getenv("COMPENSE_API_KEY"),
compense.WithEnvironment(compense.Sandbox))
if err != nil {
log.Fatal(err)
}
tx, err := client.Admin.HR.UpsertEmployee(ctx, &compense.UpsertEmployeeParams{
IDNumber: "1032456789",
Fields: map[string]any{
"id_type": "CC",
"first_name": "Laura",
"last_name": "Pérez",
"hire_date": "2026-11-01",
"email_work": "laura.perez@horizonte.example",
"phone": "3001234567",
// only on creation
"bank_name": "Banco Uno", "bank_account_type": "AHORROS", "bank_account_number": "123456789",
},
}, compense.WithIdempotencyKey("horizonte-erp-emp-1032456789-v1"))
if err != nil {
log.Fatal(err)
}
fmt.Println(tx.Data["id"], tx.Data["created"])
}curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/hr/employees/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-erp-emp-1032456789-v1" \
-H "Content-Type: application/json" \
-d '{"id_number": "1032456789", "id_type": "CC", "first_name": "Laura", "last_name": "Pérez",
"hire_date": "2026-11-01", "email_work": "laura.perez@horizonte.example", "phone": "3001234567",
"bank_name": "Banco Uno", "bank_account_type": "AHORROS", "bank_account_number": "123456789"}'Look up employees and contracts #
hr.list_employees filters by q (name, document, code or email), status (ACTIVO or
RETIRADO), document (exact match), area and position. The employee record and the contracts
(salary, type, status and dates) are requested by id or document.
activos = client.admin.hr.list_employees(status="ACTIVO", area="Tesorería")
ficha = client.admin.hr.get_employee("1000000001")
contratos = client.admin.hr.list_employee_contracts("1000000001")
print(ficha["full_name"], [c["salary"] for c in contratos.results])const activos = await client.admin.hr.listEmployees({ status: "ACTIVO", area: "Tesorería" });
const ficha = await client.admin.hr.getEmployee("1000000001");
const contratos = await client.admin.hr.listEmployeeContracts("1000000001");
console.log(ficha.full_name, contratos.results.map((c) => c.salary));$activos = $client->admin->hr->listEmployees(status: 'ACTIVO', area: 'Tesorería');
$ficha = $client->admin->hr->getEmployee('1000000001');
$contratos = $client->admin->hr->listEmployeeContracts('1000000001');
echo $ficha['full_name'], ' ', count($contratos->results), PHP_EOL;Page activos = await client.Admin.Hr.ListEmployeesAsync(status: "ACTIVO", area: "Tesorería");
JsonObject ficha = await client.Admin.Hr.GetEmployeeAsync("1000000001");
Page contratos = await client.Admin.Hr.ListEmployeeContractsAsync("1000000001");
Console.WriteLine($"{ficha["full_name"]} {contratos.Results.Count}");activos, err := client.Admin.HR.ListEmployees(ctx, &compense.EmployeeListParams{Status: "ACTIVO", Area: "Tesorería"})
if err != nil {
log.Fatal(err)
}
ficha, err := client.Admin.HR.GetEmployee(ctx, "1000000001")
if err != nil {
log.Fatal(err)
}
contratos, err := client.Admin.HR.ListEmployeeContracts(ctx, "1000000001")
if err != nil {
log.Fatal(err)
}
fmt.Println(len(activos.Results), ficha["full_name"], len(contratos.Results))curl "https://sandbox.dev.compense.co/gw/v1/gateway/admin/hr/employees/?status=ACTIVO&area=Tesorer%C3%ADa" \
-H "X-API-Key: $COMPENSE_API_KEY"
curl https://sandbox.dev.compense.co/gw/v1/gateway/admin/hr/employees/1000000001/contracts/ \
-H "X-API-Key: $COMPENSE_API_KEY"HR adjustments #
Sick leave, leaves of absence, permissions and vacations. They always stay SOLICITADA: HR
validates them in the panel (an approve_now is ignored). Maximum days, overlaps with other
adjustments and vacation balance are validated. Types (type_code) in the default catalog:
VACACIONES, PERMISO_REMUNERADO, CITA_MEDICA, CALAMIDAD, LUTO, MATERNIDAD, PATERNIDAD,
INCAPACIDAD_COMUN, INCAPACIDAD_LABORAL, LICENCIA_REMUNERADA, LICENCIA_NO_REMUNERADA,
AUSENCIA and SUSPENSION (your entity may have its own).
tx = client.admin.hr.create_novelty(
employee="1000000001", type_code="INCAPACIDAD_COMUN", start_date="2026-10-20",
days=3, certificate_number="INC-998877", issuing_entity="Employee EPS",
diagnosis_code="J069", # stored, but never returned
idempotency_key="horizonte-erp-inc-2026-0457",
)
print(tx.data["number"], tx.data["status"], tx.data["category"]) # … SOLICITADA OTRA
pendientes = client.admin.hr.list_novelties(status="SOLICITADA", from_="2026-10-01", to="2026-10-31")const tx = await client.admin.hr.createNovelty({
employee: "1000000001",
typeCode: "INCAPACIDAD_COMUN",
startDate: "2026-10-20",
fields: { days: 3, certificate_number: "INC-998877", issuing_entity: "Employee EPS", diagnosis_code: "J069" },
idempotencyKey: "horizonte-erp-inc-2026-0457",
});
console.log(tx.data.number, tx.data.status, tx.data.category);
const pendientes = await client.admin.hr.listNovelties({ status: "SOLICITADA", from: "2026-10-01", to: "2026-10-31" });$tx = $client->admin->hr->createNovelty(
employee: '1000000001',
typeCode: 'INCAPACIDAD_COMUN',
startDate: '2026-10-20',
idempotencyKey: 'horizonte-erp-inc-2026-0457',
days: 3,
certificate_number: 'INC-998877',
issuing_entity: 'Employee EPS',
diagnosis_code: 'J069',
);
echo $tx->data['number'], ' ', $tx->data['status'], PHP_EOL;
$pendientes = $client->admin->hr->listNovelties(status: 'SOLICITADA', from: '2026-10-01', to: '2026-10-31');Transaction tx = await client.Admin.Hr.CreateNoveltyAsync(
"1000000001", "INCAPACIDAD_COMUN", "2026-10-20",
new Dictionary<string, object?>
{
["days"] = 3, ["certificate_number"] = "INC-998877",
["issuing_entity"] = "Employee EPS", ["diagnosis_code"] = "J069",
},
idempotencyKey: "horizonte-erp-inc-2026-0457");
Page pendientes = await client.Admin.Hr.ListNoveltiesAsync(status: "SOLICITADA", from: "2026-10-01", to: "2026-10-31");tx, err = client.Admin.HR.CreateNovelty(ctx, &compense.HRNoveltyParams{
Employee: "1000000001", TypeCode: "INCAPACIDAD_COMUN", StartDate: "2026-10-20",
Fields: map[string]any{
"days": 3, "certificate_number": "INC-998877",
"issuing_entity": "Employee EPS", "diagnosis_code": "J069",
},
}, compense.WithIdempotencyKey("horizonte-erp-inc-2026-0457"))
if err != nil {
log.Fatal(err)
}
pendientes, err := client.Admin.HR.ListNovelties(ctx, &compense.HRNoveltyListParams{
Status: "SOLICITADA", From: "2026-10-01", To: "2026-10-31",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(tx.Data["status"], len(pendientes.Results))curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/hr/novelties/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-erp-inc-2026-0457" \
-H "Content-Type: application/json" \
-d '{"employee": "1000000001", "type_code": "INCAPACIDAD_COMUN", "start_date": "2026-10-20",
"days": 3, "certificate_number": "INC-998877", "issuing_entity": "Employee EPS",
"diagnosis_code": "J069"}'Payroll #
Load payroll adjustments #
POST/v1/gateway/admin/payroll/novelties/
accepts one payroll adjustment (fields at the root) or a batch
in items (up to 500, all or nothing: an error points to items[i].field).
- Overtime and surcharges (
HED,HEN,HRN,HRDDF,HRNDF,HEDDF,HENDF) takequantityin hours. - Bonuses, commissions and other earnings (
BONIFICACION_S,BONIFICACION_NS,COMISION,AUXILIO_NS,OTRO_DEVENGO…) and deductions (DESCUENTO,ANTICIPO,SINDICATO) takeamount. - Concepts the engine calculates (salary, health, pension, withholding, loans…) are rejected.
period_idis optional: if you send it, the period must beABIERTOorCALCULADO(otherwise409 PERIOD_CLOSED) and the date must fall within it; withoutdatethe period’s start date is used. If the period already had its pre-payroll calculated, the response includes a warning: payroll must recalculate it.
periodos = client.admin.payroll.list_periods(year=2026, status="ABIERTO")
periodo = periodos.results[0]["id"]
# A single payroll adjustment
client.admin.payroll.create_novelties(
period_id=periodo, employee="1000000001", concept_code="HED", quantity=4, date="2026-10-15",
idempotency_key="horizonte-erp-he-2026-10-1000000001",
)
# A batch (all or nothing)
tx = client.admin.payroll.create_novelties(
period_id=periodo,
items=[
{"employee": "1000000001", "concept_code": "BONIFICACION_NS", "amount": "150000", "date": "2026-10-15"},
{"employee": "1000000002", "concept_code": "DESCUENTO", "amount": "20000", "date": "2026-10-16",
"description": "Authorized deduction"},
],
idempotency_key="horizonte-erp-lote-2026-10-A",
)
print(tx.data["created"], tx.data["warnings"])const periodos = await client.admin.payroll.listPeriods({ year: 2026, status: "ABIERTO" });
const periodo = String(periodos.results[0].id);
// A single payroll adjustment
await client.admin.payroll.createNovelties({
periodId: periodo,
fields: { employee: "1000000001", concept_code: "HED", quantity: 4, date: "2026-10-15" },
idempotencyKey: "horizonte-erp-he-2026-10-1000000001",
});
// A batch (all or nothing)
const tx = await client.admin.payroll.createNovelties({
periodId: periodo,
items: [
{ employee: "1000000001", concept_code: "BONIFICACION_NS", amount: "150000", date: "2026-10-15" },
{ employee: "1000000002", concept_code: "DESCUENTO", amount: "20000", date: "2026-10-16", description: "Authorized deduction" },
],
idempotencyKey: "horizonte-erp-lote-2026-10-A",
});
console.log(tx.data.created, tx.data.warnings);$periodos = $client->admin->payroll->listPeriods(status: 'ABIERTO', year: 2026);
$periodo = $periodos->results[0]['id'];
// A single payroll adjustment
$client->admin->payroll->createNovelties(
periodId: $periodo,
idempotencyKey: 'horizonte-erp-he-2026-10-1000000001',
employee: '1000000001', concept_code: 'HED', quantity: 4, date: '2026-10-15',
);
// A batch (all or nothing)
$tx = $client->admin->payroll->createNovelties(
periodId: $periodo,
items: [
['employee' => '1000000001', 'concept_code' => 'BONIFICACION_NS', 'amount' => '150000', 'date' => '2026-10-15'],
['employee' => '1000000002', 'concept_code' => 'DESCUENTO', 'amount' => '20000', 'date' => '2026-10-16',
'description' => 'Authorized deduction'],
],
idempotencyKey: 'horizonte-erp-lote-2026-10-A',
);
echo $tx->data['created'], PHP_EOL;Page periodos = await client.Admin.Payroll.ListPeriodsAsync(status: "ABIERTO", year: 2026);
string periodo = periodos.Results[0]["id"]!.GetValue<string>();
// A single payroll adjustment
await client.Admin.Payroll.CreateNoveltiesAsync(
periodo,
fields: new Dictionary<string, object?>
{
["employee"] = "1000000001", ["concept_code"] = "HED", ["quantity"] = 4, ["date"] = "2026-10-15",
},
idempotencyKey: "horizonte-erp-he-2026-10-1000000001");
// A batch (all or nothing)
Transaction tx = await client.Admin.Payroll.CreateNoveltiesAsync(
periodo,
items: new[]
{
new Dictionary<string, object?> { ["employee"] = "1000000001", ["concept_code"] = "BONIFICACION_NS", ["amount"] = "150000", ["date"] = "2026-10-15" },
new Dictionary<string, object?> { ["employee"] = "1000000002", ["concept_code"] = "DESCUENTO", ["amount"] = "20000", ["date"] = "2026-10-16", ["description"] = "Authorized deduction" },
},
idempotencyKey: "horizonte-erp-lote-2026-10-A");
Console.WriteLine(tx.Data["created"]);periodos, err := client.Admin.Payroll.ListPeriods(ctx, &compense.PayrollPeriodListParams{Year: 2026, Status: "ABIERTO"})
if err != nil {
log.Fatal(err)
}
periodo, _ := periodos.Results[0]["id"].(string)
// A single payroll adjustment
if _, err := client.Admin.Payroll.CreateNovelties(ctx, &compense.PayrollNoveltiesParams{
PeriodID: periodo,
Fields: map[string]any{"employee": "1000000001", "concept_code": "HED", "quantity": 4, "date": "2026-10-15"},
}, compense.WithIdempotencyKey("horizonte-erp-he-2026-10-1000000001")); err != nil {
log.Fatal(err)
}
// A batch (all or nothing)
lote, err := client.Admin.Payroll.CreateNovelties(ctx, &compense.PayrollNoveltiesParams{
PeriodID: periodo,
Items: []map[string]any{
{"employee": "1000000001", "concept_code": "BONIFICACION_NS", "amount": "150000", "date": "2026-10-15"},
{"employee": "1000000002", "concept_code": "DESCUENTO", "amount": "20000", "date": "2026-10-16",
"description": "Authorized deduction"},
},
}, compense.WithIdempotencyKey("horizonte-erp-lote-2026-10-A"))
if err != nil {
log.Fatal(err)
}
fmt.Println(lote.Data["created"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/payroll/novelties/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-erp-lote-2026-10-A" \
-H "Content-Type: application/json" \
-d '{"period_id": "'"$PERIODO"'", "items": [
{"employee": "1000000001", "concept_code": "BONIFICACION_NS", "amount": "150000", "date": "2026-10-15"},
{"employee": "1000000002", "concept_code": "DESCUENTO", "amount": "20000", "date": "2026-10-16",
"description": "Authorized deduction"}
]}'Periods, payslips and electronic payroll #
payroll.list_periods(status, kind, year)andpayroll.get_period(id): status (ABIERTO,CALCULADO,APROBADO…), dates and totals.payroll.list_period_payslips(period_id, employee=…): the period’s payslips (earnings, deductions, net pay, contributions and provisions).payroll.get_payslip(id)returns the breakdown by concept.payroll.electronic(year, month, status): the month’s electronic payroll status (DIAN): provider, summary by status and documents. Generation and transmission are done in the panel.payroll.list_novelties(employee, period, status, concept, from, to): what you’ve already loaded.
Every payslip lookup is recorded in the payroll log (who looked up salaries). The complete walkthrough, from loading payroll adjustments to the journal entry, is in the recipe From payroll to accounting.
Accounting #
Draft journal entries #
POST/v1/gateway/admin/accounting/entries/
creates a journal entry always in DRAFT (post: true returns
422 POST_NOT_ALLOWED). The accountant reviews it and posts it in the panel, where double entry,
posting accounts, required third party and cost center, and an open period are validated.
external_id(your id in the ERP) makes creation idempotent in the engine: resending the sameexternal_idreturns the existing journal entry (200,created: false). If the accountant voids the draft, you can resend it with the sameexternal_id.- Journal entries from the ERP carry
source_module: "gateway": the panel doesn’t edit them; they’re corrected in the ERP and resent. - Lines:
account(code),debitorcredit, and optionallythird_party(id or document),cost_center,description,tax_concept,base_amount,document_refanddue_date.
tx = client.admin.accounting.create_entry(
date="2026-10-27",
description="October services sale",
lines=[
{"account": "13050501", "debit": "1500000", "third_party": "900123456"},
{"account": "41750101", "credit": "1500000"},
],
external_id="HORIZONTE-ERP-AS-2026-000123",
voucher_type="CC",
reference="FV-1234",
idempotency_key="horizonte-erp-as-2026-000123",
)
print(tx.data["id"], tx.data["status"], tx.data["total_debit"]) # … DRAFT 1500000.00const tx = await client.admin.accounting.createEntry({
date: "2026-10-27",
description: "October services sale",
lines: [
{ account: "13050501", debit: "1500000", third_party: "900123456" },
{ account: "41750101", credit: "1500000" },
],
fields: { external_id: "HORIZONTE-ERP-AS-2026-000123", voucher_type: "CC", reference: "FV-1234" },
idempotencyKey: "horizonte-erp-as-2026-000123",
});
console.log(tx.data.id, tx.data.status);$tx = $client->admin->accounting->createEntry(
date: '2026-10-27',
description: 'October services sale',
lines: [
['account' => '13050501', 'debit' => '1500000', 'third_party' => '900123456'],
['account' => '41750101', 'credit' => '1500000'],
],
idempotencyKey: 'horizonte-erp-as-2026-000123',
external_id: 'HORIZONTE-ERP-AS-2026-000123',
voucher_type: 'CC',
reference: 'FV-1234',
);
echo $tx->data['id'], ' ', $tx->data['status'], PHP_EOL;Transaction tx = await client.Admin.Accounting.CreateEntryAsync(
"2026-10-27",
"October services sale",
new[]
{
new Dictionary<string, object?> { ["account"] = "13050501", ["debit"] = "1500000", ["third_party"] = "900123456" },
new Dictionary<string, object?> { ["account"] = "41750101", ["credit"] = "1500000" },
},
new Dictionary<string, object?>
{
["external_id"] = "HORIZONTE-ERP-AS-2026-000123", ["voucher_type"] = "CC", ["reference"] = "FV-1234",
},
idempotencyKey: "horizonte-erp-as-2026-000123");
Console.WriteLine($"{tx.Data["id"]} {tx.Data["status"]}");asiento, err := client.Admin.Accounting.CreateEntry(ctx, &compense.EntryParams{
Date: "2026-10-27",
Description: "October services sale",
Lines: []map[string]any{
{"account": "13050501", "debit": "1500000", "third_party": "900123456"},
{"account": "41750101", "credit": "1500000"},
},
Fields: map[string]any{"external_id": "HORIZONTE-ERP-AS-2026-000123", "voucher_type": "CC", "reference": "FV-1234"},
}, compense.WithIdempotencyKey("horizonte-erp-as-2026-000123"))
if err != nil {
log.Fatal(err)
}
fmt.Println(asiento.Data["id"], asiento.Data["status"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/accounting/entries/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-erp-as-2026-000123" \
-H "Content-Type: application/json" \
-d '{"external_id": "HORIZONTE-ERP-AS-2026-000123", "voucher_type": "CC", "date": "2026-10-27",
"description": "October services sale", "reference": "FV-1234",
"lines": [
{"account": "13050501", "debit": "1500000", "third_party": "900123456"},
{"account": "41750101", "credit": "1500000"}
]}'To find out what the accountant has already posted, query accounting.list_entries with status,
voucher_type, date_from, date_to, external_id, source=gateway, q and offset (pages of
100 with a total count), or accounting.get_entry(id) for the lines, the hash and the status.
Third parties #
accounting.upsert_third_party performs an upsert by document type and number. id_type accepts the
DIAN code (31, 13, 22…) or the alias (NIT, CC, CE, PA, TI, NUIP…). The NIT check
digit is calculated (if you send it and it doesn’t match, 422). Bank details (bank_*) and
user_id_link return 422 BANK_DATA_NOT_ALLOWED; in reads, the bank account comes back masked
(••••4321).
tx = client.admin.accounting.upsert_third_party(
id_type="NIT", id_number="900123456",
business_name="Papelería La Esquina SAS", is_supplier=True,
email="pagos@laesquina.example", responsibilities=["O-13"],
idempotency_key="horizonte-erp-tp-900123456",
)
proveedores = client.admin.accounting.list_third_parties(role="supplier", q="esquina", brief=True)
tercero = client.admin.accounting.get_third_party("900123456")const tx = await client.admin.accounting.upsertThirdParty({
idType: "NIT",
idNumber: "900123456",
fields: {
business_name: "Papelería La Esquina SAS",
is_supplier: true,
email: "pagos@laesquina.example",
responsibilities: ["O-13"],
},
idempotencyKey: "horizonte-erp-tp-900123456",
});
const proveedores = await client.admin.accounting.listThirdParties({ role: "supplier", q: "esquina", brief: true });
const tercero = await client.admin.accounting.getThirdParty("900123456");$tx = $client->admin->accounting->upsertThirdParty(
idType: 'NIT',
idNumber: '900123456',
idempotencyKey: 'horizonte-erp-tp-900123456',
business_name: 'Papelería La Esquina SAS',
is_supplier: true,
email: 'pagos@laesquina.example',
responsibilities: ['O-13'],
);
$proveedores = $client->admin->accounting->listThirdParties(q: 'esquina', role: 'supplier', brief: true);
$tercero = $client->admin->accounting->getThirdParty('900123456');Transaction tx = await client.Admin.Accounting.UpsertThirdPartyAsync(
"NIT", "900123456",
new Dictionary<string, object?>
{
["business_name"] = "Papelería La Esquina SAS", ["is_supplier"] = true,
["email"] = "pagos@laesquina.example", ["responsibilities"] = new[] { "O-13" },
},
idempotencyKey: "horizonte-erp-tp-900123456");
Page proveedores = await client.Admin.Accounting.ListThirdPartiesAsync(q: "esquina", role: "supplier", brief: true);
JsonObject tercero = await client.Admin.Accounting.GetThirdPartyAsync("900123456");if _, err := client.Admin.Accounting.UpsertThirdParty(ctx, &compense.ThirdPartyParams{
IDType: "NIT", IDNumber: "900123456",
Fields: map[string]any{
"business_name": "Papelería La Esquina SAS", "is_supplier": true,
"email": "pagos@laesquina.example", "responsibilities": []string{"O-13"},
},
}, compense.WithIdempotencyKey("horizonte-erp-tp-900123456")); err != nil {
log.Fatal(err)
}
tercero, err := client.Admin.Accounting.GetThirdParty(ctx, "900123456")
if err != nil {
log.Fatal(err)
}
fmt.Println(tercero["name"], tercero["bank_account_number"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/accounting/third-parties/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-erp-tp-900123456" \
-H "Content-Type: application/json" \
-d '{"id_type": "NIT", "id_number": "900123456", "business_name": "Papelería La Esquina SAS",
"is_supplier": true, "email": "pagos@laesquina.example", "responsibilities": ["O-13"]}'Tax and withholding simulator #
POST/v1/gateway/admin/accounting/taxes/compute/
is a pure lookup: it doesn’t write, doesn’t consume quota and
doesn’t take an Idempotency-Key (the SDKs don’t send one or retry the call). Use it to calculate
withholdings before building the journal entry’s lines.
calculo = client.admin.accounting.compute_taxes(
items=[{"concept": "RF_SERVICIOS", "base": "2000000", "iva": "380000"}],
date="2026-10-27", third_party="900123456",
)
for r in calculo.results:
print(r["concept"], r["base"], r["rate"], r["amount"])const calculo = await client.admin.accounting.computeTaxes({
items: [{ concept: "RF_SERVICIOS", base: "2000000", iva: "380000" }],
date: "2026-10-27",
thirdParty: "900123456",
});
for (const r of calculo.results) console.log(r.concept, r.base, r.rate, r.amount);$calculo = $client->admin->accounting->computeTaxes(
items: [['concept' => 'RF_SERVICIOS', 'base' => '2000000', 'iva' => '380000']],
date: '2026-10-27',
thirdParty: '900123456',
);
foreach ($calculo->results as $r) {
echo $r['concept'], ' ', $r['rate'], ' ', $r['amount'], PHP_EOL;
}Page calculo = await client.Admin.Accounting.ComputeTaxesAsync(
new[] { new Dictionary<string, object?> { ["concept"] = "RF_SERVICIOS", ["base"] = "2000000", ["iva"] = "380000" } },
date: "2026-10-27", thirdParty: "900123456");
foreach (JsonObject r in calculo.Results)
Console.WriteLine($"{r["concept"]} {r["rate"]} {r["amount"]}");calculo, err := client.Admin.Accounting.ComputeTaxes(ctx, &compense.ComputeTaxesParams{
Items: []map[string]any{{"concept": "RF_SERVICIOS", "base": "2000000", "iva": "380000"}},
Date: "2026-10-27",
ThirdParty: "900123456",
})
if err != nil {
log.Fatal(err)
}
for _, r := range calculo.Results {
fmt.Println(r["concept"], r["rate"], r["amount"])
}curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/accounting/taxes/compute/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"date": "2026-10-27", "third_party": "900123456",
"items": [{"concept": "RF_SERVICIOS", "base": "2000000", "iva": "380000"}]}'Treasury #
All documents stay in DRAFT; the treasurer or the accountant posts them in the panel.
| Operation | Document | Main fields |
|---|---|---|
POST/v1/gateway/admin/treasury/receipts/ | Collection (cash receipt) | third_party, bank_account, date, amount, method, reference, applications ([{document, amount}] or [{account, document_ref, amount}]) |
POST/v1/gateway/admin/treasury/payments/ | Disbursement (supplier payment) | third_party, bank_account, date, amount, method (TRANSFERENCIA, PSE, CHEQUE, EFECTIVO, OTRO), reference, applications ([{invoice, amount}]) |
POST/v1/gateway/admin/treasury/receivables/ | Receivable (accounts receivable) | third_party, issue_date, due_date, description, income_account, subtotal, iva_concept, cost_center |
POST/v1/gateway/admin/treasury/statements/ | Bank statement | bank_account, opening_balance, closing_balance, date_from, date_to and lines ([{date, description, reference, amount}], signed amounts) or content (CSV) |
POST/v1/gateway/admin/treasury/statements/{statement_id}/auto-match/ | Auto-match | No body |
Collection, disbursement and receivable #
recaudo = client.admin.treasury.create_receipt(
third_party="80111222", bank_account="0011223344", date="2026-10-27",
amount="850000", method="TRANSFERENCIA", reference="TRX-998877",
idempotency_key="horizonte-erp-rc-TRX-998877",
)
egreso = client.admin.treasury.create_payment(
third_party="900123456", bank_account="0011223344", date="2026-10-28",
amount="1200000", method="PSE", reference="PSE-4455",
idempotency_key="horizonte-erp-eg-PSE-4455",
)
cobro = client.admin.treasury.create_receivable(
third_party="900123456", issue_date="2026-10-27", due_date="2026-11-26",
description="Event hall rental, October", income_account="41552001", subtotal="500000",
idempotency_key="horizonte-erp-cxc-2026-10-031",
)
print(recaudo.data["status"], egreso.data["status"], cobro.data["total"]) # DRAFT DRAFT …const recaudo = await client.admin.treasury.createReceipt({
thirdParty: "80111222",
fields: { bank_account: "0011223344", date: "2026-10-27", amount: "850000", method: "TRANSFERENCIA", reference: "TRX-998877" },
idempotencyKey: "horizonte-erp-rc-TRX-998877",
});
const egreso = await client.admin.treasury.createPayment({
thirdParty: "900123456",
fields: { bank_account: "0011223344", date: "2026-10-28", amount: "1200000", method: "PSE", reference: "PSE-4455" },
idempotencyKey: "horizonte-erp-eg-PSE-4455",
});
const cobro = await client.admin.treasury.createReceivable({
thirdParty: "900123456",
fields: {
issue_date: "2026-10-27", due_date: "2026-11-26", description: "Event hall rental, October",
income_account: "41552001", subtotal: "500000",
},
idempotencyKey: "horizonte-erp-cxc-2026-10-031",
});$recaudo = $client->admin->treasury->createReceipt(
thirdParty: '80111222',
idempotencyKey: 'horizonte-erp-rc-TRX-998877',
bank_account: '0011223344', date: '2026-10-27', amount: '850000',
method: 'TRANSFERENCIA', reference: 'TRX-998877',
);
$egreso = $client->admin->treasury->createPayment(
thirdParty: '900123456',
idempotencyKey: 'horizonte-erp-eg-PSE-4455',
bank_account: '0011223344', date: '2026-10-28', amount: '1200000',
method: 'PSE', reference: 'PSE-4455',
);
$cobro = $client->admin->treasury->createReceivable(
thirdParty: '900123456',
idempotencyKey: 'horizonte-erp-cxc-2026-10-031',
issue_date: '2026-10-27', due_date: '2026-11-26', description: 'Event hall rental, October',
income_account: '41552001', subtotal: '500000',
);Transaction recaudo = await client.Admin.Treasury.CreateReceiptAsync(
"80111222",
new Dictionary<string, object?>
{
["bank_account"] = "0011223344", ["date"] = "2026-10-27", ["amount"] = "850000",
["method"] = "TRANSFERENCIA", ["reference"] = "TRX-998877",
},
idempotencyKey: "horizonte-erp-rc-TRX-998877");
Transaction egreso = await client.Admin.Treasury.CreatePaymentAsync(
"900123456",
new Dictionary<string, object?>
{
["bank_account"] = "0011223344", ["date"] = "2026-10-28", ["amount"] = "1200000",
["method"] = "PSE", ["reference"] = "PSE-4455",
},
idempotencyKey: "horizonte-erp-eg-PSE-4455");
Transaction cobro = await client.Admin.Treasury.CreateReceivableAsync(
"900123456",
new Dictionary<string, object?>
{
["issue_date"] = "2026-10-27", ["due_date"] = "2026-11-26", ["description"] = "Event hall rental, October",
["income_account"] = "41552001", ["subtotal"] = "500000",
},
idempotencyKey: "horizonte-erp-cxc-2026-10-031");recaudo, err := client.Admin.Treasury.CreateReceipt(ctx, &compense.TreasuryDocumentParams{
ThirdParty: "80111222",
Fields: map[string]any{
"bank_account": "0011223344", "date": "2026-10-27", "amount": "850000",
"method": "TRANSFERENCIA", "reference": "TRX-998877",
},
}, compense.WithIdempotencyKey("horizonte-erp-rc-TRX-998877"))
if err != nil {
log.Fatal(err)
}
if _, err := client.Admin.Treasury.CreatePayment(ctx, &compense.TreasuryDocumentParams{
ThirdParty: "900123456",
Fields: map[string]any{
"bank_account": "0011223344", "date": "2026-10-28", "amount": "1200000",
"method": "PSE", "reference": "PSE-4455",
},
}, compense.WithIdempotencyKey("horizonte-erp-eg-PSE-4455")); err != nil {
log.Fatal(err)
}
cobro, err := client.Admin.Treasury.CreateReceivable(ctx, &compense.TreasuryDocumentParams{
ThirdParty: "900123456",
Fields: map[string]any{
"issue_date": "2026-10-27", "due_date": "2026-11-26", "description": "Event hall rental, October",
"income_account": "41552001", "subtotal": "500000",
},
}, compense.WithIdempotencyKey("horizonte-erp-cxc-2026-10-031"))
if err != nil {
log.Fatal(err)
}
fmt.Println(recaudo.Data["status"], cobro.Data["status"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/treasury/receipts/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-erp-rc-TRX-998877" \
-H "Content-Type: application/json" \
-d '{"third_party": "80111222", "bank_account": "0011223344", "date": "2026-10-27",
"amount": "850000", "method": "TRANSFERENCIA", "reference": "TRX-998877"}'Import the statement and reconcile #
Load the bank statement with lines (signed amounts: positive comes in, negative goes out) or with
content (a CSV with date/amount or debit/credit columns). Then trigger the auto-match, which
pairs each statement line with a posted bank movement by exact amount, date proximity and
reference. The auto-match doesn’t create journal entries or close the reconciliation: the treasurer
reviews and closes it in the panel.
sequenceDiagram
autonumber
participant E as Caja Horizonte ERP
participant G as Compense Gateway
participant T as Treasurer (panel)
E->>G: POST treasury/statements/ {bank_account, lines}
G-->>E: 201 statement OPEN, lines UNMATCHED
G-)E: webhook admin.treasury.statement.imported
E->>G: POST treasury/statements/{id}/auto-match/
G-->>E: {matched: N}
T->>T: reviews pending items and closes the reconciliationtx = client.admin.treasury.import_statement(
bank_account="0011223344",
opening_balance="0",
lines=[
{"date": "2026-10-02", "description": "Deposit", "reference": "TRX-998877", "amount": "850000"},
{"date": "2026-10-03", "description": "Fee", "reference": "C1", "amount": "-12000.50"},
],
idempotency_key="horizonte-erp-ext-0011223344-2026-10",
)
extracto = tx.data["id"]
cruce = client.admin.treasury.auto_match_statement(
extracto, idempotency_key=f"horizonte-erp-cruce-{extracto}")
print(cruce.data["matched"])const tx = await client.admin.treasury.importStatement({
bankAccount: "0011223344",
fields: {
opening_balance: "0",
lines: [
{ date: "2026-10-02", description: "Deposit", reference: "TRX-998877", amount: "850000" },
{ date: "2026-10-03", description: "Fee", reference: "C1", amount: "-12000.50" },
],
},
idempotencyKey: "horizonte-erp-ext-0011223344-2026-10",
});
const extracto = String(tx.data.id);
const cruce = await client.admin.treasury.autoMatchStatement(extracto, {
idempotencyKey: `horizonte-erp-cruce-${extracto}`,
});
console.log(cruce.data.matched);$tx = $client->admin->treasury->importStatement(
bankAccount: '0011223344',
idempotencyKey: 'horizonte-erp-ext-0011223344-2026-10',
opening_balance: '0',
lines: [
['date' => '2026-10-02', 'description' => 'Deposit', 'reference' => 'TRX-998877', 'amount' => '850000'],
['date' => '2026-10-03', 'description' => 'Fee', 'reference' => 'C1', 'amount' => '-12000.50'],
],
);
$extracto = $tx->data['id'];
$cruce = $client->admin->treasury->autoMatchStatement($extracto, idempotencyKey: "horizonte-erp-cruce-{$extracto}");
echo $cruce->data['matched'], PHP_EOL;Transaction tx = await client.Admin.Treasury.ImportStatementAsync(
"0011223344",
new Dictionary<string, object?>
{
["opening_balance"] = "0",
["lines"] = new[]
{
new Dictionary<string, object?> { ["date"] = "2026-10-02", ["description"] = "Deposit", ["reference"] = "TRX-998877", ["amount"] = "850000" },
new Dictionary<string, object?> { ["date"] = "2026-10-03", ["description"] = "Fee", ["reference"] = "C1", ["amount"] = "-12000.50" },
},
},
idempotencyKey: "horizonte-erp-ext-0011223344-2026-10");
string extracto = tx.Data["id"]!.GetValue<string>();
Transaction cruce = await client.Admin.Treasury.AutoMatchStatementAsync(
extracto, idempotencyKey: $"horizonte-erp-cruce-{extracto}");
Console.WriteLine(cruce.Data["matched"]);ext, err := client.Admin.Treasury.ImportStatement(ctx, &compense.ImportStatementParams{
BankAccount: "0011223344",
Fields: map[string]any{
"opening_balance": "0",
"lines": []map[string]any{
{"date": "2026-10-02", "description": "Deposit", "reference": "TRX-998877", "amount": "850000"},
{"date": "2026-10-03", "description": "Fee", "reference": "C1", "amount": "-12000.50"},
},
},
}, compense.WithIdempotencyKey("horizonte-erp-ext-0011223344-2026-10"))
if err != nil {
log.Fatal(err)
}
extracto, _ := ext.Data["id"].(string)
cruce, err := client.Admin.Treasury.AutoMatchStatement(ctx, extracto,
compense.WithIdempotencyKey("horizonte-erp-cruce-"+extracto))
if err != nil {
log.Fatal(err)
}
fmt.Println(cruce.Data["matched"])curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/treasury/statements/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-erp-ext-0011223344-2026-10" \
-H "Content-Type: application/json" \
-d '{"bank_account": "0011223344", "opening_balance": "0",
"lines": [
{"date": "2026-10-02", "description": "Deposit", "reference": "TRX-998877", "amount": "850000"},
{"date": "2026-10-03", "description": "Fee", "reference": "C1", "amount": "-12000.50"}
]}'
curl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/admin/treasury/statements/$EXTRACTO/auto-match/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-erp-cruce-$EXTRACTO"Common errors #
| HTTP | Engine code | When |
|---|---|---|
401 | — | X-API-Key is missing |
403 | — | Key without the scope, IP not allowed, or Administrative Management module not contracted |
404 | NOT_FOUND | Employee, period, payslip, journal entry or third party doesn’t exist |
409 | PERIOD_CLOSED | The payroll period is approved or reversed |
409 | — | Idempotency conflict, or a business conflict (the adjustment overlaps with another one) |
422 | VALIDATION_ERROR | detail states the field and the reason, in Spanish |
422 | POST_NOT_ALLOWED | You tried to post a journal entry through the API |
422 | BANK_CHANGE_NOT_ALLOWED / BANK_DATA_NOT_ALLOWED | Bank details for an existing employee or for a third party |
422 | NOT_ALLOWED_BY_API | Fields reserved for the panel (for example user_id) |
429 | — | Per-minute limit or monthly quota |
502 / 504 | — | The engine failed or didn’t respond (quote the trace_id) |
{"type": "https://errors.compense.co/gateway/upstream-error", "title": "Request rejected by engine",
"status": 422,
"detail": "Por API los comprobantes se crean en borrador; el contador los contabiliza en el panel.",
"trace_id": "9f1c2e7a4b8d4e6fa1c0d2e3f4a5b6c7"}Events #
They’re emitted in the same transaction as the record, with an event_id derived from the
Idempotency-Key (a retry doesn’t duplicate the callback). None of them means money moved:
they’re requests or drafts. All of them carry channel: "gateway" and the standard envelope; never
diagnoses or bank details.
Administrative #
| Event | Since | When it's emitted |
|---|---|---|
admin.hr.novelty.received | 2026-10-27 | An HR adjustment was recorded. |
admin.payroll.novelty.received | 2026-10-27 | A payroll adjustment was uploaded. |
admin.accounting.entry.drafted | 2026-10-27 | A draft journal entry was created. |
admin.treasury.document.registered | 2026-10-27 | A draft collection, outgoing payment or billing statement was recorded (kind). |
admin.treasury.statement.imported | 2026-10-27 | A bank statement was uploaded. |
| Event | resource fields |
|---|---|
admin.payroll.novelty.received | novelty_id, employee_id, concept_code, date, quantity, amount, period_id, status |
admin.hr.novelty.received | novelty_id, number, employee_id, category (OTRA if health-related), dates, status |
admin.accounting.entry.drafted | entry_id, external_id, voucher_type, date, totals, status |
admin.treasury.document.registered | document_id, kind (receipt, payment or receivable), third_party_id, date, amount, status, reference |
admin.treasury.statement.imported | statement_id, bank_account_id, date range, lines, closing_balance |
There’s currently no event when HR approves or rejects an adjustment, or when payroll approves the period: check the status with the read operations.
Sandbox #
With a sandbox key, responses are deterministic and don’t touch real data:
- Test employees: CC 1000000001 (Laura Sandbox) and CC 1000000002 (Andrés Sandbox) already
exist: an upsert returns
created: falseand sending bank details returns422. Any other document is “created”. CC 9999999999 →404. - Periods:
payroll.list_periodsreturns one ABIERTO period (per-O-…, October 2026) and one APROBADO period (per-C-…, September 2026): loading payroll adjustments into it returns409 PERIOD_CLOSED. System concepts (SALARIO,SALUD…) →422. - Journal entries: always
DRAFT; sameexternal_id→ same id;post: true→422. - Third parties: NIT 900000001 already exists (
created: false); bank details →422. - Treasury:
DRAFTdocuments; amount<= 0→422; the auto-match returnsmatched: 0. - The tax simulator applies a flat 4 % rate to each base.