API y webhooks
Para webs a medida y sistemas propios. Una llamada por venta; el resto (IGV, gratuitas, tipo de cambio, reintentos, envío al cliente, respaldo) lo hace BillerPro.
Necesitas el módulo «API y webhooks» (+ S/49/mes). Se activa en un clic desde Mi cuenta → Módulos, sobre cualquier plan. No hace falta para el plugin de WooCommerce, la app de Shopify, VTEX, el panel, la carga CSV, el POS, WhatsApp ni los pedidos de las apps de delivery: esos canales vienen con tu plan y usan la misma clave. Sin el módulo,
POST /v1/emisionescon unorigenpropio yPUT /v1/webhookresponden403; las consultas y descargas siguen abiertas.
Lo esencial
- Base URL:
https://api.billerpro.store - Autenticación: cabecera
X-API-Key: bp_…(tu clave de acceso, la ves en el panel → Conectar canal → API). - Formato: JSON, UTF-8. Montos como número o string decimal (
"59.90"), con IGV incluido. - Idempotencia:
origen+origen_ididentifican la venta. Repetir la llamada devuelve el mismo comprobante (ya_existia: true), nunca uno nuevo. - Límites por minuto: 60 emisiones, 120 consultas, 10 descargas de documentos, 5 reenvíos de correo. Al superarlos recibes
429conRetry-After.
Emitir un comprobante
POST /v1/emisiones
curl -X POST https://api.billerpro.store/v1/emisiones \
-H "X-API-Key: bp_tu_clave" -H "Content-Type: application/json" \
-d '{
"origen": "miweb",
"origen_id": "PED-10021",
"moneda": "PEN",
"receptor": { "numero_documento": "20601234567", "razon_social": "Cliente SAC",
"correo": "[email protected]", "direccion": "Av. Lima 123" },
"lineas": [
{ "codigo": "ZAP-42", "descripcion": "Zapatillas", "cantidad": 1, "precio_unitario": "189.90" },
{ "codigo": "ENVIO", "descripcion": "Envío", "cantidad": 1, "precio_unitario": "15.00", "unidad": "ZZ" },
{ "codigo": "BOLSA", "descripcion": "Bolsa", "cantidad": 1, "precio_unitario": "0.10", "icbper_unitario": "0.50" },
{ "codigo": "REGALO", "descripcion": "Muestra crema", "cantidad": 1, "precio_unitario": "25.00", "afectacion_igv": "15" }
]
}'
Campos del pedido
| Campo | Tipo | Notas |
|---|---|---|
origen |
string ≤30 | Nombre de tu sistema. Por defecto woocommerce. |
origen_id |
string ≤64 | Obligatorio. Tu ID de venta. Clave de idempotencia. |
fecha_emision |
YYYY-MM-DD |
Por defecto hoy. |
moneda |
PEN | USD |
En USD, el tipo de cambio se toma de SUNAT si no lo envías. |
tipo_cambio |
decimal | Opcional. Solo si quieres fijarlo tú. |
tipo_comprobante |
01 | 03 |
Factura o boleta. Si falta, se deduce del documento. |
receptor.numero_documento |
string ≤15 | RUC (11) → factura; DNI (8) → boleta; vacío → "Cliente varios". |
receptor.tipo_documento |
catálogo 06 | 1 DNI, 4 CE, 6 RUC, 7 pasaporte. Se deduce si falta. |
receptor.razon_social |
string ≤250 | Por defecto "Cliente varios". |
receptor.correo |
Si viene, el cliente recibe PDF y XML al aceptarse. | |
receptor.direccion |
string ≤250 | Recomendado en facturas. |
lineas[] |
1 a 200 | Ver abajo. |
observacion |
string ≤250 | Texto libre en el comprobante. |
Campos de cada línea
| Campo | Tipo | Notas |
|---|---|---|
codigo |
string ≤30 | Tu SKU. |
descripcion |
string ≤250 | |
cantidad |
decimal > 0 | |
precio_unitario |
decimal ≥ 0 | Con IGV. En gratuitas: precio de referencia (> 0). |
unidad |
catálogo 03 | NIU (unidad, defecto), ZZ (servicio), KGM, LTR… |
afectacion_igv |
catálogo 07 | 10 gravado (defecto), 20 exonerado, 30 inafecto, 40 exportación, 15 bonificación, 11–16 otras gratuitas, 21/31 gratuitas exoneradas/inafectas. |
icbper_unitario |
decimal | Impuesto a la bolsa por unidad (2026: S/0.50). |
Respuesta
201 Created (o 200 si ya existía):
{
"id": "emi_01J…", "numero": "F001-00000042", "tipo_comprobante": "01",
"serie": "F001", "correlativo": "42", "estado": "aceptado", "aceptado": true,
"reintentando": false, "requiere_correccion": false,
"total": "229.90", "moneda": "PEN", "intentos": 1,
"fecha_emision": "2026-09-04T15:02:11Z", "vence_en": null, "horas_restantes": null,
"ya_existia": false, "origen": "miweb", "mensaje": null
}
Estados: aceptado, aceptado_con_observacion, reintentando (SUNAT/PSE no respondió; reintentamos solos), rechazado (corrige el dato y vuelve a emitir con otro origen_id o anula), anulado, error.
Errores
| Código | Significado |
|---|---|
401 |
Clave ausente, inválida o revocada. |
403 |
Cuenta suspendida (pago pendiente o incidente). Escríbenos. |
422 |
Datos inválidos: el cuerpo detail dice qué campo. Ejemplos: gratuita con precio 0, RUC con 10 dígitos, moneda no soportada. |
429 |
Límite por minuto. Respeta Retry-After. |
503 |
Tipo de cambio de SUNAT no disponible (solo USD sin tipo_cambio). Reintenta en unos minutos o envía tipo_cambio. |
Consultar
GET /v1/emisiones?estado=&limite=50— últimos comprobantes (máx. 200).GET /v1/emisiones/pendientes— los que están reintentando o rechazados, conhoras_restanteshasta el plazo legal.GET /v1/emisiones/por-pedido/{origen}/{origen_id}— busca por tu ID de venta.GET /v1/emisiones/{id}— uno en concreto.
Descargar XML, CDR y PDF
GET /v1/emisiones/{id}/documentos
{ "numero": "F001-00000042", "xml": "<?xml …", "cdr": "<?xml …", "pdf_base64": "JVBERi0…", "hash_documento": "…" }
Se sirven desde nuestro respaldo, así que están disponibles aunque el PSE ya no los tenga.
Anular
POST /v1/emisiones/{id}/anular con { "motivo": "Pedido cancelado" }. Las facturas se anulan con comunicación de baja; las boletas, en el resumen diario. En ambos casos la respuesta trae el nuevo estado.
Reenviar el comprobante por correo
POST /v1/emisiones/{id}/reenviar-correo con { "correo": "[email protected]" }. Solo se acepta el correo del receptor original o uno que tú confirmes; 5 por minuto.
Webhooks
Recibe un aviso cuando un comprobante cambia de estado, sin consultar.
Registrar
PUT /v1/webhook con { "url": "https://tusitio.pe/billerpro" } (solo https). Respuesta:
{ "url": "https://tusitio.pe/billerpro", "secreto": "whsec_…", "aviso": "Guarda este secreto…" }
GET /v1/webhook muestra la URL registrada; DELETE /v1/webhook la quita. Cada PUT genera un secreto nuevo.
Qué recibes
POST a tu URL con JSON y cabeceras X-BillerPro-Evento y X-BillerPro-Firma:
{
"evento": "aceptado",
"comprobante": { "id": "emi_01J…", "numero": "B001-00000107", "tipo": "03", "serie": "B001",
"correlativo": "107", "estado": "aceptado", "total": "59.90", "moneda": "PEN", "mensaje": null },
"pedido": { "origen": "miweb", "origen_id": "PED-10021" }
}
Eventos: emitido, aceptado, rechazado, anulado, error.
Verificar la firma
X-BillerPro-Firma es HMAC-SHA256(cuerpo_crudo, secreto) en hexadecimal.
import hmac, hashlib
def valido(cuerpo: bytes, firma: str, secreto: str) -> bool:
return hmac.compare_digest(hmac.new(secreto.encode(), cuerpo, hashlib.sha256).hexdigest(), firma)
$ok = hash_equals(hash_hmac('sha256', file_get_contents('php://input'), $secreto), $_SERVER['HTTP_X_BILLERPRO_FIRMA']);
Responde 2xx en menos de 10 segundos. Si tu servidor no responde, el aviso se registra como fallido pero la facturación nunca se detiene: el webhook es una comodidad, no un requisito.
Buenas prácticas
- Guarda
idynumerode la respuesta junto a tu venta. - Emite al confirmar el pago, no al crear el pedido. SUNAT da 3 días para facturas.
- Para probar, usa
origen_idcon prefijotest-y anula al terminar. - Nunca pongas la clave en el frontend. Si se filtra, rótala desde el panel (Mi cuenta → Rotar clave): la anterior deja de valer al instante.