Factuarea APIDevelopers

PHP

Instala factuarea/factuarea-php con Composer, autentícate y crea tu primera factura. PSR-4, basado en Guzzle, PHP 8.2+.

El SDK oficial de PHP es factuarea/factuarea-php en Packagist — PSR-4, basado en Guzzle. Código fuente: github.com/factuarea/factuarea-php. Envuelve la API REST v1 con reintentos automáticos, idempotency keys, auto-paginación por cursor, una jerarquía de errores tipada y verificación de webhooks — todo ello descrito en la introducción al SDK.

Instalación

composer require factuarea/factuarea-php

Requiere PHP 8.2 o superior con las extensiones json y mbstring (ambas incluidas en las builds estándar de PHP).

Autenticación

Pasa tu API key. El prefijo de la clave selecciona el entorno — no hay un flag aparte: una clave fact_test_… siempre se ejecuta contra el sandbox aislado, y una clave fact_live_… contra producción.

<?php

require 'vendor/autoload.php';

use Factuarea\Sdk\Custom\FactuareaClient;

// The key prefix selects the environment:
//   fact_test_… → sandbox    fact_live_… → production
$factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY'));

FactuareaClient::create() es el punto de entrada recomendado: configura la autenticación Bearer y registra por ti el comportamiento automático de Idempotency-Key. Para configuración avanzada (cliente Guzzle personalizado, política de reintentos personalizada, base URL de staging) el builder generado sigue disponible:

use Factuarea\Sdk\Factuarea;
use Factuarea\Sdk\Models\Components\Security;

$factuarea = Factuarea::builder()
    ->setSecurity(new Security(bearerAuth: getenv('FACTUAREA_API_KEY')))
    ->setServerURL('https://api.factuarea.com/v1')
    ->build();

Solo en el servidor. Tu API key es un secreto. Nunca distribuyas el SDK con una clave live en un cliente público — úsala desde tu backend.

Inicio rápido

Crea un contacto con rol de cliente y una factura, y descarga su PDF con los métodos publicados del SDK que aparecen a continuación. Para otras operaciones, comprueba tu SDK instalado o usa el ejemplo HTTP de la referencia de la API.

La creación del contacto usa HTTP con Guzzle, que el SDK PHP ya requiere. Así funciona sin asumir que tu versión PHP instalada tiene métodos de contactos generados. El SDK de facturas conserva clientId: pasa el id del contacto con rol customer activo.

<?php

require 'vendor/autoload.php';

use Factuarea\Sdk\Custom\FactuareaClient;
use Factuarea\Sdk\Models\Components;
use Factuarea\Sdk\Models\Operations;
use Brick\DateTime\LocalDate;

$factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY'));

// Company axis: every company call carries the company id (`data.scope[].id` of GET /v1/me).
$company = getenv('FACTUAREA_COMPANY_ID');

// 1. Create a customer contact.
$http = new \GuzzleHttp\Client();
$response = $http->post('https://api.factuarea.com/v1/companies/' . $company . '/contacts', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('FACTUAREA_API_KEY'),
        'Idempotency-Key' => bin2hex(random_bytes(16)),
    ],
    'json' => [
        'name' => 'Cliente Demo SL',
        'kind' => 'company',
        'roles' => ['customer'],
        'tax_id' => 'B12345674',
    ],
]);
$contact = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR)['data'];

// 2. Create an invoice (the API computes the totals).
$created = $factuarea->invoices->publicApiV1InvoicesCreate(
    company: $company,
    body: new Components\CreateInvoiceRequest(
        clientId: $contact['id'],
        seriesId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e',
        issuedOn: LocalDate::parse('2026-06-05'),
        dueOn: LocalDate::parse('2026-07-05'),
        lines: [
            new Components\CreateInvoiceRequestLine(
                description: 'Consultoría — junio 2026',
                quantity: 10,
                unitPrice: 100,
                taxRateId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f',
            ),
        ],
    ),
);

// A new invoice comes in the 201 body; a 200 replays one that already exists.
$invoice = ($created->twoHundredAndOneApplicationJsonObject ?? $created->twoHundredApplicationJsonObject)->data;

