Ir al contenido

Primeros pasos

Este recorrido emite una boleta en Pruebas (test), sin valor fiscal. Usa HTTP con curl, sirve para cualquier lenguaje y no necesita un SDK; al final está el mismo paso en cURL, PHP, Python, C#, Java y TypeScript. El contrato completo es la referencia de /v1, que también se descarga sin API key en https://api.boletura.com/openapi/v1.json.

Necesitas una cuenta de Boletura con un plan que incluya la API: cómo pedirla.

Antes de llamar a la API, un Propietario (owner) o Administrador (admin) de la cuenta prepara cada empresa en el back office:

  1. En Empresas, registra RUC, razón social y domicilio fiscal con ubigeo. El alta crea el establecimiento principal 0000. Agrega los establecimientos anexos que correspondan. Para ensayar no hace falta un RUC real: en test solo se comprueban su formato y su dígito verificador, y 20000000010 los cumple.
  2. En la pestaña Series de la empresa, crea una serie de ambiente Pruebas por tipo y establecimiento que usarás: por ejemplo B001 para boletas y F001 para facturas. La primera de cada tipo queda por defecto. Para notas de crédito y de débito, prepara series con la letra del documento afectado, por ejemplo BC01 y FC01 para las de crédito y BD01 y FD01 para las de débito.

En test no registras certificado ni credenciales SOL: la plataforma firma con su certificado de prueba y envía a SUNAT beta con las credenciales públicas de beta. Conectar con SUNAT es solo para live.

El canal actual es SUNAT directo: facturas individuales y boletas por resumen diario. Antes de live, Boletura comprueba que ese canal sirve para cada RUC; no se admite una empresa obligada a OSE. Dar de alta una empresa en la plataforma no la inscribe ni autoriza por sí solo ante SUNAT.

En Configuración → API keys, con rol owner o admin, crea una clave de ambiente Pruebas con estos scopes, que ya vienen marcados:

Scope Para qué
companies:read Resolver empresas y series
documents:write Emitir, crear notas y solicitar bajas
documents:read Consultar y descargar

Si vas a emitir para varios RUC, elige Sin empresa por defecto y manda siempre company_id. Una empresa por defecto solo permite omitirlo; no restringe la clave a esa empresa. La clave accede a las empresas de su cuenta según sus scopes.

Copia la clave completa bol_test_…: solo se muestra una vez. Guárdala en el gestor de secretos de tu servidor, nunca en código de navegador, una app móvil, Git ni logs. La API usa Authorization: Bearer; la sesión del back office no sirve para llamarla.

La clave determina la cuenta y el ambiente. No existe un parámetro que convierta una clave test en live, ni sirve cambiarle el prefijo. Al rotar, API keys → Rotar entrega otra clave con los mismos permisos y una gracia de 24 horas por defecto: despliega la nueva, comprueba que funciona y revoca la anterior. Las claves de idempotencia siguen valiendo al rotar dentro de la misma cuenta y el mismo ambiente.

Los bloques se ejecutan en la misma terminal Bash (Git Bash en Windows), en una carpeta de trabajo privada fuera de Git. Necesitas curl y Python 3, accesible como python; si tu sistema lo llama python3, ejecuta antes alias python=python3. En Windows, python y python3 pueden ser solo el atajo de la Microsoft Store, y el alias no lo resuelve: si python --version no imprime una versión, instala Python 3 y, si el atajo sigue respondiendo en una terminal nueva, antepón la carpeta de Python al PATH. Python solo ayuda a generar UUID y a leer JSON; el protocolo es HTTP plano.

La URL base es https://api.boletura.com, sin /v1 ni / final, y es la misma en pruebas y en producción: la clave elige el ambiente. Pruebas ahí mismo con tu clave test, que envía a SUNAT beta y no tiene valor fiscal. Usa solo una clave test durante este recorrido.

Ventana de terminal
python --version
export BOLETURA_BASE_URL='https://api.boletura.com'
read -r -s -p 'API key test: ' BOLETURA_API_KEY
printf '\n'
campo() {
python -c 'import json, sys
print(json.load(open(sys.argv[1], encoding="utf-8")).get(sys.argv[2]) or "")' "$1" "$2"
}

