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-phpRequiere 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:
| Necesitas | Usa |
|---|---|
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ícito | RequestOverrides::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 FACe | options: 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.clearenumera las rutas JSON que se envían comonull(separadas por puntos en los objetos anidados, comoidentity.logo_id); no se deduce nada más.headersañade cabeceras o sustituye las del mismo nombre. Funciona en los clientes creados conFactuareaClient::create(), que lo conecta.ResponseMetaexponestatusCode,etag,retryAfter(segundos, a partir de un número o de una fecha HTTP, onull),bodyyjson(). Léelo de una respuesta tipada conResponseMeta::fromResponse($response->rawResponse), o de la excepción conResponseMeta::fromException($e).NoRetry::options()detiene los reintentos generados ante429y5xxpara una llamada. Un error de transporte (una conexión cortada, un timeout) se sigue repitiendo con el mismoIdempotency-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-Keyautomático en las mutaciones, elFactuarea-Versionfijado y cualquierRequestOverridesactivo. - 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.