// 3. Download the PDF. A 202 (still being generated) raises APIException:
//    see "Conditional requests, null fields and PDF statuses" below.
$pdf = $factuarea->invoices->publicApiV1InvoicesPdf(
    new Operations\PublicApiV1InvoicesPdfRequest(company: $company, invoice: $invoice->id),
);
file_put_contents('invoice.pdf', $pdf->bytes ?? '');

Ejecuta todo primero con una clave fact_test_ — los efectos del sandbox (VeriFactu → AEAT, FACe, email, webhooks) están desactivados. Cuando tu flujo funcione de extremo a extremo, cambia el prefijo a fact_live_. Consulta Modo de prueba y sandbox.

Modelo de cuentas

El eje de cuenta (/v1/accounts/{account}/…) lo cubren $factuarea->companies (la cartera de NIF), $factuarea->account->nifTransfers, $factuarea->managedAccounts (con ->imports) y, para webhooks y registros de peticiones, $factuarea->account->webhooks y $factuarea->account->requestLogs. Llegan con 0.5.0, la versión objetivo del modelo de cuentas; todavía no está publicada.

Los métodos del eje de cuenta reciben el id de la cuenta en el parámetro account. El ejemplo lista los NIF de la cuenta, traspasa un NIF a otra cuenta, aplica una importación masiva de cuentas gestionadas con Components\AccountsFile (accountsFile es obligatorio, customersFile es opcional) y crea un webhook de cuenta. Los métodos que escriben de forma irreversible exigen un idempotencyKey:

<?php

require 'vendor/autoload.php';

use Factuarea\Sdk\Custom\FactuareaClient;
use Factuarea\Sdk\Models\Components;
use Factuarea\Sdk\Models\Operations;

$factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY'));

// El id de la cuenta es `data->account->id` de `publicApiV1AccountShow()` (GET /v1/me).
$account = $factuarea->account->publicApiV1AccountShow()->object->data->account->id;

// 1. Lista los NIF de la cuenta.
$nifs = $factuarea->companies->publicApiV1CompaniesList(
    new Operations\PublicApiV1CompaniesListRequest(account: $account, limit: 50),
);
foreach ($nifs->companyList->data as $nif) {
    echo $nif->id, ' ', $nif->taxId, PHP_EOL;
}

// 2. Traspasa un NIF a otra cuenta (traspaso de NIF).
$transfer = $factuarea->account->nifTransfers->publicApiV1AccountNifTransfersCreate(
    new Components\CreateNifTransferV1Request(
        companyId: $companyId,
        recipientAccountId: $recipientAccountId,
    ),
    account: $account,
    idempotencyKey: bin2hex(random_bytes(16)),
);
// El secreto viaja solo en esta respuesta: entrégalo a la cuenta receptora.
$secret = $transfer->object->data->secret;

// 3. Aplica una importación masiva de cuentas gestionadas (multipart/form-data).
$import = $factuarea->managedAccounts->imports->publicApiV1ManagedAccountsImportsCreate(
    new Components\ManagedAccountImportV1Request(
        accountsFile: new Components\AccountsFile(
            fileName: 'accounts.csv',
            content: file_get_contents('accounts.csv'),
        ),
        options: new Components\ManagedAccountImportV1RequestOptions(sendEmail: false),
    ),
    idempotencyKey: bin2hex(random_bytes(16)),
);

// 4. Crea un webhook de cuenta (exige una clave fact_live_).
$endpoint = $factuarea->account->webhooks->publicApiV1AccountWebhooksCreate(
    new Components\CreateAccountWebhookV1Request(
        url: 'https://example.com/webhooks/factuarea',
        enabledEvents: ['account.nif_added', 'invoice.paid'],
        accountRelationship: Components\CreateAccountWebhookV1RequestAccountRelationship::All,
    ),
    account: $account,
);
// El secreto `whsec_…` solo se devuelve ahora: guárdalo para verificar las entregas.
$webhookSecret = $endpoint->object->data->secret;

