API SUNAT con PHP: emite facturas electrónicas con cURL
Publicado el
API SUNAT es REST con JSON, así que desde PHP no necesitas un SDK: basta la extensión cURL. En esta guía emites una factura en SUNAT beta y verificas el webhook que te avisa cuando SUNAT responde.
Lo que necesitas
PHP con la extensión cURL.
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.
Emite una factura
Envía la venta a POST /documents. Con un RUC en customer.documentNumber puedes omitir el nombre: la API lo busca.
<?php
$payload = [
'documentType' => '01',
'currency' => 'PEN',
'issueDate' => '2026-10-27',
'customer' => ['documentType' => '6', 'documentNumber' => '20100066603'],
'items' => [['description' => 'Consultoría', 'quantity' => 1, 'unitPrice' => 1180, 'unitCode' => 'ZZ']],
];
$ch = curl_init('https://api.apisunat.net/api/v1/issuer/documents');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . getenv('APISUNAT_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['code'] . ': ' . $body['error']['message']);
}
echo $body['fullNumber'] . ' ' . $body['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.
<?php
$secret = getenv('APISUNAT_WEBHOOK_SECRET'); // whsec_...
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_APISUNAT_SIGNATURE'] ?? ''), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $body, $secret);
$fresh = abs(time() - (int) ($parts['t'] ?? 0)) < 300;
if (!$fresh || !hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// Guarda $event['id'] para ignorar duplicados.
http_response_code(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.
