Errors
Simplo API error codes, SDK error classes by HTTP status, and the automatic retry rules.
Every SDK failure throws a subclass of SimploError. Catch narrow when you
can act on the case, broad otherwise:
import Simplo, { RateLimitError, SimploError, ValidationError } from '@simplohq/sdk';
const simplo = new Simplo();
try { await simplo.dtes.emit(params);} catch (error) { if (error instanceof ValidationError) { // Bad input — fix the request, don't retry it as-is. for (const issue of error.details ?? []) { console.error(`${issue.field}: ${issue.code} — ${issue.message}`); } } else if (error instanceof RateLimitError) { // The SDK already retried; back off the caller. console.error(`Rate limited. Retry in ${error.retryAfter ?? '?'}s`); } else if (error instanceof SimploError) { console.error(error.status, error.code, error.message, error.requestId); } else { throw error; // programming error — don't swallow it }}Error properties
Section titled “Error properties”| Property | Type | Description |
|---|---|---|
status |
number | undefined |
HTTP status (absent on connection failures) |
code |
string | undefined |
Machine-readable API code, e.g. "VALIDATION_ERROR" |
message |
string |
Human-readable description |
requestId |
string | undefined |
Request ID — include it when contacting support |
details |
ValidationIssue[] |
Field-level issues (field, code, message) on validation errors |
action |
string | undefined |
Recommended next step from the API, when available |
docUrl |
string |
Link to the relevant documentation |
Error classes by status
Section titled “Error classes by status”| Status | Class | Retry? | Typical causes |
|---|---|---|---|
| 400/422 | ValidationError |
No — fix the request | Invalid RUT check digit, missing required fields |
| 401 | AuthenticationError |
No — fix credentials | Wrong or revoked API key (Authentication) |
| 403 | PermissionError |
No | The key lacks the required permission or role |
| 404 | NotFoundError |
No | Wrong ID, or a resource from another company |
| 409 | ConflictError |
Depends (see below) | Duplicate folio; idempotency key still in flight |
| 429 | RateLimitError |
Automatic | Too many requests (limits are per company) |
| 5xx | ServerError |
Automatic (when safe) | Transient API issue |
| — | ConnectionError |
Automatic (when safe) | Network failure, DNS, timeout |
| — | WebhookVerificationError |
No — reject delivery | Bad signature, stale timestamp (Webhooks) |
The error envelope, layer by layer
Section titled “The error envelope, layer by layer”Errors come from two layers, and their envelopes are not identical:
- Platform (auth, allowlist, rate limit, required idempotency):
{ code, message, error, severity, action?, context? }. - Invoicing engine (emission validation, folios, states): proxied through
with its own shape —
code,message,action, plusseverity/contextwhere applicable.
Robust handling rules: branch on HTTP status + code when present,
tolerate unknown fields, and never parse message as if it were a
contract. Two real behaviors to keep in mind:
- Some engine validations currently answer with a raw message and a generic
code — e.g. omitting
ind_servicioon a boleta yields a400with"IndServicio is required for boleta", not a catalog code. And in production some typed engine errors may arrive as plain text where the sandbox returns the structured code (RUT_INVALIDO,CAF_NO_DISPONIBLE) — a known divergence, fix pending. Design your handling to work from the status alone. razon_sociallengths have two layers: the engine enforces the SII cap (100 characters, receptor and emitter) and answers400 RAZON_SOCIAL_INVALIDAnaming the field and the limit; even longer strings (> 200) are cut earlier by the platform’s shape validation with a generic400{"code":"BAD_REQUEST","message":"invalid request"}without naming the field — if you see that one, check your string lengths first.
Codes you’ll see as an integrator
Section titled “Codes you’ll see as an integrator”| Code | Description |
|---|---|
401 |
Invalid, revoked or expired credential. The response doesn’t distinguish why (anti-enumeration): check the header and the sk_simplo_ prefix. |
409 IDEMPOTENCY_IN_PROGRESS |
The same Idempotency-Key is still being processed. Wait and check the status instead of retrying immediately. |
409 (duplicate folio) |
Folio conflict on emission. |
422 IDEMPOTENCY_BODY_MISMATCH |
You reused an Idempotency-Key with a body different from the original. Use a new key per distinct emission. |
422 BILLING_METADATA_NOT_SUPPORTED |
You sent metadata with content on emission. The field is not persisted yet, so we reject it instead of dropping it silently — remove it and correlate against the id the emission returns. {} and null do pass. See Emission. |
429 |
Rate limit: 60 calls/min per company on API-key billing (120/min on MCP). Wait and retry — see Limits. |
503 (idempotency) |
The idempotency service was temporarily unavailable during emission. Retry with the same key. |
Emission validation codes (invoicing engine)
Section titled “Emission validation codes (invoicing engine)”Validations that run before a folio is allocated — the same emission path
in sandbox and production, so one of these 400s in sandbox is exactly what
you would see in production (where they additionally prevent burning a real
folio on a document the SII would reject):
| Code | Status | What to do |
|---|---|---|
EMISOR_INCOMPLETO |
400 | Your company is missing giro, address or comuna (context.missing_fields lists which). The SII requires these emitter fields in the XML (GiroEmis/DirOrigen/CmnaOrigen) — complete them in the app or via API and retry. Applies to every type, boletas included. |
RAZON_SOCIAL_INVALIDA |
400 | razon_social (the receptor’s or your company’s) exceeds the SII schema’s 100-character cap (RznSocLargaType). Shorten it; the message carries the received length. |
MONTO_INVALIDO |
400 | Negative quantity/price, or a final monto_total ≤ 0 on a document that carries amounts. Fix the lines — the exact field comes in context.field. |
REFERENCIA_INVALIDA |
400 | A credit/debit note (56/61 and also the export ones, 111/112) references a type+folio that does not exist for your company. Check tipo_doc_ref/folio_ref against the original document. |
RECEPTOR_INCOMPLETO |
400 | Invoice-family documents (33/34/43/46/56/61) require the receiver’s direccion and comuna — the real SII rejects without them (HED-3-845/HED-3-846). context.missing_fields lists what’s missing. Boletas (39/41) and export documents (110/111/112) don’t require them. |
DETALLE_DESCUADRADO |
400 | You sent a monto_item that doesn’t match cantidad × precio (± discounts/surcharges, ±1 peso rounding tolerance). The error carries the expected and received value per line. To set a final line amount directly, omit precio and send only monto_item. |
IND_TRASLADO_REQUERIDO |
400 | Dispatch guides (52) require ind_traslado per the SII table (1=sale … 9=export sale). Note: a sale guide (1 or 9) must also carry amounts — with price 0 it responds MONTO_INVALIDO. |
EXPORT_TPO_CAMBIO_REQUERIDO |
400 | Export document in a foreign currency without tpo_cambio: the real SII rejects the 1:1 fallback (“Los Valores del Encabezado No Cuadran”). Send tpo_cambio (> 0); an explicit mnt_export or forma_pago_exp: 21 (no payment) remain valid without a rate. |
Platform codes (API keys and sandbox)
Section titled “Platform codes (API keys and sandbox)”These codes come from the Simplo platform (not the invoicing engine) and always have a clear way out:
| Code | Status | What to do |
|---|---|---|
BILLING_API_KEY_ROUTE_NOT_ALLOWED |
403 | The route is session-only. A billing:dte key only reaches the 6 DTE routes — see the quickstart’s table. It’s not an unactivated account. |
BILLING_API_KEY_IDEMPOTENCY_KEY_REQUIRED |
400 | Add the Idempotency-Key header to your key-authenticated POST (/billing/dte and /billing/dte/send-boletas). |
BILLING_API_KEY_NOT_ENABLED |
403 | Your company doesn’t have production emission by key enabled — write to [email protected]. The sandbox doesn’t need this. |
BILLING_SANDBOX_API_KEY_NOT_ENABLED |
403 | Your plan doesn’t include the invoicing sandbox. |
COMPANY_RUT_REQUIRED_FOR_SANDBOX |
422 | When creating a sandbox key: your company doesn’t have a RUT yet — complete it in the app and retry. |
COMPANY_RAZON_SOCIAL_REQUIRED_FOR_SANDBOX |
422 | When creating a sandbox key: the business name (razón social) is missing — complete it in the app and retry. |
SANDBOX_COMPANY_FLOW_REMOVED |
410 | You called the old POST /api/v1/sandbox/companies flow, which no longer exists: the sandbox is a mode of your own company — create a sandbox key under Developers. |
SANDBOX_NOT_AVAILABLE |
503 | The sandbox is unavailable right now — try again later. |
Automatic retries
Section titled “Automatic retries”The SDK retries on 429, 5xx and network errors with exponential backoff
and jitter, respecting the Retry-After header. Default: 2 retries;
override with maxRetries on the client or per request.
Safety rule: a POST is only retried when it carries an
Idempotency-Key. dtes.emit generates one automatically (UUID v4), so
emission retries can never duplicate a document. Other POSTs
(folios.upload, webhooks.create) aren’t retried automatically — call
them again yourself if needed; both are safe to re-run (a duplicate CAF
returns ConflictError).
const simplo = new Simplo({ maxRetries: 3, timeout: 30_000 });await simplo.dtes.list({}, { maxRetries: 0 }); // per-request override