Una entrega de un webhook de cuenta se verifica con WebhookVerifier, igual que una de empresa; consulta Verificación de webhooks.

Peticiones condicionales, campos null y estados de PDF

El SDK incluye helpers escritos a mano en Factuarea\Sdk\Custom para lo que los métodos generados aún no pueden expresar:

NecesitasUsa
Una cabecera para la que el método generado no tiene parámetro (If-Match, If-None-Match), o un campo enviado como null explícitoRequestOverrides::run()
El estado, el ETag, el Retry-After o el cuerpo JSON de una respuesta, incluido un estado que el método generado lanza como APIException (el 202 de un PDF que aún se está generando)ResponseMeta::fromResponse() y ResponseMeta::fromException()
Una llamada que no debe repetirse ante 429 o 5xx, como el envío y la cancelación en FACeoptions: NoRetry::options()
  • RequestOverrides::run($call, clear: [...], headers: [...]) se aplica a cada petición que se lanza dentro del callback, reintentos incluidos, así que pon una sola llamada del SDK dentro. clear enumera las rutas JSON que se envían como null (separadas por puntos en los objetos anidados, como identity.logo_id); no se deduce nada más. headers añade cabeceras o sustituye las del mismo nombre. Funciona en los clientes creados con FactuareaClient::create(), que lo conecta.
  • ResponseMeta expone statusCode, etag, retryAfter (segundos, a partir de un número o de una fecha HTTP, o null), body y json(). Léelo de una respuesta tipada con ResponseMeta::fromResponse($response->rawResponse), o de la excepción con ResponseMeta::fromException($e).
  • NoRetry::options() detiene los reintentos generados ante 429 y 5xx para una llamada. Un error de transporte (una conexión cortada, un timeout) se sigue repitiendo con el mismo Idempotency-Key, al que la API responde reproduciendo el primer resultado.
<?php

require 'vendor/autoload.php';

use Factuarea\Sdk\Custom\FactuareaClient;
use Factuarea\Sdk\Custom\Request\NoRetry;
use Factuarea\Sdk\Custom\Request\RequestOverrides;
use Factuarea\Sdk\Custom\Response\ResponseMeta;
use Factuarea\Sdk\Models\Errors\APIException;
use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesPdfRequest;

$factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY'));
$company = getenv('FACTUAREA_COMPANY_ID'); // `data.scope[].id` of GET /v1/me

// 1. Download the invoice PDF, or get a 304 when the copy you hold is current.
try {
    $pdf = RequestOverrides::run(
        fn () => $factuarea->invoices->publicApiV1InvoicesPdf(
            new PublicApiV1InvoicesPdfRequest(company: $company, invoice: $invoiceId),
        ),
        headers: ['If-None-Match' => $cachedEtag],
    );
    if ($pdf->statusCode === 200) {
        file_put_contents('invoice.pdf', $pdf->bytes ?? '');
        $cachedEtag = ResponseMeta::fromResponse($pdf->rawResponse)->etag;
    }
    // 304: no bytes, keep your copy.
} catch (APIException $e) {
    $meta = ResponseMeta::fromException($e);
    if ($meta->statusCode !== 202) {
        throw $e;
    }
    // 202: the PDF is still being generated. Ask again after $meta->retryAfter seconds.
}

// 2. Submit to FACe once: no automatic retry on 429 or 5xx.
$factuarea->invoices->faceSubmissions->publicApiV1InvoicesFaceSubmissionsSubmit(
    company: $company,
    invoice: $invoiceId,
    idempotencyKey: $submissionKey, // reuse it if you ever repeat this submission
    options: NoRetry::options(),
);

Tras un fallo en FACe el resultado puede ser incierto: consulta los envíos antes de volver a enviar. Consulta Cómo responden las descargas.

Plantillas PDF

