Receta
De la nómina a la contabilidad
Carga las novedades del mes desde un sistema externo, sigue el periodo de nómina, consulta desprendibles y nómina electrónica, y causa el asiento contable en borrador con el simulador de impuestos y un external_id idempotente.
En esta página
Caja Horizonte registra las horas extra y los recargos de su personal en un sistema de turnos y lleva su contabilidad en un ERP. En esta receta conectas ambos con el módulo Administrativo de Compense: cargas las novedades del mes al periodo de nómina, esperas a que nómina lo apruebe en el panel, lees los desprendibles y el estado de la nómina electrónica, y causas el asiento contable en borrador para que el contador lo contabilice.
Todo lo que entra por API es una solicitud o un borrador: calcular, aprobar y contabilizar la nómina, y transmitir la nómina electrónica a la DIAN, lo hacen personas con rol en el panel.
sequenceDiagram
autonumber
participant T as Sistema de turnos
participant E as ERP de Caja Horizonte
participant G as Gateway Compense
participant N as Nómina (panel)
participant C as Contador (panel)
T->>E: horas extra y recargos de octubre
E->>G: payroll.list_periods(year, status=ABIERTO)
E->>G: payroll.create_novelties(period_id, items[≤500]) + Idempotency-Key
G-)E: admin.payroll.novelty.received (por novedad)
N->>N: calcula y aprueba el periodo
loop hasta APROBADO
E->>G: payroll.get_period(id)
end
E->>G: payroll.list_period_payslips(id)
E->>G: payroll.electronic(year, month)
E->>G: accounting.compute_taxes(items)
E->>G: accounting.create_entry(external_id, lines) → DRAFT
G-)E: admin.accounting.entry.drafted
C->>C: revisa y contabiliza el comprobanteRequisitos #
- Una llave con
payroll:read,payroll:write,accounting:readyaccounting:write(o el presetadmin-erp). - Los colaboradores ya existen en Compense (si hay ingresos nuevos, créalos antes con
hr.upsert_employee; ver Talento humano). - Opcional: un endpoint de webhooks suscrito a
admin.payroll.novelty.receivedyadmin.accounting.entry.drafted.
Los ejemplos crean el cliente en el paso 1 y lo reutilizan; en Go asumen ctx y client creados
como en el quickstart.
1. Encuentra el periodo abierto #
Las novedades se cargan a un periodo ABIERTO o CALCULADO. Un periodo aprobado o revertido
responde 409 PERIOD_CLOSED.
import os
from compense_gateway import CompenseClient
client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
periodos = client.admin.payroll.list_periods(year=2026, status="ABIERTO")
periodo = next(p for p in periodos.results if p["status"] in ("ABIERTO", "CALCULADO"))
print(periodo["id"], periodo["name"], periodo["start_date"], periodo["end_date"])import { CompenseClient } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
const periodos = await client.admin.payroll.listPeriods({ year: 2026, status: "ABIERTO" });
const periodo = periodos.results.find((p) => p.status === "ABIERTO" || p.status === "CALCULADO")!;
console.log(periodo.id, periodo.name, periodo.start_date, periodo.end_date);<?php
require __DIR__ . '/vendor/autoload.php';
use Compense\Sdk\CompenseClient;
$client = new CompenseClient(apiKey: getenv('COMPENSE_API_KEY'), environment: 'sandbox');
$periodos = $client->admin->payroll->listPeriods(status: 'ABIERTO', year: 2026);
$abiertos = array_values(array_filter($periodos->results, fn ($p) => in_array($p['status'], ['ABIERTO', 'CALCULADO'], true)));
$periodo = $abiertos[0];
echo $periodo['id'], ' ', $periodo['name'], PHP_EOL;using System.Globalization;
using System.Text.Json.Nodes;
using Compense.Sdk;
using Compense.Sdk.Models;
using var client = new CompenseClient(
Environment.GetEnvironmentVariable("COMPENSE_API_KEY")!, CompenseEnvironment.Sandbox);
Page periodos = await client.Admin.Payroll.ListPeriodsAsync(status: "ABIERTO", year: 2026);
JsonObject periodo = periodos.Results.First(p =>
p["status"]!.GetValue<string>() is "ABIERTO" or "CALCULADO");
string periodoId = periodo["id"]!.GetValue<string>();
Console.WriteLine($"{periodoId} {periodo["name"]}");periodos, err := client.Admin.Payroll.ListPeriods(ctx, &compense.PayrollPeriodListParams{Year: 2026, Status: "ABIERTO"})
if err != nil {
log.Fatal(err)
}
var periodoID string
for _, p := range periodos.Results {
if p["status"] == "ABIERTO" || p["status"] == "CALCULADO" {
periodoID, _ = p["id"].(string)
break
}
}
fmt.Println(periodoID)curl "https://sandbox.dev.compense.co/gw/v1/gateway/admin/payroll/periods/?year=2026&status=ABIERTO" \
-H "X-API-Key: $COMPENSE_API_KEY"2. Carga las novedades del mes #
Convierte cada registro del sistema de turnos en una novedad: las horas extra y recargos (HED,
HEN, HRN, HRDDF, HRNDF, HEDDF, HENDF) van con quantity en horas; las bonificaciones,
comisiones y deducciones (BONIFICACION_NS, COMISION, DESCUENTO…) con amount. Los conceptos que
calcula el motor (salario, salud, pensión, retención…) se rechazan.
Envíalas en lotes de hasta 500. Cada lote es todo o nada: si una fila falla, no entra
ninguna y el error señala la fila (items[3].quantity: …). Deriva la Idempotency-Key del periodo y
del número de lote de tu proceso, para que reprocesar el mismo archivo no duplique horas.
from compense_gateway import IdempotencyConflictError, ValidationError
registros = [ # lo que exportó el sistema de turnos
{"documento": "1000000001", "concepto": "HED", "horas": 4, "fecha": "2026-10-15"},
{"documento": "1000000001", "concepto": "HRN", "horas": 6, "fecha": "2026-10-18"},
{"documento": "1000000002", "concepto": "HEN", "horas": 2, "fecha": "2026-10-21"},
]
items = [{"employee": r["documento"], "concept_code": r["concepto"],
"quantity": r["horas"], "date": r["fecha"]} for r in registros]
for n, i in enumerate(range(0, len(items), 500), start=1):
try:
tx = client.admin.payroll.create_novelties(
period_id=periodo["id"], items=items[i:i + 500],
idempotency_key=f"horizonte-turnos-{periodo['id']}-lote-{n}",
)
print(f"Lote {n}: {tx.data['created']} novedades", tx.data.get("warnings"))
except ValidationError as e: # 422: una fila inválida, el lote no entró
print(f"Lote {n} rechazado: {e.detail}")
except IdempotencyConflictError as e: # 409: PERIOD_CLOSED (o llave en curso)
print(f"Lote {n}: {e.detail}")import { IdempotencyConflictError, ValidationError } from "@compense/sdk";
const registros = [ // lo que exportó el sistema de turnos
{ documento: "1000000001", concepto: "HED", horas: 4, fecha: "2026-10-15" },
{ documento: "1000000001", concepto: "HRN", horas: 6, fecha: "2026-10-18" },
{ documento: "1000000002", concepto: "HEN", horas: 2, fecha: "2026-10-21" },
];
const items = registros.map((r) => ({ employee: r.documento, concept_code: r.concepto, quantity: r.horas, date: r.fecha }));
for (let i = 0, n = 1; i < items.length; i += 500, n++) {
try {
const tx = await client.admin.payroll.createNovelties({
periodId: String(periodo.id),
items: items.slice(i, i + 500),
idempotencyKey: `horizonte-turnos-${periodo.id}-lote-${n}`,
});
console.log(`Lote ${n}: ${tx.data.created} novedades`, tx.data.warnings);
} catch (err) {
if (err instanceof ValidationError || err instanceof IdempotencyConflictError) console.log(`Lote ${n}: ${err.detail}`);
else throw err;
}
}use Compense\Sdk\Exception\IdempotencyConflictException;
use Compense\Sdk\Exception\ValidationException;
$registros = [ // lo que exportó el sistema de turnos
['documento' => '1000000001', 'concepto' => 'HED', 'horas' => 4, 'fecha' => '2026-10-15'],
['documento' => '1000000001', 'concepto' => 'HRN', 'horas' => 6, 'fecha' => '2026-10-18'],
['documento' => '1000000002', 'concepto' => 'HEN', 'horas' => 2, 'fecha' => '2026-10-21'],
];
$items = array_map(fn ($r) => [
'employee' => $r['documento'], 'concept_code' => $r['concepto'], 'quantity' => $r['horas'], 'date' => $r['fecha'],
], $registros);
foreach (array_chunk($items, 500) as $n => $lote) {
try {
$tx = $client->admin->payroll->createNovelties(
periodId: $periodo['id'],
items: $lote,
idempotencyKey: "horizonte-turnos-{$periodo['id']}-lote-" . ($n + 1),
);
echo 'Lote ', $n + 1, ': ', $tx->data['created'], " novedades\n";
} catch (ValidationException | IdempotencyConflictException $e) {
echo 'Lote ', $n + 1, ': ', $e->detail, PHP_EOL;
}
}var registros = new[] // lo que exportó el sistema de turnos
{
(Documento: "1000000001", Concepto: "HED", Horas: 4, Fecha: "2026-10-15"),
(Documento: "1000000001", Concepto: "HRN", Horas: 6, Fecha: "2026-10-18"),
(Documento: "1000000002", Concepto: "HEN", Horas: 2, Fecha: "2026-10-21"),
};
var items = registros.Select(r => new Dictionary<string, object?>
{
["employee"] = r.Documento, ["concept_code"] = r.Concepto, ["quantity"] = r.Horas, ["date"] = r.Fecha,
}).ToList();
int n = 0;
foreach (var lote in items.Chunk(500))
{
n++;
try
{
Transaction tx = await client.Admin.Payroll.CreateNoveltiesAsync(
periodoId, items: lote, idempotencyKey: $"horizonte-turnos-{periodoId}-lote-{n}");
Console.WriteLine($"Lote {n}: {tx.Data["created"]} novedades");
}
catch (CompenseApiException e) when (e is ValidationException or IdempotencyConflictException)
{
Console.WriteLine($"Lote {n}: {e.Detail}");
}
}type registro struct {
Documento, Concepto, Fecha string
Horas int
}
registros := []registro{ // lo que exportó el sistema de turnos
{"1000000001", "HED", "2026-10-15", 4},
{"1000000001", "HRN", "2026-10-18", 6},
{"1000000002", "HEN", "2026-10-21", 2},
}
items := make([]map[string]any, 0, len(registros))
for _, r := range registros {
items = append(items, map[string]any{
"employee": r.Documento, "concept_code": r.Concepto, "quantity": r.Horas, "date": r.Fecha,
})
}
for i, n := 0, 1; i < len(items); i, n = i+500, n+1 {
fin := min(i+500, len(items))
tx, err := client.Admin.Payroll.CreateNovelties(ctx, &compense.PayrollNoveltiesParams{
PeriodID: periodoID, Items: items[i:fin],
}, compense.WithIdempotencyKey(fmt.Sprintf("horizonte-turnos-%s-lote-%d", periodoID, n)))
var apiErr *compense.APIError
switch {
case err == nil:
fmt.Printf("Lote %d: %v novedades\n", n, tx.Data["created"])
case errors.As(err, &apiErr) && (compense.IsValidationError(err) || compense.IsIdempotencyConflict(err)):
fmt.Printf("Lote %d: %s\n", n, apiErr.Detail)
default:
log.Fatal(err)
}
}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-turnos-$PERIODO-lote-1" \
-H "Content-Type: application/json" \
-d '{"period_id": "'"$PERIODO"'", "items": [
{"employee": "1000000001", "concept_code": "HED", "quantity": 4, "date": "2026-10-15"},
{"employee": "1000000001", "concept_code": "HRN", "quantity": 6, "date": "2026-10-18"},
{"employee": "1000000002", "concept_code": "HEN", "quantity": 2, "date": "2026-10-21"}
]}'{"status": "success", "transaction_id": "gw-…", "module": "admin",
"data": {"created": 3, "period_id": "per-O-…", "warnings": [],
"results": [{"id": "pnv-E-…", "employee": {"id": "emp-E-…", "name": "Laura Sandbox"},
"concept": {"code": "HED"}, "date": "2026-10-15", "quantity": "4",
"status": "PENDIENTE"}]}}Si el periodo ya tenía la prenómina calculada, warnings lo avisa: nómina debe recalcularla. Para
revisar lo que ya cargaste usa payroll.list_novelties(period=…, employee=…).
3. Espera la aprobación y lee los desprendibles #
Nómina calcula y aprueba el periodo en el panel. Hoy no hay webhook de aprobación: consulta el
periodo con payroll.get_period (por ejemplo, una vez por hora el día de cierre) hasta que esté
APROBADO. Luego lee los desprendibles del periodo: devengos (earnings), deducciones
(deductions) y neto (net_pay) por colaborador. Para el detalle por concepto usa
payroll.get_payslip(id). Cada consulta de desprendibles queda en la bitácora de nómina.
import time
while client.admin.payroll.get_period(periodo["id"])["status"] != "APROBADO":
time.sleep(3600)
desprendibles = client.admin.payroll.list_period_payslips(periodo["id"])
for d in desprendibles.results:
print(d["employee_doc"], d["earnings"], d["deductions"], d["net_pay"])while ((await client.admin.payroll.getPeriod(String(periodo.id))).status !== "APROBADO") {
await new Promise((r) => setTimeout(r, 3_600_000));
}
const desprendibles = await client.admin.payroll.listPeriodPayslips(String(periodo.id));
for (const d of desprendibles.results) console.log(d.employee_doc, d.earnings, d.deductions, d.net_pay);while ($client->admin->payroll->getPeriod($periodo['id'])['status'] !== 'APROBADO') {
sleep(3600);
}
$desprendibles = $client->admin->payroll->listPeriodPayslips($periodo['id']);
foreach ($desprendibles->results as $d) {
echo $d['employee_doc'], ' ', $d['earnings'], ' ', $d['deductions'], ' ', $d['net_pay'], PHP_EOL;
}while ((await client.Admin.Payroll.GetPeriodAsync(periodoId))["status"]!.GetValue<string>() != "APROBADO")
await Task.Delay(TimeSpan.FromHours(1));
Page desprendibles = await client.Admin.Payroll.ListPeriodPayslipsAsync(periodoId);
foreach (JsonObject d in desprendibles.Results)
Console.WriteLine($"{d["employee_doc"]} {d["earnings"]} {d["deductions"]} {d["net_pay"]}");for {
p, err := client.Admin.Payroll.GetPeriod(ctx, periodoID)
if err != nil {
log.Fatal(err)
}
if p["status"] == "APROBADO" {
break
}
time.Sleep(time.Hour)
}
desprendibles, err := client.Admin.Payroll.ListPeriodPayslips(ctx, periodoID, nil)
if err != nil {
log.Fatal(err)
}
for _, d := range desprendibles.Results {
fmt.Println(d["employee_doc"], d["earnings"], d["deductions"], d["net_pay"])
}curl https://sandbox.dev.compense.co/gw/v1/gateway/admin/payroll/periods/$PERIODO/ \
-H "X-API-Key: $COMPENSE_API_KEY"
curl https://sandbox.dev.compense.co/gw/v1/gateway/admin/payroll/periods/$PERIODO/payslips/ \
-H "X-API-Key: $COMPENSE_API_KEY"4. Revisa la nómina electrónica #
Nómina genera y transmite la nómina electrónica a la DIAN desde el panel. Por API consultas el estado del mes: proveedor, resumen por estado y los documentos. Úsalo para alertar si quedó algún documento rechazado antes de causar.
ne = client.admin.payroll.electronic(year=2026, month=10)
print(ne.raw.get("provider"), ne.raw.get("summary")) # p. ej. {'ACEPTADO': 2}
for doc in ne.results:
print(doc)const ne = await client.admin.payroll.electronic({ year: 2026, month: 10 });
console.log(ne.raw.provider, ne.raw.summary, ne.results);$ne = $client->admin->payroll->electronic(year: 2026, month: 10);
echo $ne->raw['provider'] ?? '-', ' ', json_encode($ne->raw['summary'] ?? []), PHP_EOL;Page ne = await client.Admin.Payroll.ElectronicAsync(2026, 10);
Console.WriteLine($"{ne.Raw["provider"]} {ne.Raw["summary"]}");ne, err := client.Admin.Payroll.Electronic(ctx, &compense.ElectronicPayrollParams{Year: 2026, Month: 10})
if err != nil {
log.Fatal(err)
}
fmt.Println(ne.Raw["provider"], ne.Raw["summary"], len(ne.Results))curl "https://sandbox.dev.compense.co/gw/v1/gateway/admin/payroll/electronic/?year=2026&month=10" \
-H "X-API-Key: $COMPENSE_API_KEY"
# {"year": 2026, "month": 10, "enabled": true, "provider": "…", "summary": {"ACEPTADO": 2}, "results": []}Los SDK devuelven esta lectura como una página: los documentos están en results y el resumen y el
proveedor, en el cuerpo crudo (raw).
5. Causa el asiento en borrador #
Con los desprendibles aprobados, arma el comprobante de causación: el gasto de nómina al débito y,
al crédito, las deducciones y el neto por pagar. Si en la misma causación incluyes servicios de
terceros (por ejemplo, el operador de nómina electrónica), calcula antes su retención con el
simulador de impuestos, que es una consulta pura: no escribe y no lleva Idempotency-Key.
El comprobante entra en DRAFT y su external_id (tu id en el ERP) lo hace idempotente en el
motor: si reenvías el mismo external_id, recibes el comprobante existente (created: false) en vez
de uno nuevo.
from decimal import Decimal
dev = sum(Decimal(d["earnings"]) for d in desprendibles.results)
ded = sum(Decimal(d["deductions"]) for d in desprendibles.results)
neto = sum(Decimal(d["net_pay"]) for d in desprendibles.results)
# Retención del operador de nómina electrónica (servicio de un tercero)
ret = client.admin.accounting.compute_taxes(
items=[{"concept": "RF_SERVICIOS", "base": "350000"}], date="2026-10-31", third_party="900123456",
).results[0]
retencion = Decimal(ret["amount"])
tx = client.admin.accounting.create_entry(
date="2026-10-31",
description="Causación nómina octubre 2026",
lines=[
{"account": "510506", "debit": str(dev), "description": "Sueldos"},
{"account": "237005", "credit": str(ded), "description": "Deducciones de nómina"},
{"account": "250505", "credit": str(neto), "description": "Salarios por pagar"},
{"account": "513595", "debit": "350000", "third_party": "900123456", "description": "Operador nómina electrónica"},
{"account": "236525", "credit": str(retencion), "third_party": "900123456", "tax_concept": "RF_SERVICIOS", "base_amount": "350000"},
{"account": "233595", "credit": str(Decimal("350000") - retencion), "third_party": "900123456"},
],
external_id="HORIZONTE-ERP-NOM-2026-10",
voucher_type="CC",
idempotency_key="horizonte-erp-nom-2026-10",
)
print(tx.data["id"], tx.data["status"], tx.data.get("created")) # … DRAFT Trueconst suma = (k: string) => desprendibles.results.reduce((s, d) => s + Number(d[k]), 0);
const [dev, ded, neto] = [suma("earnings"), suma("deductions"), suma("net_pay")];
// Retención del operador de nómina electrónica (servicio de un tercero)
const calculo = await client.admin.accounting.computeTaxes({
items: [{ concept: "RF_SERVICIOS", base: "350000" }],
date: "2026-10-31",
thirdParty: "900123456",
});
const retencion = Number(calculo.results[0].amount);
const tx = await client.admin.accounting.createEntry({
date: "2026-10-31",
description: "Causación nómina octubre 2026",
lines: [
{ account: "510506", debit: dev.toFixed(2), description: "Sueldos" },
{ account: "237005", credit: ded.toFixed(2), description: "Deducciones de nómina" },
{ account: "250505", credit: neto.toFixed(2), description: "Salarios por pagar" },
{ account: "513595", debit: "350000", third_party: "900123456", description: "Operador nómina electrónica" },
{ account: "236525", credit: retencion.toFixed(2), third_party: "900123456", tax_concept: "RF_SERVICIOS", base_amount: "350000" },
{ account: "233595", credit: (350000 - retencion).toFixed(2), third_party: "900123456" },
],
fields: { external_id: "HORIZONTE-ERP-NOM-2026-10", voucher_type: "CC" },
idempotencyKey: "horizonte-erp-nom-2026-10",
});
console.log(tx.data.id, tx.data.status, tx.data.created);$suma = fn (string $k) => array_sum(array_map(fn ($d) => (float) $d[$k], $desprendibles->results));
$fmt = fn (float $v) => number_format($v, 2, '.', '');
[$dev, $ded, $neto] = [$suma('earnings'), $suma('deductions'), $suma('net_pay')];
// Retención del operador de nómina electrónica (servicio de un tercero)
$calculo = $client->admin->accounting->computeTaxes(
items: [['concept' => 'RF_SERVICIOS', 'base' => '350000']], date: '2026-10-31', thirdParty: '900123456',
);
$retencion = (float) $calculo->results[0]['amount'];
$tx = $client->admin->accounting->createEntry(
date: '2026-10-31',
description: 'Causación nómina octubre 2026',
lines: [
['account' => '510506', 'debit' => $fmt($dev), 'description' => 'Sueldos'],
['account' => '237005', 'credit' => $fmt($ded), 'description' => 'Deducciones de nómina'],
['account' => '250505', 'credit' => $fmt($neto), 'description' => 'Salarios por pagar'],
['account' => '513595', 'debit' => '350000', 'third_party' => '900123456', 'description' => 'Operador nómina electrónica'],
['account' => '236525', 'credit' => $fmt($retencion), 'third_party' => '900123456', 'tax_concept' => 'RF_SERVICIOS', 'base_amount' => '350000'],
['account' => '233595', 'credit' => $fmt(350000 - $retencion), 'third_party' => '900123456'],
],
idempotencyKey: 'horizonte-erp-nom-2026-10',
external_id: 'HORIZONTE-ERP-NOM-2026-10',
voucher_type: 'CC',
);
echo $tx->data['id'], ' ', $tx->data['status'], PHP_EOL;decimal Suma(string k) => desprendibles.Results.Sum(d => decimal.Parse(d[k]!.GetValue<string>(), CultureInfo.InvariantCulture));
string F(decimal v) => v.ToString("0.00", CultureInfo.InvariantCulture);
decimal dev = Suma("earnings"), ded = Suma("deductions"), neto = Suma("net_pay");
// Retención del operador de nómina electrónica (servicio de un tercero)
Page calculo = await client.Admin.Accounting.ComputeTaxesAsync(
new[] { new Dictionary<string, object?> { ["concept"] = "RF_SERVICIOS", ["base"] = "350000" } },
date: "2026-10-31", thirdParty: "900123456");
decimal retencion = decimal.Parse(calculo.Results[0]["amount"]!.GetValue<string>(), CultureInfo.InvariantCulture);
Transaction tx = await client.Admin.Accounting.CreateEntryAsync(
"2026-10-31",
"Causación nómina octubre 2026",
new[]
{
new Dictionary<string, object?> { ["account"] = "510506", ["debit"] = F(dev), ["description"] = "Sueldos" },
new Dictionary<string, object?> { ["account"] = "237005", ["credit"] = F(ded), ["description"] = "Deducciones de nómina" },
new Dictionary<string, object?> { ["account"] = "250505", ["credit"] = F(neto), ["description"] = "Salarios por pagar" },
new Dictionary<string, object?> { ["account"] = "513595", ["debit"] = "350000", ["third_party"] = "900123456", ["description"] = "Operador nómina electrónica" },
new Dictionary<string, object?> { ["account"] = "236525", ["credit"] = F(retencion), ["third_party"] = "900123456", ["tax_concept"] = "RF_SERVICIOS", ["base_amount"] = "350000" },
new Dictionary<string, object?> { ["account"] = "233595", ["credit"] = F(350000m - retencion), ["third_party"] = "900123456" },
},
new Dictionary<string, object?> { ["external_id"] = "HORIZONTE-ERP-NOM-2026-10", ["voucher_type"] = "CC" },
idempotencyKey: "horizonte-erp-nom-2026-10");
Console.WriteLine($"{tx.Data["id"]} {tx.Data["status"]}");suma := func(k string) float64 {
t := 0.0
for _, d := range desprendibles.Results {
s, _ := d[k].(string)
v, _ := strconv.ParseFloat(s, 64)
t += v
}
return t
}
f := func(v float64) string { return strconv.FormatFloat(v, 'f', 2, 64) }
dev, ded, neto := suma("earnings"), suma("deductions"), suma("net_pay")
// Retención del operador de nómina electrónica (servicio de un tercero)
calculo, err := client.Admin.Accounting.ComputeTaxes(ctx, &compense.ComputeTaxesParams{
Items: []map[string]any{{"concept": "RF_SERVICIOS", "base": "350000"}}, Date: "2026-10-31", ThirdParty: "900123456",
})
if err != nil {
log.Fatal(err)
}
retStr, _ := calculo.Results[0]["amount"].(string)
retencion, _ := strconv.ParseFloat(retStr, 64)
asiento, err := client.Admin.Accounting.CreateEntry(ctx, &compense.EntryParams{
Date: "2026-10-31",
Description: "Causación nómina octubre 2026",
Lines: []map[string]any{
{"account": "510506", "debit": f(dev), "description": "Sueldos"},
{"account": "237005", "credit": f(ded), "description": "Deducciones de nómina"},
{"account": "250505", "credit": f(neto), "description": "Salarios por pagar"},
{"account": "513595", "debit": "350000", "third_party": "900123456", "description": "Operador nómina electrónica"},
{"account": "236525", "credit": f(retencion), "third_party": "900123456", "tax_concept": "RF_SERVICIOS", "base_amount": "350000"},
{"account": "233595", "credit": f(350000 - retencion), "third_party": "900123456"},
},
Fields: map[string]any{"external_id": "HORIZONTE-ERP-NOM-2026-10", "voucher_type": "CC"},
}, compense.WithIdempotencyKey("horizonte-erp-nom-2026-10"))
if err != nil {
log.Fatal(err)
}
fmt.Println(asiento.Data["id"], asiento.Data["status"])# 1) Simular la retención (sin Idempotency-Key)
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-31", "third_party": "900123456", "items": [{"concept": "RF_SERVICIOS", "base": "350000"}]}'
# 2) Causar en borrador
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-nom-2026-10" \
-H "Content-Type: application/json" \
-d '{"external_id": "HORIZONTE-ERP-NOM-2026-10", "voucher_type": "CC", "date": "2026-10-31",
"description": "Causación nómina octubre 2026",
"lines": [
{"account": "510506", "debit": "8000000.00", "description": "Sueldos"},
{"account": "237005", "credit": "640000.00", "description": "Deducciones de nómina"},
{"account": "250505", "credit": "7360000.00", "description": "Salarios por pagar"},
{"account": "513595", "debit": "350000", "third_party": "900123456"},
{"account": "236525", "credit": "14000.00", "third_party": "900123456", "tax_concept": "RF_SERVICIOS", "base_amount": "350000"},
{"account": "233595", "credit": "336000.00", "third_party": "900123456"}
]}'Las cuentas del ejemplo son ilustrativas: usa las de tu plan de cuentas. El comprobante debe cuadrar
(débitos = créditos) para que el contador pueda contabilizarlo; el panel valida además cuentas de
movimiento, tercero y centro de costo obligatorios, y periodo abierto. post: true responde
422 POST_NOT_ALLOWED.
6. Sigue el comprobante hasta que se contabilice #
El comprobante del ERP lleva source_module: "gateway" y el panel no lo edita: si el contador
encuentra un error, lo anula y tú lo corriges en el ERP y lo reenvías con el mismo external_id.
Para saber si ya se contabilizó, consulta por external_id:
entradas = client.admin.accounting.list_entries(external_id="HORIZONTE-ERP-NOM-2026-10", source="gateway")
for e in entradas.results:
print(e["id"], e["status"], e.get("number"))const entradas = await client.admin.accounting.listEntries({ externalId: "HORIZONTE-ERP-NOM-2026-10", source: "gateway" });
for (const e of entradas.results) console.log(e.id, e.status, e.number);$entradas = $client->admin->accounting->listEntries(externalId: 'HORIZONTE-ERP-NOM-2026-10', source: 'gateway');
foreach ($entradas->results as $e) {
echo $e['id'], ' ', $e['status'], PHP_EOL;
}Page entradas = await client.Admin.Accounting.ListEntriesAsync(externalId: "HORIZONTE-ERP-NOM-2026-10", source: "gateway");
foreach (JsonObject e in entradas.Results) Console.WriteLine($"{e["id"]} {e["status"]}");entradas, err := client.Admin.Accounting.ListEntries(ctx, &compense.EntryListParams{
ExternalID: "HORIZONTE-ERP-NOM-2026-10", Source: "gateway",
})
if err != nil {
log.Fatal(err)
}
for _, e := range entradas.Results {
fmt.Println(e["id"], e["status"])
}curl "https://sandbox.dev.compense.co/gw/v1/gateway/admin/accounting/entries/?external_id=HORIZONTE-ERP-NOM-2026-10&source=gateway" \
-H "X-API-Key: $COMPENSE_API_KEY"Eventos #
| Evento | Cuándo | Úsalo para |
|---|---|---|
admin.payroll.novelty.received | Se cargó cada novedad de nómina | Marcar en el sistema de turnos qué registros ya entraron (novelty_id, employee_id, concept_code, period_id) |
admin.accounting.entry.drafted | Se creó el comprobante en borrador | Guardar el entry_id junto a tu external_id |
Los eventos se emiten en la misma transacción que el registro y su event_id sale de la
Idempotency-Key: un reintento no duplica el callback. Ninguno significa que se movió dinero. El
catálogo completo está en Eventos.
Pruébalo en el sandbox #
| Para probar | Haz esto |
|---|---|
| Periodo abierto | payroll.list_periods devuelve per-O-… (octubre 2026, ABIERTO) |
| Periodo cerrado | Carga novedades a per-C-… (septiembre 2026, APROBADO) → 409 PERIOD_CLOSED |
| Fila inválida | Un concepto del sistema (SALARIO, SALUD…) o una fecha fuera de octubre → 422 y el lote no entra |
| Colaborador inexistente | employee 9999999999 → 422 en la fila |
| Desprendibles | Cualquier periodo per-…: Laura Sandbox (1000000001) y Andrés Sandbox (1000000002) |
| Nómina electrónica | payroll.electronic(year=2026, month=10) → resumen ACEPTADO |
| Simulador | Aplica una tarifa fija del 4 % a cada base |
| Comprobante | Siempre DRAFT; mismo external_id → mismo id; post: true → 422 |
En el sandbox el periodo de octubre se queda ABIERTO (no hay quién lo apruebe): para probar el
paso 3 sin esperar, lee los desprendibles directamente.
Lista de verificación #
- Cargas novedades solo a periodos
ABIERTOoCALCULADOy manejas409 PERIOD_CLOSED. - Lotes de hasta 500 con una
Idempotency-Keypor periodo y lote. - Ante un
422corriges la fila señalada y reenvías todo el lote. - Esperas
APROBADOantes de causar y revisas la nómina electrónica del mes. - El simulador de impuestos se llama sin
Idempotency-Key. - El comprobante cuadra, entra en
DRAFTy lleva unexternal_idestable de tu ERP.