This content originally appeared on DEV Community and was authored by Luis Dev
¿Sigues emitiendo facturas manualmente? Si eres desarrollador PHP en Perú, probablemente has sufrido con los procesos manuales de facturación de SUNAT. ¡Hay una mejor manera! Te muestro cómo automatizar todo el proceso con menos de 100 líneas de código PHP.
📋 Lo que vas a aprender
- ✅ Configurar facturación electrónica desde código PHP
- ✅ Integrar SUNAT con PHP usando APIs modernas
- ✅ Manejar errores y validaciones como un pro
- ✅ Optimizar el flujo para aplicaciones en producción
- ✅ Casos de uso reales y mejores prácticas
Tiempo estimado: 10-15 minutos | Nivel: Intermedio
🎯 El problema que todos enfrentamos
Como desarrolladores PHP, constantemente necesitamos integrar sistemas de facturación en nuestras aplicaciones. El proceso tradicional implica:
- 😤 Llenar formularios web manualmente
- ⏰ Perder tiempo en tareas repetitivas
- 🐛 Errores humanos en la captura de datos
- 🔄 Falta de automatización en workflows
La solución: Una API que maneje todo esto por nosotros.
🛠️ Tecnologías que usaremos
<?php
// Stack técnico
$stack = [
'runtime' => 'PHP 8.0+',
'API' => 'Billme',
'method' => 'REST API',
'format' => 'JSON',
'auth' => 'Token-based',
'http_client' => 'cURL / Guzzle'
];
?>
🚀 Setup inicial (2 minutos)
Prerrequisitos
- PHP 8.0 o superior
- Composer (para gestión de dependencias)
- Cuenta gratuita en Billme
- RUC de prueba
1. Crear cuenta de testing
# Paso 1: Registrarse en Billme
# Paso 2: Crear empresa de prueba
# Paso 3: Obtener token de sandbox
💡 Pro tip: Billme tiene ambiente de pruebas gratuito, perfecto para desarrollo y testing.
Sigue su documentación oficial para el setup inicial.
💻 Implementación paso a paso
Estructura del proyecto
facturacion-sunat-php/
├── src/
│ ├── Config/
│ │ └── BillmeConfig.php
│ ├── Services/
│ │ └── InvoiceService.php
│ ├── Utils/
│ │ └── Logger.php
│ └── Models/
│ └── Invoice.php
├── vendor/
├── composer.json
└── script.php
1. Configuración inicial con Composer
{
"name": "tu-usuario/facturacion-sunat",
"description": "Sistema de facturación electrónica para SUNAT con PHP",
"require": {
"php": "^8.0",
"guzzlehttp/guzzle": "^7.0",
"vlucas/phpdotenv": "^5.0"
},
"autoload": {
"psr-4": {
"FacturacionSunat\\": "src/"
}
}
}
2. Configuración base
<?php
// src/Config/BillmeConfig.php
namespace FacturacionSunat\Config;
class BillmeConfig
{
private const BASE_URL = 'https://www.api.billmeperu.com/api/v1';
private const ENDPOINTS = [
'invoice' => '/Emission/EnviarBoletaFactura'
];
private string $token;
private string $environment;
public function __construct()
{
$this->token = $_ENV['BILLME_TOKEN'] ?? 'your-token-here';
$this->environment = $_ENV['BILLME_ENV'] ?? 'sandbox';
}
public function getBaseUrl(): string
{
return self::BASE_URL;
}
public function getInvoiceEndpoint(): string
{
return self::BASE_URL . self::ENDPOINTS['invoice'];
}
public function getToken(): string
{
return $this->token;
}
public function isProduction(): bool
{
return $this->environment === 'production';
}
public function getHeaders(): array
{
return [
'Content-Type' => 'application/json',
'token' => $this->token,
'Accept' => 'application/json'
];
}
}
3. Modelo de datos de factura
<?php
// src/Models/Invoice.php
namespace FacturacionSunat\Models;
class Invoice
{
private array $data;
public function __construct()
{
$this->data = [];
}
public function generateSampleInvoice(): array
{
$this->data = [
'tipoOperacion' => '010',
'serie' => 'F001',
'correlativo' => $this->generateCorrelativo(),
'fechaEmision' => date('Y-m-d'),
'horaEmision' => '00:00:00',
'fechaVencimiento' => date('Y-m-d'),
'codigoTipoOperacion' => '0101',
'codigoTipoDocumento' => '01',
'moneda' => 'PEN',
'montoCredito' => 0,
'formaPago' => 'Contado',
'igv' => 18.00,
'icbper' => 0,
'cuotas' => [],
// Datos del emisor (tu empresa)
'emisor' => [
'codigoTipoDocumento' => '6',
'numDocumento' => '20123456721',
'razonSocial' => 'TU EMPRESA SAC',
'nombreComercial' => 'TU EMPRESA',
'ubigeo' => '150101', // Lima
'ciudad' => 'LIMA',
'distrito' => 'LIMA',
'provincia' => 'LIMA',
'direccion' => 'AV. EJEMPLO 123, LIMA'
],
// Datos del cliente
'cliente' => [
'codigoTipoDocumento' => '1', // DNI
'numDocumento' => '12345678',
'razonSocial' => 'CLIENTE EJEMPLO',
'nombreComercial' => 'CLIENTE EJEMPLO',
'ubigeo' => '',
'ciudad' => '',
'distrito' => '',
'provincia' => '',
'direccion' => ''
],
// Cálculos automáticos
'totales' => [
'totalOpGravadas' => 100.00,
'totalOpInafectas' => 0,
'totalOpExoneradas' => 0,
'totalImpuestos' => 18.00,
'totalSinImpuestos' => 100.00,
'totalConImpuestos' => 118.00,
'totalPagar' => 118.00,
'totalDescuentoGlobal' => 0,
'totalDescuentoProductos' => 0
],
// Productos/servicios
'productos' => [[
'unidades' => 1,
'codigoUnidad' => 'NIU', // Unidad
'nombre' => 'Desarrollo de Software',
'moneda' => 'PEN',
'precioUnitario' => 100.00,
'precioLista' => '118.00',
'montoSinImpuesto' => 100.00,
'montoImpuestos' => 18.00,
'montoTotal' => 100.00,
'montoIcbper' => 0,
'factorIcbper' => 0,
'montoDescuento' => 0,
'codigoTipoPrecio' => '01',
'impuestos' => [[
'monto' => 18.00,
'idCategoria' => 'S',
'porcentaje' => 18,
'codigoAfectacionIgv' => '10',
'codigoInterTributo' => 'VAT',
'nombreTributo' => 'IGV',
'codigoTributo' => '1000'
]],
'id' => 'DEV001',
'codigoClasificacion' => '531117'
]]
];
return $this->data;
}
/**
* Genera correlativo único basado en timestamp
*/
private function generateCorrelativo(): string
{
$timestamp = substr((string) time(), -5);
return str_pad($timestamp, 8, '0', STR_PAD_LEFT);
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data): void
{
$this->data = $data;
}
}
4. Servicio de facturación (¡La magia sucede aquí!)
<?php
// src/Services/InvoiceService.php
namespace FacturacionSunat\Services;
use FacturacionSunat\Config\BillmeConfig;
use FacturacionSunat\Models\Invoice;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Exception;
class InvoiceService
{
private BillmeConfig $config;
private Client $httpClient;
public function __construct()
{
$this->config = new BillmeConfig();
$this->httpClient = new Client([
'timeout' => 30,
'verify' => true
]);
}
/**
* Emite una factura electrónica a SUNAT
*
* @param array $invoiceData Datos de la factura
* @return array Respuesta de SUNAT
* @throws Exception
*/
public function emitInvoice(array $invoiceData): array
{
try {
echo "🚀 Emitiendo factura a SUNAT...\n";
$response = $this->httpClient->post($this->config->getInvoiceEndpoint(), [
'headers' => $this->config->getHeaders(),
'json' => $invoiceData,
'timeout' => 30
]);
$statusCode = $response->getStatusCode();
$responseBody = $response->getBody()->getContents();
$result = json_decode($responseBody, true);
if ($statusCode !== 200) {
throw new Exception("Error HTTP {$statusCode}: " . ($result['message'] ?? 'Error desconocido'));
}
echo "✅ Factura emitida exitosamente\n";
echo "📄 Serie: {$invoiceData['serie']}-{$invoiceData['correlativo']}\n";
echo "💰 Monto: S/ {$invoiceData['totales']['totalPagar']}\n";
return $result;
} catch (GuzzleException $e) {
$errorMessage = "Error de conexión: " . $e->getMessage();
echo "❌ $errorMessage\n";
throw new Exception($errorMessage);
} catch (Exception $e) {
echo "❌ Error al emitir factura: {$e->getMessage()}\n";
throw $e;
}
}
/**
* Emite factura con reintentos automáticos
*
* @param array $invoiceData
* @param int $maxRetries
* @return array
* @throws Exception
*/
public function emitInvoiceWithRetry(array $invoiceData, int $maxRetries = 3): array
{
$lastException = null;
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try {
return $this->emitInvoice($invoiceData);
} catch (Exception $e) {
$lastException = $e;
if ($attempt === $maxRetries) {
throw $e;
}
echo "⚠️ Intento {$attempt} falló, reintentando...\n";
sleep($attempt); // Backoff exponencial simple
}
}
throw $lastException;
}
/**
* Valida los datos de la factura antes de enviar
*
* @param array $invoiceData
* @return bool
* @throws Exception
*/
public function validateInvoiceData(array $invoiceData): bool
{
$requiredFields = [
'serie', 'correlativo', 'fechaEmision', 'emisor',
'cliente', 'totales', 'productos'
];
foreach ($requiredFields as $field) {
if (!isset($invoiceData[$field])) {
throw new Exception("Campo requerido faltante: {$field}");
}
}
// Validar que el total sea mayor a 0
if (($invoiceData['totales']['totalPagar'] ?? 0) <= 0) {
throw new Exception("El total a pagar debe ser mayor a 0");
}
// Validar que tenga al menos un producto
if (empty($invoiceData['productos'])) {
throw new Exception("La factura debe tener al menos un producto");
}
return true;
}
/**
* Procesa múltiples facturas de forma segura
*
* @param array $invoicesData
* @return array Resultados de todas las facturas
*/
public function processBatchInvoices(array $invoicesData): array
{
$results = [];
$rateLimitDelay = 1; // 1 segundo entre requests
foreach ($invoicesData as $index => $invoiceData) {
try {
echo "\n📋 Procesando factura " . ($index + 1) . " de " . count($invoicesData) . "\n";
$this->validateInvoiceData($invoiceData);
$result = $this->emitInvoiceWithRetry($invoiceData);
$results[] = [
'success' => true,
'data' => $result,
'invoice' => $invoiceData['serie'] . '-' . $invoiceData['correlativo']
];
// Rate limiting para evitar saturar la API
if ($index < count($invoicesData) - 1) {
sleep($rateLimitDelay);
}
} catch (Exception $e) {
$results[] = [
'success' => false,
'error' => $e->getMessage(),
'invoice' => ($invoiceData['serie'] ?? 'N/A') . '-' . ($invoiceData['correlativo'] ?? 'N/A')
];
}
}
return $results;
}
}
5. Aplicación principal con manejo elegante de errores
<?php
// script.php
require_once 'vendor/autoload.php';
use FacturacionSunat\Services\InvoiceService;
use FacturacionSunat\Models\Invoice;
use Dotenv\Dotenv;
class InvoiceApp
{
private InvoiceService $invoiceService;
private Invoice $invoice;
public function __construct()
{
// Cargar variables de entorno
if (file_exists('.env')) {
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();
}
$this->invoiceService = new InvoiceService();
$this->invoice = new Invoice();
}
public function run(): void
{
echo "🏁 Iniciando proceso de facturación electrónica...\n\n";
try {
// Generar factura de ejemplo
$invoiceData = $this->invoice->generateSampleInvoice();
echo "📋 Datos de la factura:\n";
echo " Serie: {$invoiceData['serie']}-{$invoiceData['correlativo']}\n";
echo " Cliente: {$invoiceData['cliente']['razonSocial']}\n";
echo " Monto: S/ {$invoiceData['totales']['totalPagar']}\n\n";
// Validar y emitir factura
$this->invoiceService->validateInvoiceData($invoiceData);
$result = $this->invoiceService->emitInvoiceWithRetry($invoiceData);
// Mostrar resultado
echo "\n🎉 ¡FACTURA EMITIDA EXITOSAMENTE!\n";
} catch (Exception $e) {
$this->handleError($e);
}
}
/**
* Maneja errores de manera elegante
*/
private function handleError(Exception $error): void
{
echo "\n❌ ERROR EN LA EMISIÓN\n";
echo str_repeat('═', 50) . "\n";
echo "💥 Mensaje: {$error->getMessage()}\n";
// Errores comunes y soluciones
$message = $error->getMessage();
if (strpos($message, 'token') !== false) {
echo "💡 Solución: Verifica tu token de Billme\n";
} elseif (strpos($message, '400') !== false) {
echo "💡 Solución: Revisa los datos de la factura\n";
} elseif (strpos($message, 'conexión') !== false || strpos($message, 'timeout') !== false) {
echo "💡 Solución: Revisa tu conexión a internet\n";
} elseif (strpos($message, 'Campo requerido') !== false) {
echo "💡 Solución: Completa todos los campos obligatorios\n";
}
echo str_repeat('═', 50) . "\n";
}
/**
* Demo de facturación masiva
*/
public function runBatchDemo(): void
{
echo "🔥 DEMO: Facturación masiva\n\n";
// Crear múltiples facturas de ejemplo
$invoicesData = [];
for ($i = 1; $i <= 3; $i++) {
$invoice = new Invoice();
$data = $invoice->generateSampleInvoice();
$data['cliente']['razonSocial'] = "CLIENTE EJEMPLO {$i}";
$data['totales']['totalPagar'] = 100 * $i;
$data['totales']['totalConImpuestos'] = 100 * $i;
$invoicesData[] = $data;
}
try {
$results = $this->invoiceService->processBatchInvoices($invoicesData);
echo "\n📊 RESUMEN DE PROCESAMIENTO:\n";
echo str_repeat('═', 50) . "\n";
$successful = array_filter($results, fn($r) => $r['success']);
$failed = array_filter($results, fn($r) => !$r['success']);
echo "✅ Exitosas: " . count($successful) . "\n";
echo "❌ Fallidas: " . count($failed) . "\n";
if (!empty($failed)) {
echo "\n❌ Facturas fallidas:\n";
foreach ($failed as $failure) {
echo " - {$failure['invoice']}: {$failure['error']}\n";
}
}
echo str_repeat('═', 50) . "\n";
} catch (Exception $e) {
$this->handleError($e);
}
}
}
// 🚀 Ejecutar la aplicación
try {
$app = new InvoiceApp();
// Factura individual
$app->run();
// Descomentar para probar facturación masiva
// echo "\n" . str_repeat('-', 60) . "\n";
// $app->runBatchDemo();
} catch (Exception $e) {
echo "💥 Error fatal: {$e->getMessage()}\n";
exit(1);
}
6. Archivo de configuración de entorno
# .env
BILLME_TOKEN=your-token-here
BILLME_ENV=sandbox
MAX_RETRIES=3
RATE_LIMIT_MS=1000
🎯 Ejecutar el código
# Instalar dependencias
composer install
# Configurar variables de entorno
cp .env.example .env
# Editar .env con tu token de Billme
# Ejecutar el script
php script.php
🔥 Casos de uso avanzados
1. Integración con Laravel
<?php
// app/Services/SunatInvoiceService.php
namespace App\Services;
use FacturacionSunat\Services\InvoiceService;
use App\Models\Invoice as InvoiceModel;
use Illuminate\Support\Facades\Log;
class SunatInvoiceService
{
private InvoiceService $invoiceService;
public function __construct()
{
$this->invoiceService = new InvoiceService();
}
public function processInvoice(InvoiceModel $invoice): bool
{
try {
$invoiceData = $this->mapInvoiceModelToArray($invoice);
$result = $this->invoiceService->emitInvoice($invoiceData);
// Actualizar modelo
$invoice->update([
'status' => 'emitted',
'sunat_response' => $result,
'pdf_url' => $result['linkPdf'] ?? null
]);
Log::info("Factura emitida: {$invoice->serie}-{$invoice->correlativo}");
return true;
} catch (\Exception $e) {
$invoice->update([
'status' => 'failed',
'error_message' => $e->getMessage()
]);
Log::error("Error emitiendo factura: " . $e->getMessage());
return false;
}
}
private function mapInvoiceModelToArray(InvoiceModel $invoice): array
{
// Mapear tu modelo a la estructura requerida por Billme
return [
'serie' => $invoice->serie,
'correlativo' => $invoice->correlativo,
'fechaEmision' => $invoice->fecha_emision->format('Y-m-d'),
// ... resto de campos
];
}
}
2. API REST para facturación
<?php
// api/invoice.php
require_once '../vendor/autoload.php';
use FacturacionSunat\Services\InvoiceService;
header('Content-Type: application/json');
try {
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Datos de factura inválidos');
}
$invoiceService = new InvoiceService();
$invoiceService->validateInvoiceData($input);
$result = $invoiceService->emitInvoice($input);
http_response_code(200);
echo json_encode([
'success' => true,
'data' => $result,
'message' => 'Factura emitida exitosamente'
]);
} else {
http_response_code(405);
echo json_encode([
'success' => false,
'message' => 'Método no permitido'
]);
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
3. Comando de Artisan para Laravel
<?php
// app/Console/Commands/ProcessPendingInvoices.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Invoice;
use App\Services\SunatInvoiceService;
class ProcessPendingInvoices extends Command
{
protected $signature = 'invoices:process-pending';
protected $description = 'Procesa facturas pendientes de emisión';
public function handle(SunatInvoiceService $invoiceService)
{
$pendingInvoices = Invoice::where('status', 'pending')->get();
if ($pendingInvoices->isEmpty()) {
$this->info('No hay facturas pendientes');
return;
}
$this->info("Procesando {$pendingInvoices->count()} facturas...");
$bar = $this->output->createProgressBar($pendingInvoices->count());
foreach ($pendingInvoices as $invoice) {
$success = $invoiceService->processInvoice($invoice);
if ($success) {
$this->line("✅ {$invoice->serie}-{$invoice->correlativo}");
} else {
$this->line("❌ {$invoice->serie}-{$invoice->correlativo}");
}
$bar->advance();
sleep(1); // Rate limiting
}
$bar->finish();
$this->newLine();
$this->info('Procesamiento completado');
}
}
🤝 Conclusión
¡Felicidades! Acabas de automatizar uno de los procesos más tediosos para cualquier developer peruano. Con este setup puedes:
✅ Emitir facturas desde cualquier aplicación
✅ Integrar con tus sistemas existentes
✅ Escalar sin límites (casi)
✅ Olvidarte de procesos manuales
💬 ¿Te funcionó?
Si implementaste este tutorial:
- 👍 Dale like si te sirvió
- 💬 Comparte tu experiencia en los comentarios
- 🔄 Compártelo con otros devs que necesiten esto
- 🐙 Sube tu versión a GitHub y comparte el repo
¿Tienes dudas? Comenta abajo y te ayudo a resolverlas.
📚 Referencias útiles
Tags: #php #sunat #facturacion #api #peru #automation #fintech
This content originally appeared on DEV Community and was authored by Luis Dev

Luis Dev | Sciencx (2025-06-26T23:07:59+00:00) 🚀 Automatiza tu Facturación Electrónica en Perú: De Manual a API en 10 minutos con PHP. Retrieved from https://www.scien.cx/2025/06/26/%f0%9f%9a%80-automatiza-tu-facturacion-electronica-en-peru-de-manual-a-api-en-10-minutos-con-php/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.