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.
'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.
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).
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.
