PHP · cURL

Integración de VeriFactu con PHP

Si estás desarrollando un ERP, un SaaS de facturación o cualquier software que necesite cumplir con la normativa VeriFactu, esta guía te muestra el camino más corto desde cero hasta emitir tu primera factura verificada con PHP.

VeriFactuAPI es un servicio SaaS que abstrae toda la complejidad del protocolo de la AEAT: la generación del XML firmado, la cadena de huellas, el envío y la gestión de respuestas. Tú llamas a una API REST y nosotros nos encargamos del resto. No necesitas certificados propios, ni lidiar con los XSD de Hacienda, ni mantener la infraestructura de envío.

Esta guía usa PHP nativo con cURL como cliente HTTP. No necesitas SDK oficial ni dependencias externas para empezar: la API es REST pura y puedes integrarla con funciones estándar de PHP.

Requisitos e instalación

No necesitas instalar librerías adicionales para esta integración. Solo necesitas:

  • PHP con la extensión cURL habilitada
  • Una cuenta en verifactuapi.es (el plan gratuito incluye entorno de pruebas)
  • Funciones nativas: curl_init, curl_setopt, curl_exec, json_encode, json_decode y cabecera Authorization: Bearer <token>

No existe un SDK oficial específico para PHP: la integración se hace directamente contra la API REST, lo cual significa cero dependencias propietarias y total control sobre tu código.

Flujo completo: de la autenticación a la factura registrada

El flujo para enviar una factura tiene tres pasos: autenticarte, generar el registro de facturación y comprobar su estado en la AEAT. Esta comprobación puede hacerse mediante webhook (recomendado) o con consulta manual (polling).

Paso 1 — Autenticación y obtención del token

PHP
<?php

function login(string $email, string $password): string
{
    $payload = json_encode([
        'email'    => $email,
        'password' => $password,
    ], JSON_UNESCAPED_UNICODE);

    $ch = curl_init('https://app.verifactuapi.es/api/login');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Accept: application/json',
        ],
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_TIMEOUT        => 30,
    ]);

    $rawResponse = curl_exec($ch);
    $curlError   = curl_error($ch);
    curl_close($ch);

    if ($rawResponse === false) {
        throw new RuntimeException('Error de red en login: ' . $curlError);
    }

    $data = json_decode($rawResponse, true);

    if (! is_array($data)) {
        throw new RuntimeException('Respuesta JSON inválida en login.');
    }

    if (! $data['success']) {
        throw new RuntimeException('Login fallido: '. $data['code'] . ', ' . $data['message']);
    }

    return $data['token'];
}

Ejemplo de uso

PHP
$token = login('tu@email.com', 'tu_password');
El token tiene una validez de 4 horas. Puedes generar uno nuevo en cada ciclo de ejecución o guardarlo en caché (APCu, Redis o fichero) y renovarlo al caducar para reducir llamadas de login.

Paso 2 — Generar registro de facturación

Este es el núcleo de la integración. El siguiente ejemplo cubre el caso más habitual: una factura ordinaria (F1) con IVA al 21%.

PHP
function registrarFactura(string $token, array $factura): array
{
    $payload = json_encode($factura, JSON_UNESCAPED_UNICODE);

    $ch = curl_init('https://app.verifactuapi.es/api/alta-registro-facturacion');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Accept: application/json',
            'Authorization: Bearer ' . $token,
        ],
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_TIMEOUT        => 30,
    ]);

    $rawResponse = curl_exec($ch);
    $curlError   = curl_error($ch);
    curl_close($ch);

    if ($rawResponse === false) {
        throw new RuntimeException('Error de red al registrar factura: ' . $curlError);
    }

    $data = json_decode($rawResponse, true);

    if (! is_array($data)) {
        throw new RuntimeException('Respuesta JSON inválida al registrar factura.');
    }

    if (! $data['success']) {
        throw new RuntimeException('Error al registrar factura: ' . $data['message']);
    }

    return $data['data']['items'][0];
}

Ejemplo de uso

PHP
$datosFactura = [
    'IDEmisorFactura'        => 'A39200019',
    'NumSerieFactura'        => 'FAC/2025/0001',
    'FechaExpedicionFactura' => '2025-04-22',
    'TipoFactura'            => 'F1',
    'DescripcionOperacion'   => 'Servicios de desarrollo web - Abril 2025',
    'EmitidaPorTercODesti'   => null,

    'Destinatarios' => [
        [
            'NombreRazon' => 'Cliente Ejemplo S.A.',
            'NIF'         => '39707287H',
        ],
    ],

    'Desglose' => [
        [
            'Impuesto'                      => 1,
            'ClaveRegimen'                  => 1,
            'CalificacionOperacion'         => 1,
            'TipoImpositivo'                => 21,
            'BaseImponibleOImporteNoSujeto' => 1000,
            'CuotaRepercutida'              => 210,
        ],
    ],

    'CuotaTotal'   => 210,
    'ImporteTotal' => 1210,
];

