API SUNAT con Laravel: factura electrónica con el cliente HTTP de Laravel

Publicado el

En Laravel la integración cabe en el cliente HTTP del framework: no necesitas paquetes. En esta guía configuras la clave, emites una factura en SUNAT beta y recibes la respuesta de SUNAT en un controlador.

Lo que necesitas

Laravel con su cliente HTTP (facade Http).

Una cuenta de API SUNAT y una clave de prueba asu_test_: emite en SUNAT beta, con numeración propia y sin consumir tu plan.

Un RUC de prueba registrado con su certificado digital. La guía de inicio rápido de la documentación explica cómo hacerlo desde el panel.

La clave guardada en la variable de entorno APISUNAT_KEY de tu servidor, nunca en el código ni en el navegador.

Configura la clave

Agrega APISUNAT_KEY y APISUNAT_WEBHOOK_SECRET a tu .env y léelas desde config/services.php, así nunca aparecen en el código.

PHPconfig/services.php
'apisunat' => [
    'key' => env('APISUNAT_KEY'),
    'webhook_secret' => env('APISUNAT_WEBHOOK_SECRET'),
],

Emite una factura

Http::post envía el arreglo como JSON. Guarda el Idempotency-Key en tu pedido para reusarlo si reintentas.

PHPapp/Services/ApiSunat.php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

$response = Http::withHeaders([
        'X-API-Key' => config('services.apisunat.key'),
        'Idempotency-Key' => (string) Str::uuid(),
    ])
    ->timeout(60)
    ->post('https://api.apisunat.net/api/v1/issuer/documents', [
        'documentType' => '01',
        'currency' => 'PEN',
        'issueDate' => '2026-10-27',
        'customer' => ['documentType' => '6', 'documentNumber' => '20100066603'],
        'items' => [
            ['description' => 'Consultoría', 'quantity' => 1, 'unitPrice' => 1180, 'unitCode' => 'ZZ'],
        ],
    ]);

if ($response->failed()) {
    throw new RuntimeException($response->json('error.code') . ': ' . $response->json('error.message'));
}

$document = $response->json();
// $document['fullNumber'] y $document['status']

Reintenta sin emitir dos veces

POST /documents exige el header Idempotency-Key. Guarda esa clave junto a tu pedido y reúsala si reintentas: la misma clave con el mismo cuerpo devuelve el mismo resultado y no emite un comprobante duplicado.

Si SUNAT rechaza el comprobante, la API no responde con un error HTTP: responde 202 con status "rejected" y el motivo en sunat.errorCode y sunat.errorMessage.

Recibe la respuesta de SUNAT por webhook

La emisión responde de inmediato y SUNAT puede seguir procesando. Registra un webhook para recibir document.accepted, document.rejected o document.failed.

Verifica la firma del header X-Apisunat-Signature (HMAC-SHA256 de "<t>.<cuerpo crudo>" con tu secreto whsec_) y rechaza las entregas con más de 5 minutos de antigüedad.

Registra la ruta en routes/api.php, que no pasa por la protección CSRF (en Laravel 11 o superior, créalo con php artisan install:api).

PHPapp/Http/Controllers/ApiSunatWebhookController.php
use Illuminate\Http\Request;

// routes/api.php: Route::post('/webhooks/apisunat', ApiSunatWebhookController::class);
class ApiSunatWebhookController
{
    public function __invoke(Request $request)
    {
        parse_str(str_replace(',', '&', $request->header('X-Apisunat-Signature', '')), $parts);
        $body = $request->getContent();
        $expected = hash_hmac(
            'sha256',
            ($parts['t'] ?? '') . '.' . $body,
            config('services.apisunat.webhook_secret'),
        );
        $fresh = abs(time() - (int) ($parts['t'] ?? 0)) < 300;

        abort_unless($fresh && hash_equals($expected, $parts['v1'] ?? ''), 400);

        $event = json_decode($body, true);
        // Guarda $event['id'] para ignorar duplicados y procésalo en una cola.
        return response()->noContent(200);
    }
}

Siguientes pasos

Para emitir boletas cambia documentType a "03"; para notas de crédito, a "07". Todos los campos y códigos de error están en la documentación de la API.

Cuando todo funcione en beta, cambia a una clave asu_live_ y habilita producción en tu RUC.

Otras guías