Projects and tasks
Projects with keys and board columns, tasks with positions, labels, comments, relations, custom fields, links to documents and attachments — the work-management API under /v1/projects and /v1/tasks.
Factuarea includes a native task module: projects with a board of columns,
and tasks that can be linked to the invoices, quotes, contacts and other
documents of your company. The API exposes it under /v1/projects,
/v1/tasks, /v1/task-labels, /v1/task-timers, /v1/users,
/v1/notifications and /v1/agenda. Every operation is also an
MCP tool and a CLI command.
Tasks need the tasks module, which every plan includes, and the
fine-grained scopes projects:read|write|delete, tasks:read|write|delete,
users:read and notifications:read|write. Issuing a key with those scopes
for a company without the module is rejected with
scope_not_allowed_by_plan. The projects:delete and tasks:delete scopes are API-key-only: an OAuth
consent never grants them. See
Scopes & irreversibility.
Projects and keys
A project groups tasks and owns the board they live on. Its key — one
uppercase letter followed by up to nine uppercase letters or digits, such as
DEV — prefixes the key of every task: DEV-1, DEV-2… Keys are unique in
your company, ignoring case and counting archived projects. A key already in use
returns 409 project_key_in_use.
curl -X POST https://api.factuarea.com/v1/projects \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Web redesign",
"key": "WEB",
"icon": "rocket",
"description": "Storefront redesign and follow-up fixes."
}'The project is created with its default board columns and comes back with its
task statistics (total_tasks, completed_tasks, completion_percentage and
next_due_on).
GET /v1/projectslists the projects in the order the app shows them. Archived projects stay out unless you passinclude_archived=true.POST /v1/projects/find-by-keyresolves a project from its key (WEB), ignoring case and archived or not. It is a read: it needs noIdempotency-Key.PUT /v1/projects/{project}is partial. Changing thekeykeeps the previous one as an alias, so tasks keep answering to their old keys. The billing defaults used to invoice logged time live here too — see Time logged on tasks.POST /v1/projects/{project}/archiveand.../unarchiveare reversible: an archived project leaves the default listings and stops accepting new tasks (422project_archived), but everything stays readable.DELETE /v1/projects/{project}is permanent and takes the project's tasks, comments, attachments and logged time with it.
Columns and board status
A task is always in one of three board states:
status | Where the task is | column_id |
|---|---|---|
planned | The backlog: waiting to be scheduled | null |
active | In a column of the board | The column |
archived | Archived, out of the board | null |
planned and archived are virtual states, not columns, and their names are
reserved: a column cannot use them as its slug (422 column_slug_reserved).
A new project gets four columns — to do, in progress, in review and done — named
in the language of the user who creates it, with the canonical slugs to-do,
in-progress, in-review and done. Only the last one is final
(is_final: true): moving a task into a final column completes it
(completed_at is set), and moving it out reopens it.
POST /v1/projects/{project}/columnsadds a column at the end of the board. Itsslugderives from the name and is unique in the project (409column_slug_in_use).coloris a palette key (gray,red,orange,amber,green,teal,blue,cyan,violetorpink) or a hexadecimal code.PUT .../columns/{column}is partial. Turning a column final completes the tasks it holds; turning it back reopens them.PUT .../columns/reordertakescolumn_idswith every column of the project exactly once, in the new order.- Columns come back as a plain list, without pagination.
Deleting a column that has tasks
Deleting a column is permanent, and a column with tasks needs somewhere to send
them. Pass the destination column of the same project in the
move_to_column_id query parameter:
curl -X DELETE \
"https://api.factuarea.com/v1/projects/$PROJECT_ID/columns/$COLUMN_ID?move_to_column_id=$OTHER_COLUMN_ID" \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"In one transaction the tasks are appended to the end of the destination column,
in their order, each one emits its task.status_changed event, and the column
is deleted. Without move_to_column_id, a column that still has tasks returns
409 column_has_tasks and nothing changes; a
destination that does not exist, belongs to another project or is the column
being deleted returns 422
invalid_move_target_column. An empty
column needs no destination. The operation is irreversible, so
Idempotency-Key is required.
Tasks, keys and the board
project_id and title are the only required fields of
POST /v1/tasks. A task
starts in the first column of its project unless you send a column_id or a
virtual status. The optional fields are description (Markdown), priority
(none, low, medium, high or urgent; none by default), start_on and
due_on (YYYY-MM-DD; a start_on after due_on returns 422
invalid_task_schedule), assignee_id (a member of your company), label_ids,
custom_fields and entity_link.
curl -X POST https://api.factuarea.com/v1/tasks \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "0193a4f2-7c20-7a11-8b52-4d6e8f0a2c01",
"title": "Call the customer about the checkout demo",
"priority": "high",
"due_on": "2026-10-15",
"assignee_id": "0193a4f2-6b1c-7d3e-8a41-2c5e7f9a1b02",
"label_ids": ["0193a4f2-9a64-7e55-8f96-8b0c2d4e6a02"],
"custom_fields": { "0193a4f2-7e42-7c33-8d74-6f8a0b2c4e01": 4 },
"entity_link": { "type": "quote", "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a4d" }
}'The task comes back with its key (DEV-16), its number in the project,
its board status and column_id, its labels, its custom field values and
counters for subtasks, comments and attachments. The number only grows: a
deleted task never frees its number for reuse.
Resolve a task from its key
People talk about DEV-12, not about a UUID.
POST /v1/tasks/find-by-key takes { "key": "DEV-12" }, ignoring case, and
returns the task. Previous keys keep working: a task moved from DEV-12 to
another project, or whose project changed its key, is still found by the old
key. A malformed key returns 422 invalid_task_key
and an unknown one 404 task_not_found. The
q filter of the search accepts a key too.
Search
GET /v1/tasks searches
across projects:
curl "https://api.factuarea.com/v1/tasks?project_id=$PROJECT_ID&status=active&assignee_id=me&sort=due_on&limit=50" \
-H "Authorization: Bearer $FACTUAREA_API_KEY"The filters are q, project_id, status, column_id, priority,
assignee_id (a member UUID or me), label_id, due_before, due_after and
completed. sort accepts created_at, updated_at, due_on, priority or
number, with a - prefix for descending.
Status, position and movement
POST /v1/tasks/{task}/statusmoves a task to acolumn_idof its project or to a virtualstatus, at the end of the destination.POST /v1/tasks/{task}/repositionplaces it at an exact position: anindex(0 is first) or a neighbour,before_task_idorafter_task_id, and optionally another column in the same call.POST /v1/tasks/{task}/movesends it to another project. The task gets a new key there and keeps answering to the previous one; custom field values the destination does not define are dropped.POST /v1/tasks/{task}/duplicatecopies it to the end of the same column. The copy keeps description, priority, dates, labels, custom field values and assignee; comments, activity, time entries and links are not copied.POST /v1/tasks/{task}/assignand.../unassignset and clear the assignee. Find member ids withGET /v1/users; a user who is not a member returns 422task_assignee_not_member.
Edit and delete
PUT /v1/tasks/{task} is partial: only the fields you send change, and an
explicit null clears description, start_on, due_on and assignee_id.
priority and title cannot be null.
DELETE /v1/tasks/{task} is permanent. It takes the task's comments,
attachments, relations and time entries with it, and it is irreversible:
Idempotency-Key is required.
Bulk operations
Three operations act on up to 200 tasks at once, all or nothing:
POST /v1/tasks/bulk-status, POST /v1/tasks/bulk-update (priority, assignee,
due date and one label to add or remove) and POST /v1/tasks/bulk-delete. All
three require an Idempotency-Key. A larger selection returns 422
bulk_task_selection_too_large; tasks that no longer exist are skipped and the
result reports how many changed. See Bulk operations.
Labels, comments, relations and custom fields
Task labels live in a catalog per company and are unrelated to the tags of
documents: see Tags and custom fields for
those. A label has a name, unique in your company ignoring case (409
task_label_name_in_use), and a color from the palette or a hexadecimal code.
The catalog lists each label with its tasks_count: how many tasks carry it,
archived ones included. POST /v1/tasks/{task}/labels adds a label to a task
and DELETE .../labels/{label} removes it; deleting a label from the catalog
removes it from every task.
Comments are Markdown, from 1 to 10,000 characters. Mentions of members of
the company notify the people mentioned; mentions of anyone else are ignored.
The author of a comment has a type: user, external (synchronized from a
code forge), imported or automation. Only the author can edit a comment, and
comments synchronized or imported cannot be edited (422
task_comment_not_editable); the author and the owners and admins of the
company can delete one.
Relations join two tasks of the company as subtask, blocks or related.
A task cannot relate to itself (422 task_self_relation), a relation that would
close a cycle is rejected (422 task_relation_cycle) and a duplicate returns 409
task_relation_exists. The list shows each relation from the point of view of
the task in the path, through direction: parent, child, blocks,
blocked_by or related. Deleting a relation leaves both tasks untouched.
Task custom fields are defined per project, with a type of text,
number, date, dropdown, boolean or multiselect, and they are not the
custom fields of documents. A required field needs a non-empty default value,
which existing tasks receive. Set values when you create the task, with the
custom_fields map (field id to value), or later with
PUT /v1/tasks/{task}/custom-fields/{field}; null clears an optional field. A
value that does not fit the type returns 422 invalid_custom_field_value, and a
definition that contradicts itself, or an edit that would strand existing
values, 422 invalid_custom_field_definition. Deleting a definition deletes its
values in every task of the project.
Links to documents, contacts and other tools
A task can point at the rest of Factuarea. The vocabulary of linkable entities
is closed: invoice, quote, proforma, delivery_note, purchase_invoice,
recurring_invoice, contact, product and employee.
- At creation,
entity_linklinks the task in the same transaction. If the entity does not exist, belongs to another company or its module is not accessible, the call returns 404linked_entity_not_foundand the task is not created. - Afterwards,
POST /v1/tasks/{task}/entity-linkslinks it; linking the same entity again returns the existing link. A link to an entity that was deleted stays in the list withavailable: false. - From the entity,
GET /v1/tasks/linked?entity_type=invoice&entity_id=…lists the tasks linked to an invoice, a quote or any other entity, archived ones flagged. - External links —
POST /v1/tasks/{task}/external-links— attach a plainhttporhttpsaddress. Links that mirror an issue, a pull request or a branch of a code forge (GitHub, GitLab or Gitea) are kept by the integration.
Attachments and upload links
POST /v1/tasks/{task}/attachments uploads a file as multipart/form-data. The
maximum is 10 MB per file, and the type is checked by content, never by
extension: images (PNG, JPEG, GIF, WebP, HEIC, AVIF), PDF, Word, Excel and
PowerPoint documents, OpenDocument text and spreadsheets, plain text, CSV,
Markdown and ZIP. target places the file in a new comment (comment, the
default) or at the end of the description (description), and note becomes
the comment text. Files count towards the storage quota of the plan (402
storage_quota_exceeded when it is full) and the upload needs an
Idempotency-Key.
To receive a photo from a phone, or a file from someone without an account,
create an upload link with POST /v1/tasks/{task}/upload-links. The link is
single-use, expires after 30 minutes, and its url is shown only in that
response. Once it is used or expired it answers 410
task_upload_link_expired.
Attachments are listed, read and downloaded per task, and
DELETE .../attachments/{attachment} removes the file for good and frees its
storage.
Activity
GET /v1/tasks/{task}/activities
returns the activity log, newest first: status and column changes, edits with
the changed fields, assignments, labels, comments, relations, attachments and
logged time, each with its actor (user, api_key, external or system).
Entries have no id, so the cursor is opaque: pass the next_cursor back
as starting_after untouched.
Pagination, partial updates and errors
- Cursor pagination. Lists of projects, tasks, labels, comments, time entries,
users, notifications and activity return
{ data, has_more, next_cursor }; passnext_cursorasstarting_afterfor the next page. Columns, custom field definitions, relations, links and attachments are short and come back as a plain list. See Pagination. PUTis partial everywhere: omitted fields keep their value, and an explicitnullclears the optional ones.- Idempotency. The nine irreversible operations, the bulk operations, the
import and the file upload require an
Idempotency-Key; every other write accepts it. See Idempotency. - Errors. Every task error has a stable code, listed under Tasks error codes.
Some parts of the module exist only in the app and have no operation in the API: the integrations with code forges and chat tools, the iCal feeds (see the iCal feed guide), the read-only public board, the project background, the order of the projects and the notification preferences.
Next steps
manual entries, the timer and turning billable hours into a draft invoice.
subscribe to the tasks of a project, or your own, from a calendar app.
the 21 events of tasks, comments, logged time and projects.
create, move, assign and comment on tasks from a rule.
the same surface as tools for an agent.
projects, tasks and time from the terminal.
every code with its cause and what to do.