Factuarea API
SDKs

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.

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/sdk

Requires 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-php

Requires 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 prefix

Optional 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 (429 and 5xx, plus network errors in TypeScript) are retried with exponential backoff and jitter, honouring the Retry-After header. Deterministic client errors (e.g. 422 validation) are never retried.
  • Automatic idempotency — every mutation gets a generated Idempotency-Key so 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_id and status. 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.ts
openapi-python-client generate \
  --url https://docs.factuarea.com/api/openapi
openapi-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.

On this page