Agent-first CLI for SUNAT tax automation in Peru. Built for AI agents as primary consumers, humans as supervisors.
| Domain | Coverage | Surface |
|---|---|---|
| REST APIs (Consulta CPE, Padrón RUC, Tipo Cambio SBS) | 90% | OAuth2 client_credentials + scrape (cpe consulta, padron, tipo-cambio) |
| RHE + F616 (personas naturales, RUC 10) | 95% | agent-browser, no CAPTCHA |
| CPE Factura / Boleta / NC / ND | 85% | UBL 2.1 + XAdES-BES + SOAP directo |
| Resumen Diario + Comunicación de Baja | 80% | sendSummary + ticket polling |
| SIRE (RVIE Ventas + RCE Compras) | 70% | propuesta, ticket, descarga ZIP, TUS 1.0.0 import |
| GRE Remitente / Transportista | 50% | REST + JWT, modal 02 shipped |
| Drivers (mock, sunat-direct, facturador, PSE/OSE) | 40% | 2 of 5 — facturador / nubefact / apisperu shaped |
| CPE void T3 | 30% | shaped, intent-token flow pending |
| Producción end-to-end | 10% | beta-only — never run prod blind |
Overall: ~61% agent-ready across 9 SUNAT surfaces. Live coverage breakdown + roadmap on the website.
- Bun v1.2+
- agent-browser v0.22+ (only for RHE / F616)
- Chrome or Chromium (CDP transport for agent-browser)
- For CPE / SIRE / GRE: a SUNAT digital certificate (PFX) + clave SOL
bun add -g @crafter/sunat-cliRun sunat-cli --help after installing. That list is the contract: every command
it prints is available in the version you have.
Every section below is tagged npm, meaning it ships in the published release.
A section tagged source would exist on main only. Right now nothing is
source-only: the published release matches main.
# Health check + driver info
sunat-cli cpe doctor --output json
sunat-cli cpe info
# Schema introspection (agents self-serve)
sunat-cli schema cpe-factura
# Always preview (T0 — no side effects)
sunat-cli cpe factura preview --params @factura.json
# Emit (T2 — requires --yes)
sunat-cli cpe factura emit --params @factura.json --yes
# Boleta / NC / ND — same shape
sunat-cli cpe boleta emit --params @boleta.json --yes
sunat-cli cpe nc emit --params @nota.json --yes
sunat-cli cpe nd emit --params @nota.json --yes
# Resumen diario (boletas <S/700)
sunat-cli cpe resumen send --fecha 2026-04-29 --yes
sunat-cli cpe resumen status --ticket 12345...
# Comunicación de Baja
sunat-cli cpe baja send --params @baja.json --yes
# GRE — Guía de Remisión Remitente (modal 02)
sunat-cli cpe gre emit --params @guia.json --yes
sunat-cli cpe gre status --ticket 12345...
# Consulta CPE (validación post-emisión, propia o de un proveedor)
sunat-cli cpe consulta --tipo 01 --serie F001 --numero 1234# RVIE (Ventas)
sunat-cli sire ventas periodos
sunat-cli sire ventas propuesta --periodo 202504
sunat-cli sire ventas ticket --id <ticket-id>
sunat-cli sire ventas archivo --ticket <ticket-id> # downloads ZIP
# RCE (Compras)
sunat-cli sire compras propuesta --periodo 202504
sunat-cli sire compras importar --periodo 202504 --file ./compras.zip --yes# OAuth2 token for the SUNAT REST APIs
sunat-cli api token# Padrón RUC: download once (~370MB ZIP), then look up locally
sunat-cli padron status
sunat-cli padron sync
sunat-cli padron ruc 20123456789
sunat-cli padron batch --file ./rucs.csv
# Single RUC without syncing the padrón (drives the portal, ~5-10s)
sunat-cli padron ruc-online 20123456789
# Tipo de Cambio SBS
sunat-cli tipo-cambio --fecha hoy
sunat-cli tipo-cambio --fecha 2026-04-29 --output jsonSecrets resolve from the environment first, then the OS keychain. Existing env-var setups keep working unchanged.
# Omit --value on a terminal and it prompts without echo, so the secret
# never lands in shell history
sunat-cli keychain set CPE_CERT_PASSWORD
sunat-cli keychain list
sunat-cli keychain clear CPE_CERT_PASSWORDsunat-cli cpe profile set --name acme --ruc 20123456789 --razon-social "ACME SAC" --mode beta
sunat-cli cpe profile list
sunat-cli cpe profile use acme# Inspect, optionally scoped to one emisor
sunat-cli audit list --ruc 20123456789 --limit 20
# Archive audit months older than the retention window
sunat-cli audit compact
# Delete archived months before a cutoff. Never runs on its own
sunat-cli audit prune --before 2025-01# Login (browser, no CAPTCHA)
sunat-cli login
# Schema
sunat-cli schema rhe
# Dry-run first
sunat-cli rhe emit --dry-run --json '{"empresa":"Acme Corp.","monto":5000,"moneda":"PEN","medioPago":"TRANSFERENCIA","tipoDoc":"SIN DOCUMENTO","descripcion":"Servicios"}'
# Batch emit
sunat-cli rhe emit --batch ./data/example.csv
# F616 mensual (periodo en YYYY-MM). El ingreso lo precarga SUNAT desde tus
# RHE registrados: el CLI no lo escribe, asi que verificalo en el portal
sunat-cli f616 declare --dry-run --json '{"periodo":"2026-03"}'
sunat-cli f616 declare --json '{"periodo":"2026-03"}'
# Varios periodos de una
sunat-cli f616 declare --batch --months 2025-03..2026-02 --dry-run
# Lecturas del F616 por API, headless (abre el navegador solo si el token vencio)
sunat-cli f616 periodo 2026-03
sunat-cli f616 oficiosFollows Agent DX principles and the agentskills.io specification.
--params @file.jsonover bespoke flags--dry-runfor all mutations (T0/T1)--yesconfirmation gate for irreversible ops (T2)- Intent-token flow for destructive ops (T3, e.g.
cpe void) --output jsonby default; NDJSON when pipedschema <command>introspection at runtime (25+ schemas)- Two-phase audit (
pending→success/error) on every write - Idempotency cache by natural key
RUC-tipo-serie-numero - Input hardening against agent hallucinations
SUNAT's monthly-declaration form looks server-rendered and behaves like one: driving the DOM never works, because the fields stay disabled until a background call returns. Underneath sits a JSON API, and reaching it needs a session token (IdCache) the portal mints only during its own browser login. A self-registered API client can never request that audience.
So the CLI captures the token once from the browser, then reads the API headless for its hour of life. f616 periodo and f616 oficios open the browser only when the cached token is missing or expired; otherwise they are plain HTTP. Verified with zero browser processes running.
The OAuth2 surfaces (SIRE, GRE, CPE, buzón) need no browser at all: their token comes from a password grant against api-seguridad.sunat.gob.pe with a client registered from the SOL menu.
End-to-end verified against e-beta.sunat.gob.pe (2026-04-29):
- Factura ·
cdrCode=0Aceptado, hash deterministic - Boleta ·
cdrCode=0Aceptado - Nota Crédito · FC01-555 Aceptado (Catálogo 09)
- Nota Débito · FD01-777 Aceptado (Catálogo 10)
- GRE Remitente modal 02 · ticket polling + JWT refresh ok
- SIRE RVIE propuesta · ZIP descarga, parse correcto
Producción (e-factura.sunat.gob.pe): never run blind. Always preview first, then emit --dry-run, then --yes only with real cert + RUC. See packages/cli/LIMITATIONS.md for the full list of known boundaries.
Tracked in GitHub issues — milestones board.
| Priority | Issues |
|---|---|
| P0 — Now | #18 Live verification against production |
| P1 — Next | #10 cpe void T3 + safety rail · #12 Drivers nubefact + apisperu · #11 GRE modal 01 + Transportista |
| P2 — Later | #13 Driver facturador · #14 SIRE reportes complementarios · #15 sqlite padrón index · #16 CI smoke jobs · #17 TUS auto-resume |
| P3 — Backlog | #30 F.1683 arrendamiento |
3 SUNAT portals reverse-engineered. F616 input mask cracked via raw CDP WebSocket. reCAPTCHA bypassed through OAuth state exploitation. UBL 2.1 + XAdES-BES manually implemented (xml-crypto v6 had nested-signature quirks).
Findings:
packages/cli/src/commands/cpe/RESEARCH.md— CPE ecosystem dossierpackages/cli/LIMITATIONS.md— single source of truth for known boundaries
packages/
cli/ @crafter/sunat-cli — the CLI
website/ sunat-cli.crafter.ing landing
Publishing is automatic. Bump version in packages/cli/package.json and merge
that PR to main. The release workflow then runs the test suite, publishes to
npm with OIDC trusted publishing (no token stored in the repo), installs the
published tarball to confirm it runs and exposes the expected commands, and
finally tags the commit and opens a GitHub Release.
A merge that leaves version untouched publishes nothing.
MIT — Crafter Station, Lima, Peru