Usage
The factuarea command tree — list, show, create, domain actions, PDF templates, conditional writes, binary downloads, multipart uploads, the generic api escape hatch and the commands --json manifest.
The command tree covers every resource in the API
(factuarea <resource> [<sub-resource>] <action>), generated from the OpenAPI
spec so it never drifts from the live surface.
Reading data
# List (with automatic cursor pagination)
factuarea invoices list --json
factuarea contacts list --paginate --json
# Get one
factuarea invoices show <uuid> --json--json emits the raw API body on stdout. --paginate walks every page for
you, following next_cursor until has_more is false. See
Pagination for the underlying cursor semantics.
Writing data
Pass the JSON body with -d (inline) or --data-file (a path). The API
computes totals — do not pre-round them.
factuarea invoices create -d '{"client_id":"…","series_id":"…","lines":[…]}'Every mutation gets an automatic Idempotency-Key so a retried request never
double-creates a resource. See Idempotency.
The key is generated once per invocation and reused on the CLI's own retries of
429 and 5xx answers, which wait what Retry-After says. To repeat an
operation from a later invocation, for example when your script runs again after
a timeout, pin the key with --idempotency-key <key> and send the same body.
FACe submission and cancellation (invoices face-submissions submit and
face-submissions cancel) make a single call and are never retried: if one
fails, read the submission before sending again.
Domain actions
State changes are discrete actions, not a generic status flag — mirroring the API's own design:
factuarea invoices send <uuid>
factuarea invoices mark-paid <uuid>Some actions are irreversible (deletes, void, conversions, fiscal
emission). The CLI asks you to confirm them before the call — see
Irreversible operations and the
scopes & irreversibility guide.
Company and account axes
A resource hangs from one of two axes, and the CLI fills the one in its path from a flag so you do not have to type it on every command:
| Axis | Path | Flag | Commands |
|---|---|---|---|
| Company | /v1/companies/{company}/… | --company | invoices, contacts, time-entries, … |
| Account | /v1/accounts/{account}/… | --account | account (members, invitations, api-keys, nif-transfers, owner, webhooks, request-logs, usage, provisioning-usage), companies, employee-seats, gestoria workforce-summary |
--company fills the company axis: pass the company as the first positional
argument or with the flag, or leave it out when your credential reaches a single
company (factuarea invoices list --company acme_co --json). It never fills the
account slot, and --account never fills the company one.
--account fills the account axis, with these rules:
- A read (
GET) without an account uses the own account of your credential: the oneGET /v1/mepublishes indata.account.id. It is looked up once per invocation and never written to disk.whoamidoes not print that id;factuarea account show --jsondoes. - A write never deduces the account: name it as the positional argument or
with
--account. Without either, the CLI exits with a usage error (code2) and sends nothing; the message names the own account of your credential and both ways to pass it. Giving it as the positional and with the flag is also a usage error. - An irreversible operation whose last path parameter is the account
(
account nif-transfers create,account nif-transfers acceptandaccount owner transfer) takes it only as the positional argument: the flag is not accepted there. - In
managed-accountsthe account is the managed one: a required resource that you always write as the positional argument and that the flag never fills. - The root provisioning and import routes (
GETandPOST /v1/accounts,POST /v1/accounts/imports,POST /v1/accounts/imports/previewandGET /v1/accounts/imports/template) carry no account at all, even if you pass--account.
# 1) A read resolves the account by itself
# (GET /v1/me, then GET /v1/accounts/<your account>/members)
factuarea account members list --json
# 2) A write must name it. Without the account it fails with code 2 and sends
# nothing; with --account the request goes to POST /v1/accounts/<account>/webhooks
factuarea account webhooks create --url https://example.com/hooks --enabled-events account.member_added
factuarea account webhooks create --account <account> --url https://example.com/hooks --enabled-events account.member_added
# 3) A managed account is the resource: always the positional argument
# (GET /v1/accounts/<managed_account>); --account does not fill it
factuarea managed-accounts show <managed_account> --json
# 4) A root provisioning route carries no account (POST /v1/accounts)
factuarea managed-accounts create --external-ref cliente-0042 --plan emprendedor \
--tax-id B12345674 --legal-name "Talleres García SL" --confirm createfactuarea commands --json publishes the omittable argument in optional_args
({"account": "--account"}, or {"company": "--company"} on the company axis)
for every command where a flag fills it; see
Agents & scripting. The commands of managed-accounts and the
irreversible ones with the account as last parameter do not publish it.
Managed accounts
For whoever provisions accounts for its own clients. The --confirm token is
the literal create on the root operations and the resource id on the rest:
factuarea managed-accounts list --json
factuarea managed-accounts show <managed_account>
factuarea managed-accounts plan preview <managed_account> --plan empresario
factuarea managed-accounts plan update <managed_account> --plan empresario --confirm <managed_account>
factuarea managed-accounts access-level update <managed_account> --level view
factuarea managed-accounts claim-tokens create <managed_account> --send-email --confirm <managed_account>
factuarea managed-accounts management end <managed_account> --confirm <managed_account>managed-accounts claim-tokens create reissues the claim voucher of a managed
account. The own account no longer has a claim voucher: the tax ID transfer
(factuarea account nif-transfers create, accept, list and revoke)
replaced it.
Account webhooks and request logs
The webhook endpoints of the account (account webhooks, with their deliveries
in account webhooks deliveries) and its request logs (account request-logs)
use the account axis above: a read deduces the account, a write asks for it. See
Account webhooks for what an account endpoint
receives.
factuarea account webhooks list --json
factuarea account webhooks create --account <account> --url https://example.com/hooks \
--enabled-events account.member_added --account-relationship own
factuarea account webhooks deliveries list <webhook> --status failed --json
factuarea account webhooks deliveries retry --account <account> <webhook> <delivery>
factuarea account request-logs list --only_errors true --json
factuarea account request-logs show <request_id> --jsonTime tracking (control-horario)
The control-horario add-on adds workforce resources — employees, work schedules,
time entries, absences, presence, holidays, monthly closes and the consolidated workforce
overview of the account (gestoria workforce-summary). Each command is generated from the spec and gated by its fine-grained
scope (employees:*, time_entries:*, absences:*, work_schedules:*,
presence:read, holidays:read, payroll_exports:*).
# Clock an employee in and out — every entry is hash-chained (RD-ley 8/2019)
factuarea time-entries clock-in -d '{"employee_id":"…","source":"web"}'
factuarea time-entries clock-out -d '{"employee_id":"…","source":"web"}'
# Request time off, then approve it
factuarea absence-requests create \
-d '{"employee_id":"…","absence_type_id":"…","start_date":"2026-08-01","end_date":"2026-08-05"}'
factuarea absence-requests approve <uuid>
# Live team presence right now
factuarea presence live --json
# Close a month's immutable register, then export it (ITSS RD-ley 8/2019)
factuarea monthly-time-record-closes create -d '{"year":2026,"month":7}'
factuarea monthly-time-record-closes export <uuid> --format rdley_8_2019 --jsonAutomations
The automations add-on exposes the rule engine: a rule listens to an event, a
condition decides whether to act, and an ordered list of actions runs. The module
gates the whole group, and each command carries its fine-grained scope —
automations:read (8 commands), automations:write (6),
automation_runs:read (3) and automations:delete (1). Four subgroups mirror
the API: catalog, rules (with rules versions), runs (with runs steps)
and usage. See the Automations guide for the model.
Start at the catalog. The evaluable fields of a trigger are a second call, one per trigger — they do not travel inside the catalog:
# What can be automated: triggers, operators and registered actions.
factuarea automations catalog show --json
# The evaluable fields of one trigger.
factuarea automations catalog trigger-fields invoice.paid --jsonCreating a rule goes through the full JSON body. actions is a list of
objects, so no typed flag is generated for it, and the binary refuses to mix
field flags with -d/--data-file — pick one. --skeleton prints the template
of the body without calling the API:
factuarea automations rules create --skeleton{
"actions": [],
"conditions": {
"<key>": "<string>"
},
"description": "<string>",
"name": "<string>",
"scope": "<empresa|cartera>",
"trigger_type": "<string>"
}# Fill it in and send it whole. `scope` is chosen here and cannot be changed later.
factuarea automations rules create --json -d '{
"name": "Warn me on a large payment",
"scope": "empresa",
"trigger_type": "invoice.paid",
"conditions": {"field": "total", "operator": "gte", "value": 1000},
"actions": [
{"type": "notify_in_app", "order": 0, "parameters": {
"recipient_type": "role", "role": "admin", "category": "invoice",
"title": "Large payment", "message": "An invoice over 1000 EUR has been paid"
}}
]
}'
# Dry-run it against a sample event: it returns the trace per step and materialises nothing.
factuarea automations rules dry-run <rule_id> --json \
-d '{"event_type":"invoice.paid","event_payload":{"total":1200,"status":"paid"}}'
# A rule is created in draft and fires nothing until you activate it.
factuarea automations rules activate <rule_id>
factuarea automations rules pause <rule_id>
# Read one, and edit it — every edit seals a new version.
factuarea automations rules show <rule_id> --json
factuarea automations rules update <rule_id> --json -d '{"name":"Warn me on a payment over 2000"}'
# List with filters, walking the cursor for you.
factuarea automations rules list --paginate --json
factuarea automations rules list --status active --trigger_type invoice.paid --json
# Only the portfolio rules: the ones that watch the account's other tax IDs.
factuarea automations rules list --scope cartera --json
# The sealed versions of the definition.
factuarea automations rules versions list <rule_id> --json
factuarea automations rules versions show <rule_id> 2 --json
# What fired, and what each step did.
factuarea automations runs list --paginate --json
factuarea automations runs list --automation_rule_id <rule_id> --status failed --json
# What a portfolio rule did on one of the tax IDs it watches.
factuarea automations runs list --subject_company_id <company_id> --json
factuarea automations runs show <run_id> --json
factuarea automations runs steps list <run_id> --json
# Consumption against the plan budget for the period.
factuarea automations usage show --jsonA cartera (portfolio) rule is created only by the holder of the account, and
only when the account has the gestoria module granted. It watches the account's
other tax IDs (an additional one, one transferred to the account) and the ones
another account grants it with the operate access level, never the holder's own
tax ID. In that scope a rule only notifies: notify_in_app, notify_channel,
emit_webhook and create_calendar_event. It is stored under the holder, who
receives its notices, and each run carries the subject tax ID it acted on.
Four listings paginate by cursor — rules list, rules versions list,
runs list and runs steps list — and --paginate walks every page for you.
The version history is the only one whose cursor is an opaque numeric string
instead of a UUID, because it paginates by position; pass back the next_cursor
you were given and never build one yourself.
Three commands of this group are irreversible and refuse to guess:
# Replay runs the parked work again FOR REAL: it sends email and delivers webhooks.
factuarea automations runs replay <run_id> --confirm <run_id>
factuarea automations runs steps replay <run_id> 0 --confirm 0
# Deleting a rule stops it firing for good; its versions and runs stay auditable.
factuarea automations rules delete <rule_id> --confirm <rule_id>--confirm takes the last positional argument of the command: the rule id
for rules delete, the run id for runs replay and the step index for
runs steps replay. Without a TTY (or with --no-input) and without
--confirm, the command exits 2 instead of guessing. On top of that, every
mutation made with a fact_live_ key needs the explicit --live flag — see
Test mode and
Irreversible operations.
Tasks
The tasks module — included in every plan — adds 80 commands in seven groups
to the tree, each one gated by its own fine-grained scope (projects:read|write|delete,
tasks:read|write|delete, users:read and notifications:read|write), and
projects time-invoices create also needs invoices:write, because it creates an
invoice. The groups come from the operationIds of the spec:
| Group | What it covers |
|---|---|
projects | Projects, their columns (projects columns), custom fields, tasks export and import (projects tasks), the time summary and the invoice of logged hours |
tasks | Tasks and their subresources: comments, attachments, time entries (tasks time-entries), timer, labels, relations, links to documents and contacts, activity |
task-labels | The task label catalog of the company |
task-timers | The running timer of the key holder |
users | The company members a task can be assigned to |
notifications | The notifications of the key holder |
agenda | The combined agenda: due tasks, document due dates, tax deadlines and absences |
# A project: its key (one letter plus up to nine letters or digits) prefixes the keys of its tasks (WEB-1, WEB-2…)
factuarea projects list --json
factuarea projects create --name "Web redesign" --key WEB --json
factuarea projects columns list <project_id> --json
# Create a task. The full body can also link it to a document or a contact in the
# same call with `entity_link` (invoice, quote, contact, product…).
factuarea tasks create --json -d '{
"project_id": "<project_id>",
"title": "Prepare the quote",
"priority": "high",
"due_on": "2026-10-15",
"entity_link": {"type": "quote", "id": "<quote_id>"}
}'
# With flags, each custom field is `<field id>=<value>` and every value travels as text.
factuarea tasks create --project-id <project_id> --title "Call the customer" \
--custom-fields <field_id>=42 --custom-fields <other_field_id>=high
# Commands take the UUID; resolve a key such as WEB-1 first.
factuarea tasks find-by-key WEB-1 --json
# Change its status: to a column of the project, or to a virtual state (`planned` is the backlog).
factuarea tasks status <task_id> --column-id <column_id>
factuarea tasks status <task_id> --status planned
# Search, walking the cursor for you.
factuarea tasks search --project_id <project_id> --status active --paginate --json
# The task labels of the company.
factuarea task-labels list --json
# Log time: a closed period (ISO 8601 with a time zone)…
factuarea tasks time-entries list <task_id> --paginate --json
factuarea tasks time-entries create <task_id> \
--started-at 2026-09-28T09:00:00+02:00 --ended-at 2026-09-28T10:30:00+02:00 \
--description "Quote review"
# …or with the timer of the key holder, who can have only one running.
factuarea tasks timer start <task_id>
factuarea task-timers current --json # `{"data": null}` when nothing is running
factuarea task-timers stop --json
# Preview, then create, the draft invoice for the billable hours of a month.
factuarea projects time-invoices preview <project_id> \
-d '{"period":{"from":"2026-09-01","to":"2026-09-30"},"grouping":"per_task"}' --json
# Deleting a column that has tasks needs a destination for them. It is irreversible: without
# an interactive terminal you must confirm with --confirm, and what you confirm is the LAST
# positional argument (here, the deleted column).
factuarea projects columns delete <project_id> <column_id> \
--move_to_column_id <other_column_id> --confirm <column_id>Deleting projects, columns, custom fields, labels, tasks (also tasks bulk-delete),
comments, attachments and time entries is irreversible and asks for confirmation.
See the Projects and tasks guide for the model.
task-timers is not the working-day clock-in. The timer and the
tasks time-entries commands record the time spent on a task, to measure and
bill it. They are not the register of the Time tracking module
(factuarea time-entries clock-in, above), which has its own scope and its own
unalterable ledger. See Time logged on tasks.
PDF templates and document presentation
The PDF template library and the presentation of each document have their own commands, generated from the spec like the rest of the tree:
| Group | Commands |
|---|---|
pdf-templates | list, create, find, update, duplicate, archive, restore, impact, publish-revision, revision, defaults-get, defaults-set, contact-default-get, contact-default-set, asset-upload, asset-get |
pdf-commercial-identities | list, create, find, update, archive, restore, impact, publish-revision, revision |
pdf-template-previews | create |
pdf-artifacts | prepare, render, show, download |
invoices, quotes, proformas, delivery-notes | get-presentation, assign-presentation, list-presentations, revise-presentation |
series | get-pdf-template, assign-pdf-template |
All of them hang from the company axis (/v1/companies/{company}/pdf-templates,
/v1/companies/{company}/pdf-artifacts,
/v1/companies/{company}/invoices/{invoice}/presentation…), so they take the
company like any other company command: as the first positional argument, with
--company, or not at all when your credential reaches a single company. See
Company and account axes and
the company segment. Older binaries do not have these
commands: check that factuarea commands --json lists them.
# Create a template in the company library: only --name is required.
factuarea pdf-templates create --company "$COMPANY_ID" \
--name "Factura clásica" --base-preset classic --json
# Template lists paginate by page: one request per call, without --paginate.
factuarea pdf-templates list --company "$COMPANY_ID" --page 1 --per_page 20 --json
# Rename it, with the ETag of the last read as the If-Match precondition.
ETAG=$(factuarea pdf-templates find --company "$COMPANY_ID" "$TEMPLATE_ID" \
--show-etag --json 2>&1 >/dev/null | sed -n 's/^etag: //p')
factuarea pdf-templates update --company "$COMPANY_ID" "$TEMPLATE_ID" \
--name "Factura clásica 2026" --if-match "$ETAG" --json
# Pin that template on an invoice: expected_version is the selection_version
# of get-presentation…
factuarea invoices get-presentation --company "$COMPANY_ID" "$INVOICE_ID" --json
factuarea invoices assign-presentation --company "$COMPANY_ID" "$INVOICE_ID" \
--template-id "$TEMPLATE_ID" --expected-version 1 --json
# …or hand the choice back to the defaults: template_id is required and accepts null.
factuarea invoices assign-presentation --company "$COMPANY_ID" "$INVOICE_ID" \
--null-template-id --expected-version 2 --json
# Exact PDF of the invoice: prepare it from the id of its presentation, render it
# and download it.
factuarea pdf-artifacts prepare --company "$COMPANY_ID" --document-type invoice \
--document-id "$INVOICE_ID" --presentation-id "$PRESENTATION_ID" \
--purpose complete --format a4 --variant official --json
factuarea pdf-artifacts render --company "$COMPANY_ID" "$ARTIFACT_ID" --mode async --json
factuarea pdf-artifacts download --company "$COMPANY_ID" "$ARTIFACT_ID" -o invoice.pdf --show-etagSee the PDF template library, Document presentations and PDF previews and artifacts for the model.
Conditional writes and explicit null
When the spec declares a conditional header on an operation, its command gets a
flag with the same name: --if-match on the writes that accept it
(pdf-templates update, defaults-set and contact-default-set,
pdf-commercial-identities update, series assign-pdf-template and
assign-presentation on each document) and --if-none-match on the reads that
can answer 304, such as the PDF downloads. The value travels verbatim, quotes
included; an empty value is a usage error (exit 2).
--show-etag, on the commands whose response carries an ETag, writes
etag: <value> to stderr and leaves stdout untouched, so you can capture it
next to --json as in the example above. The CLI never fills an ETag or a
version for you:
| You send | When it no longer holds |
|---|---|
--if-match with an old ETag | 412 |
--expected-version with an old version | 409 |
| Neither, on a write that needs a precondition | 428 |
See Concurrency for the error codes and how to recover.
Explicit null. The body tells an omitted field apart from null, an empty
value, 0 and false. A field flag you do not pass is not sent, and a value you
pass travels as given, even --description "". To send null, use
--null-<field>, which exists only on fields that accept null, or write null
in -d/--data-file. Passing --<field> and --null-<field> together is a
usage error.
# Clear the description (null). --name is not passed, so the name is kept.
factuarea pdf-templates update --company "$COMPANY_ID" "$TEMPLATE_ID" \
--null-description --if-match "$ETAG" --jsonA required field that accepts null, such as template_id of
assign-presentation, is satisfied with its --null-<field> flag
(--null-template-id).
Binary downloads and uploads
PDF, ZIP and XML endpoints stream a binary you save with -o. Multipart
uploads take the file with a --file-<field> flag:
# Download a PDF
factuarea invoices pdf <uuid> -o invoice.pdf
# Upload a certificate (multipart)
factuarea verifactu certificates upload \
-d '{"certificate_password":"…"}' --file-certificate_file cert.p12
# Import managed accounts in bulk (multipart). Each file goes with --file-<field>:
# `accounts_file` is required, `customers_file` is optional. Options travel in -d
# as a flat JSON object keyed by the form field names: only `options[send_email]`,
# `options[apply_customers_to_own_company]` and `options[confirm_plan_changes]`
# are accepted (booleans, `false` by default).
factuarea managed-accounts imports template -o template.csv
factuarea managed-accounts imports preview \
--file-accounts_file accounts.csv --file-customers_file customers.csv # writes nothing
factuarea managed-accounts imports create \
--file-accounts_file accounts.csv --file-customers_file customers.csv \
-d '{"options[send_email]":true}' --confirm create # irreversibleWhen a PDF is not ready or has not changed
A binary command writes the exact bytes to -o, or to stdout when stdout is not a
terminal. The other answers of a PDF download never leave a partial file:
| Status | What the CLI does |
|---|---|
200 | Writes the bytes. |
202 | The document PDF (invoices pdf, quotes pdf, proformas pdf, delivery-notes pdf) is still being generated. Prints the status JSON on stdout and retry-after: <seconds> on stderr, creates no file and exits 0. Run the same command again after that time. |
304 | You passed --if-none-match with the current ETag. Exits 0 with nothing on stdout and leaves your file as it was. |
503 | pdf-artifacts download of an artifact that is not ready yet (pdf_artifact_not_ready). The CLI retries it, waiting what Retry-After says; if it is still not ready, the error on stderr carries the Retry-After value. |
# Keep the ETag of the copy you download…
factuarea invoices pdf --company "$COMPANY_ID" "$INVOICE_ID" -o invoice.pdf --show-etag
# …and download it again only if it changed: a 304 leaves invoice.pdf untouched.
factuarea invoices pdf --company "$COMPANY_ID" "$INVOICE_ID" -o invoice.pdf \
--if-none-match "$PDF_ETAG"An exit code 0 alone does not prove that a file was written: look for the
retry-after: line on stderr, or for the file. See
How downloads answer.
The api escape hatch
Any endpoint is reachable directly with factuarea api <method> <path>, even
ones without a dedicated command yet:
factuarea api get /v1/me --json
factuarea api post /v1/companies/acme_co/invoices -d '{…}'The command manifest
factuarea commands --json dumps the full manifest of commands in one call
— path, args, flags, whether each mutates, whether it is binary or paginated,
its required scope, whether it is irreversible, and an example. An agent
discovers the entire surface in a single call:
factuarea commands --jsonSee Agents & scripting for the manifest fields and the JSON contract.
Embedded API reference
A quick API reference travels with the binary — searches stay on your machine:
factuarea docs search invoicedocs search reads the OpenAPI spec embedded in the binary and answers
"which command do I call?". It returns operations — command, summary, method
and path — and never touches the network.
Searching the published docs
docs list, docs grep and docs get read the published documentation —
the llms-full corpus of docs.factuarea.com — and
answer "what do the docs say about this?". They return pages and sections, from
the guides, the API reference and the error catalog:
factuarea docs list # every page: <path> — <title>
factuarea docs list /guides # only the ones under that prefix
factuarea docs grep "idempotency-key" # documentation sections that match
factuarea docs get /guides/idempotency # the whole page, as MarkdownThe corpus is downloaded whole and once, stored in the system cache directory
(~/Library/Caches/factuarea/docs/ on macOS, ~/.cache/factuarea/docs/ on
Linux) and filtered locally. While the copy is younger than 15 minutes no
network request happens at all, so a session that chains list, grep and get
downloads once.
Your search term never leaves the machine. The URL fetched is fixed and
does not depend on what you type — there is no search server at the other end.
None of the four docs subcommands reads or sends an API key.
| Option | What it does |
|---|---|
--refresh | Downloads again, ignoring a copy that is still valid |
--lang | Language of the guides: en, es or ca (default en, the source language). The API reference is not translated and always comes through |
--json | Stable output on stdout: path/title for list, path/title/section/snippet for grep, path/title/markdown for get |
If the download fails and a cached copy exists — even an expired one — that copy
is used, a warning goes to stderr so the JSON on stdout stays parseable, and
the exit code is 0. With no copy at all, the exit code is 10 (network). Set
FACTUAREA_DOCS_URL to fetch the corpus from another origin.
REST fallback
The CLI’s command catalog comes from its bundled spec. In the September 2026 baseline, release v0.2.0 is available and the main-branch spec contains 414 entries, with 38 REST endpoints absent from generated commands. Inspect factuarea commands --json for your installed version. Use the raw HTTP command for a supported REST endpoint missing from that catalog:
factuarea api get /v1/companies/acme_co/stores