Factuarea APIDevelopers

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’s official SDKs provide automatic retries, idempotency, cursor pagination, typed errors, webhook verification and binary downloads for their released method catalog. REST currently exposes 718 operations across 53 resources; see the release coverage below.

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 customer contact and an invoice, then download its PDF using the released SDK methods below. For other operations, check your installed SDK or use the HTTP snippet in the API Reference.

The PHP contact creation below uses HTTP with Guzzle, already required by the PHP SDK. It works without assuming that your installed PHP release has generated contact methods. The invoice SDK keeps clientId; pass the canonical contact id with an active customer role.

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 customer contact.
const { data: contact } = await factuarea.contacts.create({
  name: "Cliente Demo SL",
  kind: "company",
  roles: ["customer"],
  tax_id: "B12345674",
});

// 2. Create an invoice (the API computes the totals).
const { data: invoice } = await factuarea.invoices.create({
  client_id: contact.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 customer contact.
$http = new \GuzzleHttp\Client();
$response = $http->post('https://api.factuarea.com/v1/companies/' . getenv('FACTUAREA_COMPANY_ID') . '/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).
$invoice = $factuarea->invoices->publicApiV1InvoicesCreate(
    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',
            ),
        ],
    ),
);

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

Account model

Every credential belongs to one account, and the account owns its tax IDs (NIF). The account axis hangs off /v1/accounts/{account}/…, next to the company axis (/v1/companies/{company}/…). Both SDKs cover it with these names:

  • NIF portfolio of the account. TypeScript: factuarea.companies.list, factuarea.companies.create and factuarea.companies.stats. PHP: $factuarea->companies->publicApiV1CompaniesList, publicApiV1CompaniesCreate and publicApiV1CompaniesStats.
  • NIF transfer, which replaces the claim voucher. TypeScript: factuarea.account.nifTransfers.create, .list, .accept and .revoke. PHP: $factuarea->account->nifTransfers->publicApiV1AccountNifTransfersCreate, publicApiV1AccountNifTransfersList, publicApiV1AccountNifTransfersAccept and publicApiV1AccountNifTransfersRevoke.
  • Managed accounts. TypeScript: factuarea.managedAccounts.list, .create, .show, plus .accessLevel, .plan and .management. PHP: $factuarea->managedAccounts->publicApiV1ManagedAccountsList, publicApiV1ManagedAccountsCreate and publicApiV1ManagedAccountsShow, plus the accessLevel, plan and management groups.
  • Bulk import of managed accounts, sent as multipart/form-data: accounts_file (required), customers_file (optional) and the options send_email, apply_customers_to_own_company and confirm_plan_changes. TypeScript: factuarea.managedAccounts.imports.preview, .create and .template; the first two take a standard FormData. PHP: $factuarea->managedAccounts->imports->publicApiV1ManagedAccountsImportsPreview, publicApiV1ManagedAccountsImportsCreate and publicApiV1ManagedAccountsImportsTemplate, with a Components\ManagedAccountImportV1Request that carries a Components\AccountsFile.
  • Account webhooks and request logs. TypeScript: factuarea.account.webhooks (create, list, show, update, delete, rotateSecret, test, and deliveries.list, .show and .retry) and factuarea.account.requestLogs (list, show). PHP: $factuarea->account->webhooks (publicApiV1AccountWebhooksCreate and its siblings, with ->deliveries) and $factuarea->account->requestLogs (publicApiV1AccountRequestLogsList, publicApiV1AccountRequestLogsShow). Writing an account webhook needs a fact_live_ key.

The account argument is positional. In TypeScript, account-scoped methods take the account id as their first argument, before any other id and before the body. TypeScript callers get a compile error, but plain JavaScript callers would shift every argument by one place without an error: review each call when you upgrade. The account id is the one the credential reports about itself:

// `factuarea.account.show()` calls GET /v1/me; the account id is `data.account.id`.
const me = (await factuarea.account.show()) as { data: { account: { id: string } } };
const account = me.data.account.id;
// `publicApiV1AccountShow()` calls GET /v1/me; the account id is `data->account->id`.
$account = $factuarea->account->publicApiV1AccountShow()->object->data->account->id;

Complete examples are in the TypeScript and PHP pages.

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.

Account deliveries

A delivery of an account webhook is verified with the same verifier and the same signature as a company one, so your handler does not change. Its envelope adds three fields: account ({ "id": "<account uuid>" }), account_relationship (own or managed, never all) and company_id (the NIF the event is about, or null in the account.* events that are not about a NIF). The delivery also carries the header Factuarea-Account-Relationship, with the same own or managed value.

const event = factuarea.webhooks.verify(
  req.body.toString("utf8"),
  req.headers[SIGNATURE_HEADER.toLowerCase()] as string,
  process.env.FACTUAREA_WEBHOOK_SECRET!,
) as unknown as {
  type: string;
  account: { id: string };
  account_relationship: "own" | "managed";
  company_id: string | null;
};

if (event.company_id === null) {
  // An event of the account itself, such as account.member_added.
}
$event = $verifier->verify($rawBody, $signature, getenv('FACTUAREA_WEBHOOK_SECRET'));

$accountId    = $event['account']['id'];
$relationship = $event['account_relationship']; // "own" or "managed"
$nifId        = $event['company_id'];           // null in an event of the account itself

Per‑endpoint snippets

Each API Reference page provides TypeScript, PHP and cURL HTTP examples generated from the documented OpenAPI contract. They cover the request, required headers and response format without relying on SDK release coverage.

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.

Read the documented OpenAPI contract, exported from the backend and versioned with this portal.

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.

Release coverage

The September 2026 baseline is 0.2.0 for both SDKs. Their main-branch specs contain 413 entries, with 39 REST endpoints still absent. A package upgrade is needed before newly generated SDK methods become available. The API Reference provides complete TypeScript fetch, PHP cURL and shell HTTP examples for the documented contract; these examples do not assume an SDK method exists.

The account model (the NIF portfolio, the NIF transfer, managed accounts with their bulk import, and account webhooks and request logs) is carried by PHP 0.5.0 and Node 0.7.0. Those are the target versions: this page does not mean they are published, so check the package registry before you upgrade. The API Reference covers the same operations with HTTP examples that do not assume an SDK method exists.

On this page

Need a hand?Contact support