campo archivo.json nombre imprime un campo de primer nivel de una respuesta guardada. Si cierras la terminal, repite este bloque: las variables y la función no se conservan.

Los cuerpos se crean como archivos UTF-8 sin BOM y se envían con --data-binary @archivo; así se conservan los acentos y los bytes de los reintentos, también con curl.exe en Git Bash. python -m json.tool muestra los acentos como é: es el mismo texto. Los archivos de solicitud y respuesta contienen datos del cliente: consérvalos con acceso restringido.

Ventana de terminal
curl -sS -D empresas.headers -o empresas.json -w '%{http_code}\n' \
-H "Authorization: Bearer $BOLETURA_API_KEY" \
"$BOLETURA_BASE_URL/v1/companies?limit=200"
python -m json.tool empresas.json

Espera 200. Busca el RUC emisor y copia su id, no el de la cuenta ni el del establecimiento:

Ventana de terminal
export COMPANY_ID='UUID de la empresa elegida'

Cada entrada trae ruc, legal_name, status, environments_ready y series. Con esta clave, series contiene solo las activas de test: comprueba boleta B…, factura F… e is_default. Si llega vacía, créalas en el paso 1 con el ambiente Pruebas. Una empresa en draft puede emitir en test; ready es su habilitación para live. environments_ready.test: true solo indica que la empresa no está suspendida.

Para emitir desde una sucursal basta enviar su series: la API deduce el establecimiento. Este listado trae el establishment_id de cada serie, no su código ni su nombre, así que en tu sistema guarda qué serie corresponde a cada sucursal.

Si next_cursor no es nulo, pide las páginas siguientes conservando los filtros:

Ventana de terminal
CURSOR=$(campo empresas.json next_cursor)
curl -sS -G -o empresas.json -w '%{http_code}\n' \
-H "Authorization: Bearer $BOLETURA_API_KEY" \
--data-urlencode 'limit=200' --data-urlencode "cursor=$CURSOR" \
"$BOLETURA_BASE_URL/v1/companies"

Ejecuta ese bloque solo si hay cursor; reemplaza empresas.json por la página siguiente. Léela con python -m json.tool empresas.json y repite el bloque mientras su next_cursor no sea nulo. En tu sistema guarda qué company_id corresponde a cada RUC. Al emitir para otro RUC, cambia explícitamente esa relación, aunque uses la misma clave. /v1/companies es solo lectura: el alta se hace desde el back office o la hace Boletura.

Genera una identidad por operación de negocio, antes del primer envío. En el ensayo usamos UUID nuevos; en tu sistema external_id debe derivar de la venta o del pedido y ser estable. Guárdalo junto con la clave de idempotencia y el cuerpo exacto en tu trabajo pendiente persistente.

Ventana de terminal
IDEMPOTENCY_KEY=$(python -c 'import uuid; print(uuid.uuid4())')
EXTERNAL_ID="guia-boleta-$(python -c 'import uuid; print(uuid.uuid4())')"
cat > boleta.json <<JSON
{
"type": "boleta",
"company_id": "$COMPANY_ID",
"customer": {
"document_type": "dni",
"document_number": "45678912",
"name": "Juan Pérez"
},
"items": [{
"description": "Estacionamiento 2 horas · Placa ABC-123",
"quantity": 1,
"unit": "ZZ",
"unit_price": 12.00
}],
"external_id": "$EXTERNAL_ID",
"metadata": {"placa": "ABC-123", "pedido": "$EXTERNAL_ID"}
}
JSON
printf '%s\n' "$IDEMPOTENCY_KEY" > boleta-idempotencia.txt
curl -sS -D boleta.headers -o boleta-respuesta.json -w '%{http_code}\n' \
-H "Authorization: Bearer $BOLETURA_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H 'Content-Type: application/json' \
--data-binary @boleta.json "$BOLETURA_BASE_URL/v1/documents"
python -m json.tool boleta-respuesta.json

