Overview
Official TypeScript and PHP SDKs for the Factuarea API — install @factuarea/sdk or factuarea/factuarea-php and get retries, idempotency, cursor pagination, typed errors and webhook verification out of the box.
Factuarea ships official SDKs that wrap the full v1 REST API (234 operations across 17 resources) with a premium runtime so you don't hand‑roll HTTP: automatic retries, automatic idempotency keys, transparent cursor auto‑pagination, a typed error hierarchy, typed webhook verification and binary (PDF) downloads.
TypeScript / Node.js
@factuarea/sdk on npm. Dual ESM + CommonJS, full type declarations.
Source: github.com/factuarea/factuarea-node.
PHP
factuarea/factuarea-php on Packagist. PSR‑4, Guzzle‑based, PHP 8.2+.
Source: github.com/factuarea/factuarea-php.
Pre‑GA (0.x). Both SDKs are at 0.x. The public method surface is
stable and follows the
SDK method‑naming contract, protected by
SemVer — but while in 0.x, minor versions may include breaking changes
until 1.0.0, which tracks the API's GA. Each release pins one
Factuarea-Version and sends it on every request, so
the API's behaviour stays stable until you upgrade the SDK.
Server‑side only. Your API key is a secret. Never ship an SDK with a live key to a browser, mobile app or any public client — use the SDK from your backend.
Installation
npm install @factuarea/sdkRequires Node 20 or newer. The SDK is built on the Web fetch
standard, so it also runs on Deno, Bun and Cloudflare Workers.
composer require factuarea/factuarea-phpRequires PHP 8.2 or newer with the json and mbstring extensions
(both bundled with standard PHP builds).
Authentication & environments
Pass your API key. The key prefix selects the environment — there is no
separate flag: a fact_test_… key always runs against the isolated
sandbox, a fact_live_… key against production.
import { Factuarea } from "@factuarea/sdk";
const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });
factuarea.environment; // "test" or "live", derived from the key prefixOptional configuration:
new Factuarea({
apiKey: "fact_live_…", // required
baseUrl: "https://api.factuarea.com/v1", // override for staging
timeout: 60_000, // per-request ms (default 60s)
maxRetries: 2, // attempts after the first try
factuareaVersion: "2026-06-04", // pinned API version header
defaultHeaders: {}, // extra headers on every request
});<?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() is the recommended entry point: it wires
Bearer authentication and registers the automatic Idempotency-Key
behaviour for you. For advanced configuration (custom Guzzle client,
custom retry policy, staging base URL) the generated builder is still
available:
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();Quickstart
Create a client and an invoice, then download its PDF. Every operation is
reachable as <resource>.<method> (TypeScript) or
->{resource}->publicApiV1{Resource}{Action} (PHP) following the naming
contract — the per‑endpoint snippets in the
API reference show the exact call for each operation.
import { Factuarea } from "@factuarea/sdk";
const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });
// Responses are the API's `{ data: … }` envelope — read the resource off `.data`.
// 1. Create a client.
const { data: client } = await factuarea.clients.create({
name: "Cliente Demo SL",
tax_id: "B98765432",
});
// 2. Create an invoice (the API computes the totals).
const { data: invoice } = await factuarea.invoices.create({
client_id: client.id,
series_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e",
issued_on: "2026-06-05",
due_on: "2026-07-05",
lines: [
{
description: "Consultoría — junio 2026",
quantity: 10,
unit_price: 100,
tax_rate_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f",
},
],
});
// 3. Download the PDF (a BinaryResponse, not JSON).
const pdf = await factuarea.invoices.pdf(invoice.id);
await import("node:fs/promises").then((fs) =>
fs.writeFile("invoice.pdf", pdf.toBuffer()),
);<?php
require 'vendor/autoload.php';
use Factuarea\Sdk\Custom\FactuareaClient;
use Factuarea\Sdk\Models\Components;
use Brick\DateTime\LocalDate;
$factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY'));
// 1. Create a client.
$client = $factuarea->clients->publicApiV1ClientsCreate(
new Components\CreateClientRequest(
name: 'Cliente Demo SL',
taxId: 'B98765432',
),
);
// 2. Create an invoice (the API computes the totals).
$invoice = $factuarea->invoices->publicApiV1InvoicesCreate(
new Components\CreateInvoiceRequest(
clientId: $client->object->data->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',
),
],
),
);
// 3. Download the PDF.
$pdf = $factuarea->invoices->publicApiV1InvoicesPdf($invoice->object->data->id);
file_put_contents('invoice.pdf', $pdf->bytes ?? '');Run everything with a fact_test_ key first — sandbox effects
(VeriFactu → AEAT, FACe, email, webhooks) are switched off. When your flow works
end‑to‑end, swap the prefix to fact_live_. The API surface is identical
in both. See Test mode & sandbox.
Runtime features
Both SDKs share the same hand‑written runtime on top of the generated typed surface:
- Automatic retries — transient failures (
429and5xx, plus network errors in TypeScript) are retried with exponential backoff and jitter, honouring theRetry-Afterheader. Deterministic client errors (e.g.422validation) are never retried. - Automatic idempotency — every mutation gets a generated
Idempotency-Keyso a retried request never double‑creates a resource. Override it per call when you want app‑level deduplication. See Idempotency. - Cursor auto‑pagination — list methods return an iterable that walks
every page for you, managing
next_cursor/has_more. See Paginating with the SDK. - Typed errors — the API's error envelope maps to a
typed exception hierarchy exposing
code,type,request_idandstatus. Your API key is never included in any error message. See Handling errors. - Webhook verification — a constant‑time HMAC‑SHA256 verifier that honours the secret‑rotation grace window. See Verifying webhooks.
- Binary downloads — PDF and file endpoints return a binary response you turn into a Buffer / stream, not JSON.
Paginating with the SDK
List methods return a Page, which is itself an async iterable:
const page = await factuarea.invoices.list({ status: "paid", limit: 50 });
// (a) iterate every item across every page
for await (const invoice of page) {
console.log(invoice.id);
}
// (b) page by page
page.data; // items on this page
page.hasMore; // boolean
page.nextCursor; // opaque cursor or null
const next = await page.getNextPage(); // Page | null
// (c) collect everything into an array
const all = await page.toArray();The PageIterator helper streams every item across all pages without
manual cursor handling:
use Factuarea\Sdk\Custom\Pagination\PageIterator;
use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesListRequest;
$pages = new PageIterator(
fn (?string $cursor) => $factuarea->invoices->publicApiV1InvoicesList(
new PublicApiV1InvoicesListRequest(startingAfter: $cursor),
)->rawResponse,
);
// items() yields each item as a decoded associative array.
foreach ($pages->items() as $invoice) {
echo $invoice['id'], PHP_EOL;
}See Pagination for the underlying cursor semantics.
Handling errors
import {
FactuareaError,
ValidationError,
RateLimitError,
} from "@factuarea/sdk";
try {
await factuarea.invoices.create(body);
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.fields); // { tax_id: ["NIF inválido"], … }
} else if (error instanceof RateLimitError) {
console.error(error.retryAfter); // seconds to wait
} else if (error instanceof FactuareaError) {
console.error(error.code, error.requestId);
}
}use Factuarea\Sdk\Models\Errors\ErrorThrowable;
try {
$factuarea->invoices->publicApiV1InvoicesCreate($body);
} catch (ErrorThrowable $e) {
$error = $e->container->error;
echo $error->type->value; // e.g. "invalid_request_error"
echo $error->code; // e.g. "parameter_invalid"
echo $error->param; // e.g. "client_id"
echo $error->requestId; // quote this to support
}Branch on the stable code, never on the human‑facing Spanish message.
The full catalog is in Errors.
Verifying webhooks
Pass the raw request body (not a re‑serialized object), the
Factuarea-Signature header and the endpoint secret:
import { Factuarea, WebhookSignatureError, SIGNATURE_HEADER } from "@factuarea/sdk";
const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });
// Express, with express.raw({ type: "application/json" }) on the route:
app.post("/webhooks/factuarea", (req, res) => {
try {
const event = factuarea.webhooks.verify(
req.body.toString("utf8"),
req.headers[SIGNATURE_HEADER.toLowerCase()] as string,
process.env.FACTUAREA_WEBHOOK_SECRET!,
);
if (event.type === "invoice.paid") { /* … */ }
res.sendStatus(200);
} catch (e) {
if (e instanceof WebhookSignatureError) return res.sendStatus(400);
throw e;
}
});use Factuarea\Sdk\Custom\Webhooks\WebhookVerifier;
use Factuarea\Sdk\Custom\Webhooks\WebhookSignatureException;
$verifier = new WebhookVerifier();
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? '';
try {
$event = $verifier->verify($rawBody, $signature, getenv('FACTUAREA_WEBHOOK_SECRET'));
// $event is the decoded, authenticated payload
} catch (WebhookSignatureException $e) {
http_response_code(400);
}Verification uses HMAC‑SHA256 with a constant‑time comparison and a configurable timestamp tolerance (default 5 minutes) to reject replays, and accepts both signatures during a secret‑rotation grace window. See Webhooks.
Per‑endpoint snippets
Every page in the API reference shows a ready‑to‑copy TypeScript, PHP and cURL snippet for that exact operation, generated from the spec so they never drift from the live surface.
Generate your own client
If your language isn't covered yet, or you prefer a client you own and check into your repo, the canonical machine contract is the OpenAPI 3.1 spec — point any generator at it.
The spec lives at
https://docs.factuarea.com/api/openapi. It is generated
from the same backend that serves the API, so it never drifts from the
live surface.
npx openapi-typescript https://docs.factuarea.com/api/openapi \
-o src/factuarea.d.tsopenapi-python-client generate \
--url https://docs.factuarea.com/api/openapiopenapi-generator-cli generate \
-i https://docs.factuarea.com/api/openapi \
-g <language> -o ./factuarea-client<language> can be any
supported generator —
Go, Java, C#, Ruby, Rust and more.
A generated client won't include the official SDK's runtime (retries, idempotency, pagination, webhook verification) — you wire those yourself following the core concept guides.
Building with an AI assistant?
If you want an AI agent to operate Factuarea directly rather than generate
client code, connect it to the MCP server — the public API exposed as
tools, with OAuth and API-key auth. For Claude Code, the official factuarea-mcp
plugin wires it up in two commands.