$factura    = registrarFactura($token, $datosFactura);
$registroId = $factura['id'];
$qrImageB64 = $factura['qr_image'];
$urlQr      = $factura['url_qr'] ?? null;
$estadoAeat = $factura['estado_aeat'];

echo "Factura registrada con ID: {$registroId}\n";
echo "Estado inicial: {$estadoAeat}\n";
echo "URL validación AEAT: " . ($urlQr ?? 'No disponible') . "\n";

Paso 3 — Consultar el estado en la AEAT

El envío a la AEAT es asíncrono. La factura se registra inmediatamente en nuestra plataforma con estado "No Registrado" y se envía en el siguiente ciclo de proceso. Puedes consultar el estado de dos formas:

Opción A — Polling manual

PHP
function consultarEstado(string $token, int $registroId): array
{
    $ch = curl_init("https://app.verifactuapi.es/api/alta-registro-facturacion/{$registroId}");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPGET        => true,
        CURLOPT_HTTPHEADER     => [
            'Accept: application/json',
            'Authorization: Bearer ' . $token,
        ],
        CURLOPT_TIMEOUT        => 30,
    ]);

    $rawResponse = curl_exec($ch);
    $curlError   = curl_error($ch);
    curl_close($ch);

    if ($rawResponse === false) {
        throw new RuntimeException('Error de red al consultar estado: ' . $curlError);
    }

    $data = json_decode($rawResponse, true);

    if (! is_array($data)) {
        throw new RuntimeException('Respuesta JSON inválida al consultar estado.');
    }

    if (! $data['success']) {
        throw new RuntimeException('Error al consultar el estado de la factura: ' . $data['message']);
    }

    return [
        'estado_aeat'            => $data['data']['items']['estado_aeat'],
        'codigo_error_aeat'      => $data['data']['items']['codigo_error_aeat'],
        'descripcion_error_aeat' => $data['data']['items']['descripcion_error_aeat'],
    ];
}

Ejemplo de uso

PHP
$estado = consultarEstado($token, $registroId);
echo "Estado AEAT: " . $estado['estado_aeat'] . "\n";
// Valores posibles: "No Registrado" | "Pendiente" | "Incorrecto" | "Correcto"

Opción B — Webhook (recomendado)

Configura un endpoint en tu aplicación y registra la URL en el panel de VeriFactuAPI. Recibirás una notificación en cuanto la AEAT responda, sin necesidad de hacer polling:

JSON
{
  "estado": "Correcto",
  "num_serie": "FAC/2025/0001",
  "factura_id": 1,
  "codigo_error": "",
  "descripcion_error": "",
  "fecha_notificacion": "YYYY-MM-DD HH:MM:SS"
}

Formatos de respuesta JSON

Las respuestas de la API siguen una estructura JSON consistente. Entender estos formatos te permite validar respuestas de forma uniforme en tus funciones cURL.

Formato 1 — success (operación correcta)

JSON
{
  "success": true,
  "message": "Inicio de sesión exitoso",
  "code": 200,
  "token": "eyJ...",
  "expires_at": "2026-04-24 18:30:00"
}
  • success: true cuando la operación finaliza correctamente.
  • message: texto descriptivo del resultado.
  • code: código HTTP devuelto por la API.

Campos opcionales: token, expires_at, user_name, user_email, api_key, etc.

Formato 2 — fail (error)

JSON
{
  "success": false,
  "message": "El campo IDEmisorFactura es obligatorio.",
  "error": "VALIDATION_ERROR",
  "code": 400
}
  • success: false cuando la operación falla.
  • message: descripción del error para mostrar o registrar en logs.
  • error: código interno o técnico del error.
  • code: código HTTP asociado al fallo.

Formato 3 — list (listados)

JSON
{
  "success": true,
  "message": "Invoice listed successfully",
  "code": 200,
  "data": {
    "items": [],
    "count": 0
  },
  "pagination": {
    "total": 0,
    "perPage": 0,
    "currentPage": 0,
    "lastPage": 0
  }
}
  • data.items: array con resultados.
  • data.count: número de elementos devueltos en la página actual.
  • pagination: bloque con metadatos de paginación.

Próximos pasos

  • Consulta la referencia completa de la API para ver todos los parámetros disponibles (facturas rectificativas, TicketBAI, webhooks, etc.)
  • Revisa las listas de valores (/api/listas) para conocer todos los códigos válidos de TipoFactura, Impuesto, ClaveRegimen y CalificacionOperacion

¿Listo para probarlo?

Crea tu cuenta gratuita y emite tu primera factura verificada en menos de 10 minutos. El entorno de pruebas está completamente habilitado desde el primer día: sin tarjeta, sin límites artificiales, con la misma API que usarás en producción.