Los métodos tipados de la biblioteca de plantillas PDF, las vistas previas, los artefactos PDF, la presentación de cada documento y la plantilla de una serie (/v1/companies/{company}/pdf-templates, /v1/companies/{company}/pdf-artifacts, /v1/companies/{company}/invoices/{invoice}/presentation…) aún no están generados en este SDK; llegarán con la próxima versión del SDK. Mientras tanto, llama a esas operaciones con RawRequest::send(), que pasa por el mismo cliente autenticado:

  • Reutiliza la clave Bearer, el Idempotency-Key automático en las mutaciones, el Factuarea-Version fijado y cualquier RequestOverrides activo.
  • La ruta es relativa a la URL base, que ya termina en /v1, así que empieza por /companies/{company}/…. Cada marcador {name} toma su valor del último argumento, codificado para URL.
  • No aplica reintentos, nunca lanza una excepción ante un estado HTTP de error y devuelve la respuesta PSR-7, que lees con ResponseMeta::fromResponse().
<?php

require 'vendor/autoload.php';

use Factuarea\Sdk\Custom\FactuareaClient;
use Factuarea\Sdk\Custom\Request\RawRequest;
use Factuarea\Sdk\Custom\Response\ResponseMeta;

$factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY'));
$company = getenv('FACTUAREA_COMPANY_ID');

// 1. List the library, one page per call. A list returns `items` and `meta`.
$list = ResponseMeta::fromResponse(RawRequest::send(
    $factuarea,
    'GET',
    '/companies/{company}/pdf-templates',
    ['query' => ['page' => 1, 'per_page' => 20]],
    ['company' => $company],
));
foreach ($list->json()['items'] ?? [] as $template) {
    echo $template['id'], ' ', $template['name'], PHP_EOL;
}

// 2. Read one template and keep its ETag.
$read = ResponseMeta::fromResponse(RawRequest::send(
    $factuarea,
    'GET',
    '/companies/{company}/pdf-templates/{template}',
    [],
    ['company' => $company, 'template' => $templateId],
));

// 3. Rename it with the literal ETag as If-Match. `null` clears the description.
$update = ResponseMeta::fromResponse(RawRequest::send(
    $factuarea,
    'PATCH',
    '/companies/{company}/pdf-templates/{template}',
    [
        'headers' => ['If-Match' => $read->etag],
        'json' => ['name' => 'Factura corporativa', 'description' => null],
    ],
    ['company' => $company, 'template' => $templateId],
));
if ($update->statusCode !== 200) {
    // 412: the ETag is stale. 428: no precondition. Read it again and retry.
    throw new RuntimeException('HTTP '.$update->statusCode.': '.$update->body);
}

// 4. Download an exact PDF artifact: it answers 503 with Retry-After until it is ready.
for ($attempt = 0; $attempt < 12; $attempt++) {
    $response = RawRequest::send(
        $factuarea,
        'GET',
        '/companies/{company}/pdf-artifacts/{artifact}/download',
        ['headers' => ['Accept' => 'application/pdf']],
        ['company' => $company, 'artifact' => $artifactId],
    );
    if ($response->getStatusCode() === 200) {
        file_put_contents('invoice.pdf', (string) $response->getBody());
        break;
    }
    if ($response->getStatusCode() !== 503) {
        throw new RuntimeException('HTTP '.$response->getStatusCode());
    }
    sleep(max(1, ResponseMeta::fromResponse($response)->retryAfter ?? 1));
}

Consulta la biblioteca de plantillas PDF, Concurrencia y Vistas previas y artefactos PDF para el modelo.

Siguientes pasos

El comportamiento en runtime — reintentos, idempotencia, auto-paginación por cursor, la jerarquía de errores tipada y la verificación de webhooks — es común a ambos SDK y está documentado una sola vez en la introducción al SDK:

Cobertura de las versiones

La referencia de septiembre de 2026 es 0.2.0 para ambos SDK. Los specs de sus ramas principales contienen 413 entradas y todavía les faltan 39 endpoints REST. Es necesario actualizar el paquete para disponer de nuevos métodos generados. La referencia de la API ofrece ejemplos HTTP completos con fetch de TypeScript, cURL de PHP y terminal para el contrato documentado; estos ejemplos no presuponen que exista un método del SDK.

En esta página

¿Te echamos una mano?Contactar con soporte