Espera 201. Si hay un error, léelo antes de seguir. Con éxito, guarda el UUID:

Ventana de terminal
DOCUMENT_ID=$(campo boleta-respuesta.json id)
echo "$DOCUMENT_ID"

Comprueba environment: "test", company_id, full_number, un totals.total de 12 y los tres estados. Normalmente verás status: "issued", sunat_status: "pending" y delivery_status: "none". 201 confirma la emisión; no confirma la aceptación fiscal: el resultado de SUNAT llega después, en sunat_status y por webhook. Guarda también external_id, updated_at, sunat.deadline_at y X-Request-Id.

Se usa el establecimiento principal y la serie por defecto. Para otra sucursal agrega su series al cuerpo antes del primer intento. La API asigna el número; no lo envíes. Sin issue_date usa hoy en Lima y sin currency, PEN. unit_price incluye IGV; unit_value lo excluye: envía solo uno. ZZ identifica servicios. La plataforma calcula los totales; los importes son números JSON, nunca cadenas.

El cliente de arriba es ficticio para test. En una boleta de hasta S/ 700 puede omitirse customer; sobre ese importe se exige identificar al cliente. No traslades datos ficticios ni supuestos sobre detracciones o cortesías a ventas reales.

Cada programa emite una boleta de S/ 12 en test y repite la misma solicitud con la misma clave, como lo haría tras perder la respuesta: la segunda vez llega el mismo 201 con Idempotent-Replayed: true, sin emitir otro comprobante. Solo usan la biblioteca estándar de su lenguaje y leen BOLETURA_API_KEY, BOLETURA_COMPANY_ID y, si no es producción, BOLETURA_BASE_URL.

issue-document.sh
#!/bin/sh
# Emite una boleta de S/ 12 en test y repite la misma solicitud, como lo haría tras perder la respuesta.
# Requiere curl y openssl. Variables: BOLETURA_API_KEY (bol_test_…), BOLETURA_COMPANY_ID y, si no es producción,
# BOLETURA_BASE_URL.
set -eu
: "${BOLETURA_API_KEY:?Falta BOLETURA_API_KEY}"
: "${BOLETURA_COMPANY_ID:?Falta BOLETURA_COMPANY_ID}"
base_url="${BOLETURA_BASE_URL:-https://api.boletura.com}"
# La clave y el cuerpo se guardan antes del primer envío y se reutilizan tal cual en cada reintento. Cualquier valor
# único de hasta 128 caracteres sirve como clave; aquí, 32 caracteres hexadecimales al azar.
idempotency_key="$(openssl rand -hex 16)"
external_id="venta-$(openssl rand -hex 8)"
cat > boleta.json <<JSON
{
"type": "boleta",
"company_id": "$BOLETURA_COMPANY_ID",
"external_id": "$external_id",
"customer": { "document_type": "dni", "document_number": "45678912", "name": "Juan Pérez" },
"items": [
{ "description": "Estacionamiento 2 horas", "quantity": 1, "unit": "ZZ", "unit_price": 12.00 }
]
}
JSON
issue() {
curl -sS -o "$1.json" -D "$1.headers" -w '%{http_code}' \
-H "Authorization: Bearer $BOLETURA_API_KEY" \
-H "Idempotency-Key: $idempotency_key" \
-H 'Content-Type: application/json' \
--data-binary @boleta.json "$base_url/v1/documents"
}
field() { sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" "$1.json"; }
status="$(issue respuesta)"
if [ "$status" != 201 ]; then
echo "Error $status: $(cat respuesta.json)" >&2
exit 1
fi
echo "Emitido: $status $(field respuesta full_number) (sunat_status: $(field respuesta sunat_status))"
status="$(issue reintento)"
replayed="$(sed -n 's/^[Ii]dempotent-[Rr]eplayed: *\([a-z]*\).*/\1/p' reintento.headers)"
echo "Reintento con la misma clave: $status, Idempotent-Replayed: ${replayed:-false}"

Sigue con reintentos e idempotencia, para recuperar una emisión cuya respuesta se perdió.