Recent operations join the axis
The operations released since 21 September — projects and tasks, the purchase scanner, notifications, users, VeriFactu representation, issuing without sending and series editing — live under /v1/companies/{company}, and eight partial updates move from PUT to PATCH.
5 October 2026 — the operations that reached the API since 21 September were published on flat paths, before the company axis existed. They now live where every other company resource lives, under /v1/companies/{company}/…: the operations of the tables below. This is the same migration as the axis window, applied to what came after it, and it needs no second step.
- No flat alias. A call to the old path answers the standard
404of a route that does not exist. Use the path in the Now column. - Nothing else changes by default. Route names (
public-api.v1.*), scopes, bodies and responses stay as published, except what the sections below state: eight verbs, a few paths that change more than the prefix, a handful of error statuses and one webhook payload version. - The company comes from the credential. The
{company}segment is validated against the scope of the credential:403company_inactiveif the tax ID is inactive, and404profile_not_foundif it is outside the scope, does not exist or belongs to the other environment. The three404look the same on purpose. Locationcarries the axis. The creations ofPOST …/tasks,…/tasks/{task}/duplicate,…/projects,…/task-labelsandPOST …/verifactu/representationanswer/v1/companies/{company}/<resource>/<id>.
Adapt once. Replace the path, send PATCH where the table says so, read the new statuses, and your client is done. There is no compatibility mode for the old paths and verbs.
Verbs: PUT becomes PATCH
The partial updates of the new families follow the convention of the rest of the API: PATCH changes only the fields you send. PUT stays where the whole resource is replaced by design.
| Operation | Before | Now |
|---|---|---|
public-api.v1.projects.update | PUT /v1/projects/{project} | PATCH /v1/companies/{company}/projects/{project} |
public-api.v1.projects.columns.update | PUT /v1/projects/{project}/columns/{column} | PATCH /v1/companies/{company}/projects/{project}/columns/{column} |
public-api.v1.projects.custom_fields.update | PUT /v1/projects/{project}/custom-fields/{field} | PATCH /v1/companies/{company}/projects/{project}/custom-fields/{field} |
public-api.v1.task_labels.update | PUT /v1/task-labels/{label} | PATCH /v1/companies/{company}/task-labels/{label} |
public-api.v1.tasks.update | PUT /v1/tasks/{task} | PATCH /v1/companies/{company}/tasks/{task} |
public-api.v1.tasks.time_entries.update | PUT /v1/tasks/{task}/time-entries/{time_entry} | PATCH /v1/companies/{company}/tasks/{task}/time-entries/{time_entry} |
public-api.v1.purchase_scans.review | PUT /v1/purchase_scans/{purchase_scan}/review | PATCH /v1/companies/{company}/purchase-scans/{purchase_scan}/review |
public-api.v1.series.update | PUT /v1/series/{series} | PATCH /v1/companies/{company}/series/{series} |
Still PUT, because they replace the whole resource: projects.columns.reorder, tasks.comments.update and tasks.custom_fields.set. A PUT to one of the eight migrated paths answers 405.
Editing a series keeps the same partial body and the same fiscal guards (BR-SER-020, BR-SER-021, 422 with its subcode). On …/series/{series} the 405 series_immutable now carries Allow: GET, PATCH and its message points to PATCH /v1/companies/{company}/series/{id} and to POST …/archive; PUT and DELETE answer 405. The MCP tool update_series is the same tool, described as a partial PATCH; its arguments do not change.
Paths that change more than the prefix
| Operation | Before | Now |
|---|---|---|
public-api.v1.purchase_invoices.expense_categories | GET /v1/purchase_invoices/expense_categories | GET /v1/companies/{company}/purchase-invoices/expense-categories |
public-api.v1.users.list | GET /v1/users | GET /v1/companies/{company}/users |
public-api.v1.users.me | GET /v1/users/me | GET /v1/companies/{company}/users/me |
public-api.v1.contacts.imports.show | GET /v1/contacts/imports/{id} | GET /v1/companies/{company}/contacts/imports/{id} |
public-api.v1.purchase_scans.list | GET /v1/purchase_scans | GET /v1/companies/{company}/purchase-scans |
public-api.v1.purchase_scans.duplicate_resolution | POST /v1/purchase_scans/{purchase_scan}/duplicate_resolution | POST /v1/companies/{company}/purchase-scans/{purchase_scan}/duplicate-resolution |
public-api.v1.purchase_scan_emails.list | GET /v1/purchase_scan_emails | GET /v1/companies/{company}/purchase-scan-emails |
Two visible consequences: the source_url of the scanner responses and the error_report_url of the contact imports now carry the axis. The 101 MB upload limit of the scanner applies to POST /v1/companies/<uuid>/purchase-scans.
Error codes and statuses
| Case | Before | Now |
|---|---|---|
idempotency_key_reused: an Idempotency-Key sent again with another body | 409, type: idempotency_error | 422, same code and type. It applies to every operation that documented the 409: tasks and projects, the purchase scanner (including an upload with another file), the unattended checkout with its header, and the batch of the hours of a project |
A checkout resent by external_id with another type or total | 409 with code: idempotency_key_reused, subcode: unattended_replay_mismatch | code: unattended_replay_mismatch, same 409, type, subcode and param: external_id. One code, one status: idempotency_key_reused is only for the header (422) |
The header X-Active-Profile | Accepted by the new families | 400 parameter_unknown, subcode retired_active_profile_header: the company travels in the path |
{company} outside the scope of the credential, nonexistent or from the other environment | Not applicable: the company came from the credential | 404 profile_not_found; 403 company_inactive if it is inactive |
api_key_creator_not_member (scanner) | Membership by pivot row | Same code; membership is decided by the access of the creator to the tax ID |
409 idempotency_key_in_use (the key is in flight) does not change.
The flat /v1/clients and /v1/suppliers operations and the *_client and *_supplier MCP tools stay retired; contacts replace them under /v1/companies/{company}/contacts. The tool names search_clients, find_client_by_tax_id, create_client and bulk_create_clients keep resolving to the contact tools until 15 November 2026, flagged with _meta["io.factuarea/deprecation"].
Webhook payload version
The payload versions are now 2026-05-22, 2026-10-01 and 2026-10-05 (the latest). 2026-10-01 is the one that introduced the issued vocabulary of invoices; 2026-10-05 adds amounts as decimal strings and paid_recorded_at and paid_date. An endpoint pinned to 2026-10-01 or 2026-05-22 does not change: it keeps numeric amounts, paid_at and paid_on. An endpoint created without an explicit api_version stays on 2026-05-22, unless the request that creates it uses the REST version 2026-10-01, in which case it takes the latest one (2026-10-05). You can move an existing endpoint with PATCH and api_version. The request version Factuarea-Version: 2026-10-01 keeps its date and effect. See Webhook payload versions.
MCP
The tools take no company argument: the company comes from the token or the session. The tools of projects and tasks, notifications, users, agenda, the scanner, the VeriFactu representation, issue_invoice, list_purchase_invoice_expense_categories and the contact imports (get_contact_import, download_contact_import_errors) mirror one to one the routes of the tables below. A domain error of tools/call is now a result with isError: true, the message of the domain and _meta["com.factuarea/error"] (code, subcode, param, with the same status as the REST API).
Other contract changes
- Contact import templates.
GET /v1/companies/{company}/contacts/import/templatetakesformat:csv(default, separated by;) orxlsx, a workbook with the Data, Instructions, Examples and Allowed values sheets. Both come from the same column descriptors as the importer. - Import reports. The import resource (
GET …/contacts/imports/{id}and the response ofPOST …/contacts/import) addsupdated_count,warnings_count,outcome_kind,skip_reasons,first_diagnosticsandoutcomes_truncated, and each row ofoutcomescarries areason.outcome_kind: nothing_changedmeans the import finished without creating or updating anything: do not present it as a success. The new codemapping_header_not_foundis returned bycontacts.importandcontacts.preview_import. - Sales documents. Invoices (and their events) publish
origin(nativeorhistorical_import); quotes and pro-forma invoices publishreferenceandoperation_regime. - Task import.
POST …/projects/{project}/tasks/importand the MCP toolimport_project_tasksaccept an optionalidempotency_keyand reportwarnings_totalandwarnings_truncated. - Scanner writes without access. On the writes of the purchase scanner (
POST …/purchase-scans,…/retry,PATCH …/review,…/duplicate-resolution,…/convert,DELETE …/{purchase_scan},…/restore), a key whose creator has no access to the company now answers404profile_not_foundinstead of403api_key_creator_not_member. The403stays on the download of the original (GET …/source) and on the MCP write tools. - Payment amounts in the activity. In
GET …/invoices/{invoice}/activities(and the toolget_invoice_activities),metadata.amountof the eventspayment.payment_createdandpayment.payment_reversedis now a two-decimal string ("50.50"), not a number in euros.
Every operation, by family
Projects and tasks
Tasks, projects, labels, timers and the agenda (75).
| Operation | Before | Now |
|---|---|---|
public-api.v1.agenda.list | GET /v1/agenda | GET /v1/companies/{company}/agenda |
public-api.v1.projects.archive | POST /v1/projects/{project}/archive | POST /v1/companies/{company}/projects/{project}/archive |
public-api.v1.projects.columns.create | POST /v1/projects/{project}/columns | POST /v1/companies/{company}/projects/{project}/columns |
public-api.v1.projects.columns.delete | DELETE /v1/projects/{project}/columns/{column} | DELETE /v1/companies/{company}/projects/{project}/columns/{column} |
public-api.v1.projects.columns.list | GET /v1/projects/{project}/columns | GET /v1/companies/{company}/projects/{project}/columns |
public-api.v1.projects.columns.reorder | PUT /v1/projects/{project}/columns/reorder | PUT /v1/companies/{company}/projects/{project}/columns/reorder |
public-api.v1.projects.columns.update | PUT /v1/projects/{project}/columns/{column} | PATCH /v1/companies/{company}/projects/{project}/columns/{column} |
public-api.v1.projects.create | POST /v1/projects | POST /v1/companies/{company}/projects |
public-api.v1.projects.custom_fields.create | POST /v1/projects/{project}/custom-fields | POST /v1/companies/{company}/projects/{project}/custom-fields |
public-api.v1.projects.custom_fields.delete | DELETE /v1/projects/{project}/custom-fields/{field} | DELETE /v1/companies/{company}/projects/{project}/custom-fields/{field} |
public-api.v1.projects.custom_fields.list | GET /v1/projects/{project}/custom-fields | GET /v1/companies/{company}/projects/{project}/custom-fields |
public-api.v1.projects.custom_fields.update | PUT /v1/projects/{project}/custom-fields/{field} | PATCH /v1/companies/{company}/projects/{project}/custom-fields/{field} |
public-api.v1.projects.delete | DELETE /v1/projects/{project} | DELETE /v1/companies/{company}/projects/{project} |
public-api.v1.projects.find_by_key | POST /v1/projects/find-by-key | POST /v1/companies/{company}/projects/find-by-key |
public-api.v1.projects.list | GET /v1/projects | GET /v1/companies/{company}/projects |
public-api.v1.projects.show | GET /v1/projects/{project} | GET /v1/companies/{company}/projects/{project} |
public-api.v1.projects.tasks.export | GET /v1/projects/{project}/tasks/export | GET /v1/companies/{company}/projects/{project}/tasks/export |
public-api.v1.projects.tasks.import | POST /v1/projects/{project}/tasks/import | POST /v1/companies/{company}/projects/{project}/tasks/import |
public-api.v1.projects.time_invoices.create | POST /v1/projects/{project}/time-invoices | POST /v1/companies/{company}/projects/{project}/time-invoices |
public-api.v1.projects.time_invoices.preview | POST /v1/projects/{project}/time-invoices/preview | POST /v1/companies/{company}/projects/{project}/time-invoices/preview |
public-api.v1.projects.time_summary.show | GET /v1/projects/{project}/time-summary | GET /v1/companies/{company}/projects/{project}/time-summary |
public-api.v1.projects.unarchive | POST /v1/projects/{project}/unarchive | POST /v1/companies/{company}/projects/{project}/unarchive |
public-api.v1.projects.update | PUT /v1/projects/{project} | PATCH /v1/companies/{company}/projects/{project} |
public-api.v1.task_labels.create | POST /v1/task-labels | POST /v1/companies/{company}/task-labels |
public-api.v1.task_labels.delete | DELETE /v1/task-labels/{label} | DELETE /v1/companies/{company}/task-labels/{label} |
public-api.v1.task_labels.list | GET /v1/task-labels | GET /v1/companies/{company}/task-labels |
public-api.v1.task_labels.show | GET /v1/task-labels/{label} | GET /v1/companies/{company}/task-labels/{label} |
public-api.v1.task_labels.update | PUT /v1/task-labels/{label} | PATCH /v1/companies/{company}/task-labels/{label} |
public-api.v1.task_timers.current | GET /v1/task-timers/current | GET /v1/companies/{company}/task-timers/current |
public-api.v1.task_timers.stop | POST /v1/task-timers/stop | POST /v1/companies/{company}/task-timers/stop |
public-api.v1.tasks.activities.list | GET /v1/tasks/{task}/activities | GET /v1/companies/{company}/tasks/{task}/activities |
public-api.v1.tasks.assign | POST /v1/tasks/{task}/assign | POST /v1/companies/{company}/tasks/{task}/assign |
public-api.v1.tasks.attachments.create | POST /v1/tasks/{task}/attachments | POST /v1/companies/{company}/tasks/{task}/attachments |
public-api.v1.tasks.attachments.delete | DELETE /v1/tasks/{task}/attachments/{attachment} | DELETE /v1/companies/{company}/tasks/{task}/attachments/{attachment} |
public-api.v1.tasks.attachments.download | GET /v1/tasks/{task}/attachments/{attachment}/download | GET /v1/companies/{company}/tasks/{task}/attachments/{attachment}/download |
public-api.v1.tasks.attachments.list | GET /v1/tasks/{task}/attachments | GET /v1/companies/{company}/tasks/{task}/attachments |
public-api.v1.tasks.attachments.show | GET /v1/tasks/{task}/attachments/{attachment} | GET /v1/companies/{company}/tasks/{task}/attachments/{attachment} |
public-api.v1.tasks.bulk_delete | POST /v1/tasks/bulk-delete | POST /v1/companies/{company}/tasks/bulk-delete |
public-api.v1.tasks.bulk_status | POST /v1/tasks/bulk-status | POST /v1/companies/{company}/tasks/bulk-status |
public-api.v1.tasks.bulk_update | POST /v1/tasks/bulk-update | POST /v1/companies/{company}/tasks/bulk-update |
public-api.v1.tasks.comments.create | POST /v1/tasks/{task}/comments | POST /v1/companies/{company}/tasks/{task}/comments |
public-api.v1.tasks.comments.delete | DELETE /v1/tasks/{task}/comments/{comment} | DELETE /v1/companies/{company}/tasks/{task}/comments/{comment} |
public-api.v1.tasks.comments.list | GET /v1/tasks/{task}/comments | GET /v1/companies/{company}/tasks/{task}/comments |
public-api.v1.tasks.comments.update | PUT /v1/tasks/{task}/comments/{comment} | PUT /v1/companies/{company}/tasks/{task}/comments/{comment} |
public-api.v1.tasks.create | POST /v1/tasks | POST /v1/companies/{company}/tasks |
public-api.v1.tasks.custom_fields.set | PUT /v1/tasks/{task}/custom-fields/{field} | PUT /v1/companies/{company}/tasks/{task}/custom-fields/{field} |
public-api.v1.tasks.delete | DELETE /v1/tasks/{task} | DELETE /v1/companies/{company}/tasks/{task} |
public-api.v1.tasks.duplicate | POST /v1/tasks/{task}/duplicate | POST /v1/companies/{company}/tasks/{task}/duplicate |
public-api.v1.tasks.entity_links.create | POST /v1/tasks/{task}/entity-links | POST /v1/companies/{company}/tasks/{task}/entity-links |
public-api.v1.tasks.entity_links.delete | DELETE /v1/tasks/{task}/entity-links/{link} | DELETE /v1/companies/{company}/tasks/{task}/entity-links/{link} |
public-api.v1.tasks.entity_links.list | GET /v1/tasks/{task}/entity-links | GET /v1/companies/{company}/tasks/{task}/entity-links |
public-api.v1.tasks.external_links.create | POST /v1/tasks/{task}/external-links | POST /v1/companies/{company}/tasks/{task}/external-links |
public-api.v1.tasks.external_links.delete | DELETE /v1/tasks/{task}/external-links/{link} | DELETE /v1/companies/{company}/tasks/{task}/external-links/{link} |
public-api.v1.tasks.external_links.list | GET /v1/tasks/{task}/external-links | GET /v1/companies/{company}/tasks/{task}/external-links |
public-api.v1.tasks.find_by_key | POST /v1/tasks/find-by-key | POST /v1/companies/{company}/tasks/find-by-key |
public-api.v1.tasks.labels.assign | POST /v1/tasks/{task}/labels | POST /v1/companies/{company}/tasks/{task}/labels |
public-api.v1.tasks.labels.unassign | DELETE /v1/tasks/{task}/labels/{label} | DELETE /v1/companies/{company}/tasks/{task}/labels/{label} |
public-api.v1.tasks.linked | GET /v1/tasks/linked | GET /v1/companies/{company}/tasks/linked |
public-api.v1.tasks.move | POST /v1/tasks/{task}/move | POST /v1/companies/{company}/tasks/{task}/move |
public-api.v1.tasks.relations.create | POST /v1/tasks/{task}/relations | POST /v1/companies/{company}/tasks/{task}/relations |
public-api.v1.tasks.relations.delete | DELETE /v1/tasks/{task}/relations/{relation} | DELETE /v1/companies/{company}/tasks/{task}/relations/{relation} |
public-api.v1.tasks.relations.list | GET /v1/tasks/{task}/relations | GET /v1/companies/{company}/tasks/{task}/relations |
public-api.v1.tasks.reposition | POST /v1/tasks/{task}/reposition | POST /v1/companies/{company}/tasks/{task}/reposition |
public-api.v1.tasks.search | GET /v1/tasks | GET /v1/companies/{company}/tasks |
public-api.v1.tasks.show | GET /v1/tasks/{task} | GET /v1/companies/{company}/tasks/{task} |
public-api.v1.tasks.status | POST /v1/tasks/{task}/status | POST /v1/companies/{company}/tasks/{task}/status |
public-api.v1.tasks.time_entries.create | POST /v1/tasks/{task}/time-entries | POST /v1/companies/{company}/tasks/{task}/time-entries |
public-api.v1.tasks.time_entries.delete | DELETE /v1/tasks/{task}/time-entries/{time_entry} | DELETE /v1/companies/{company}/tasks/{task}/time-entries/{time_entry} |
public-api.v1.tasks.time_entries.list | GET /v1/tasks/{task}/time-entries | GET /v1/companies/{company}/tasks/{task}/time-entries |
public-api.v1.tasks.time_entries.show | GET /v1/tasks/{task}/time-entries/{time_entry} | GET /v1/companies/{company}/tasks/{task}/time-entries/{time_entry} |
public-api.v1.tasks.time_entries.update | PUT /v1/tasks/{task}/time-entries/{time_entry} | PATCH /v1/companies/{company}/tasks/{task}/time-entries/{time_entry} |
public-api.v1.tasks.timer.start | POST /v1/tasks/{task}/timer/start | POST /v1/companies/{company}/tasks/{task}/timer/start |
public-api.v1.tasks.unassign | POST /v1/tasks/{task}/unassign | POST /v1/companies/{company}/tasks/{task}/unassign |
public-api.v1.tasks.update | PUT /v1/tasks/{task} | PATCH /v1/companies/{company}/tasks/{task} |
public-api.v1.tasks.upload_links.create | POST /v1/tasks/{task}/upload-links | POST /v1/companies/{company}/tasks/{task}/upload-links |
Purchase scanner
The purchase scanner and its mailbox (12). Segments are in kebab-case.
| Operation | Before | Now |
|---|---|---|
public-api.v1.purchase_scan_emails.list | GET /v1/purchase_scan_emails | GET /v1/companies/{company}/purchase-scan-emails |
public-api.v1.purchase_scans.archive | DELETE /v1/purchase_scans/{purchase_scan} | DELETE /v1/companies/{company}/purchase-scans/{purchase_scan} |
public-api.v1.purchase_scans.convert | POST /v1/purchase_scans/{purchase_scan}/convert | POST /v1/companies/{company}/purchase-scans/{purchase_scan}/convert |
public-api.v1.purchase_scans.create | POST /v1/purchase_scans | POST /v1/companies/{company}/purchase-scans |
public-api.v1.purchase_scans.duplicate_resolution | POST /v1/purchase_scans/{purchase_scan}/duplicate_resolution | POST /v1/companies/{company}/purchase-scans/{purchase_scan}/duplicate-resolution |
public-api.v1.purchase_scans.list | GET /v1/purchase_scans | GET /v1/companies/{company}/purchase-scans |
public-api.v1.purchase_scans.restore | POST /v1/purchase_scans/{purchase_scan}/restore | POST /v1/companies/{company}/purchase-scans/{purchase_scan}/restore |
public-api.v1.purchase_scans.retry | POST /v1/purchase_scans/{purchase_scan}/retry | POST /v1/companies/{company}/purchase-scans/{purchase_scan}/retry |
public-api.v1.purchase_scans.review | PUT /v1/purchase_scans/{purchase_scan}/review | PATCH /v1/companies/{company}/purchase-scans/{purchase_scan}/review |
public-api.v1.purchase_scans.show | GET /v1/purchase_scans/{purchase_scan} | GET /v1/companies/{company}/purchase-scans/{purchase_scan} |
public-api.v1.purchase_scans.source | GET /v1/purchase_scans/{purchase_scan}/source | GET /v1/companies/{company}/purchase-scans/{purchase_scan}/source |
public-api.v1.purchase_scans.stats | GET /v1/purchase_scans/stats | GET /v1/companies/{company}/purchase-scans/stats |
Notifications
The notifications of the credential holder (3).
| Operation | Before | Now |
|---|---|---|
public-api.v1.notifications.list | GET /v1/notifications | GET /v1/companies/{company}/notifications |
public-api.v1.notifications.mark_all_read | POST /v1/notifications/mark-all-read | POST /v1/companies/{company}/notifications/mark-all-read |
public-api.v1.notifications.read | POST /v1/notifications/{notification}/read | POST /v1/companies/{company}/notifications/{notification}/read |
VeriFactu: representation and blocked records
Representation and retry of blocked records (4).
| Operation | Before | Now |
|---|---|---|
public-api.v1.verifactu.records.retry_blocked | POST /v1/verifactu/records/retry-blocked | POST /v1/companies/{company}/verifactu/records/retry-blocked |
public-api.v1.verifactu.representation.register | POST /v1/verifactu/representation | POST /v1/companies/{company}/verifactu/representation |
public-api.v1.verifactu.representation.revoke | DELETE /v1/verifactu/representation | DELETE /v1/companies/{company}/verifactu/representation |
public-api.v1.verifactu.representation.show | GET /v1/verifactu/representation | GET /v1/companies/{company}/verifactu/representation |
Invoices, contact imports, users, expense categories and series
(7)
| Operation | Before | Now |
|---|---|---|
public-api.v1.invoices.issue | POST /v1/invoices/{invoice}/issue | POST /v1/companies/{company}/invoices/{invoice}/issue |
public-api.v1.contacts.imports.show | GET /v1/contacts/imports/{id} | GET /v1/companies/{company}/contacts/imports/{id} |
public-api.v1.contacts.imports.errors | GET /v1/contacts/imports/{id}/errors.csv | GET /v1/companies/{company}/contacts/imports/{id}/errors.csv |
public-api.v1.users.list | GET /v1/users | GET /v1/companies/{company}/users |
public-api.v1.users.me | GET /v1/users/me | GET /v1/companies/{company}/users/me |
public-api.v1.purchase_invoices.expense_categories | GET /v1/purchase_invoices/expense_categories | GET /v1/companies/{company}/purchase-invoices/expense-categories |
public-api.v1.series.update | PUT /v1/series/{series} | PATCH /v1/companies/{company}/series/{series} |