Integrar VeriFactu en PHP y Laravel

· Equipo Factuneo

Emite facturas VeriFactu desde PHP/Laravel con una llamada HTTP: alta del emisor, emisión con idempotencia y webhook de estado. Con ejemplos usando el cliente Http de Laravel.

Integra VeriFactu en tu aplicación PHP/Laravel sin construir el XML, la huella ni el SOAP con la AEAT: emites con una llamada HTTP.

1. API key

Regístrate y copia tu API key (x-api-key). Guárdala en .env (FACTUNEO_API_KEY) y expónla en config/services.php, nunca en el código.

2. Da de alta el emisor

use Illuminate\Support\Facades\Http;

Http::withHeaders(['x-api-key' => config('services.factuneo.key')])
    ->post('https://factuneo.es/api/v1/emisores', [
        'nif' => 'B12345678',
        'nombreRazon' => 'Mi Empresa SL',
        'certMode' => 'PROPIO',
    ]);

3. Emite la factura

$res = Http::withHeaders([
        'x-api-key' => config('services.factuneo.key'),
        'Idempotency-Key' => 'FAC-2026-0001',
    ])->post('https://factuneo.es/api/v1/invoices', [
        'emisorNif' => 'B12345678',
        'numeroSerie' => 'FAC-2026-0001',
        'fechaExpedicion' => '2026-06-08',
        'tipoFactura' => 'F1',
        'nombreRazonEmisor' => 'Mi Empresa SL',
        'descripcionOperacion' => 'Servicios',
        'baseImponible' => 100.00,
        'cuotaIva' => 21.00,
        'importeTotal' => 121.00,
        'destinatarios' => [['nombreRazon' => 'Cliente Ejemplo SL', 'nif' => 'B87654321']],
        'desglose' => [[
            'claveRegimen' => '01',
            'calificacionOperacion' => 'S1',
            'tipoImpositivo' => 21,
            'baseImponible' => 100.00,
            'cuotaRepercutida' => 21.00,
        ]],
    ]);

$factura = $res->json(); // huella, qr y estado en la respuesta

La cabecera Idempotency-Key evita duplicados si Laravel reintenta la petición.

4. Webhook de estado

Route::post('/webhooks/factuneo', function (Illuminate\Http\Request $r) {
    // Verifica la firma HMAC-SHA256 con el secreto de tu cuenta antes de fiarte.
    // El mismo id llega de nuevo si reenvias el evento: descarta duplicados.
    if (yaProcesado($r->input('id'))) {
        return response()->noContent();
    }

    match ($r->input('event')) {
        'invoice.accepted'  => marcarAceptada($r->input('data.invoiceId')),
        'invoice.rejected'  => marcarRechazada($r->input('data.invoiceId')),
        'invoice.cancelled' => marcarAnulada($r->input('data.invoiceId')),
        default             => null,
    };

    guardarProcesado($r->input('id'));
    return response()->noContent();
});

Referencia completa (autenticación, certificados, todos los campos) en la documentación. ¿Comparando opciones? Mira la comparativa.

← Volver al blog · Factuneo