Skip to content

API ​

  Table of Contents

Factorial's API supports deterministic integrations and data changes outside the user interface. It provides nearly all functionality available through the Factorial application. See Integrations for guidance on combining the API with Factorial's pollable event stream.

API Keys ​

Custom Pages served by Factorial can use the signed-in browser session for API reads and writes. Same-origin requests send the session cookie automatically; no API key or token handling is needed in the page's JavaScript.

Factorial authenticates API requests with API keys. Create a key from the Keys page, copy it when it is displayed, and send it in the HTTP Authorization header:

http
Authorization: Bearer <api-key>

Factorial displays the secret only when the key is created and stores a hash rather than the original value. Keys expire after three months and may be revoked from the Keys page. Use the key's description to identify its purpose, owner, and environment.

Treat an API key like a password. Store it in a secrets manager, exclude it from source control and logs, and rotate it before expiration. Revoke and replace it immediately if it may have been exposed.

API Keys and Service Accounts ​

For an integration, automation, or AI agent, create a dedicated service-account user. Do not use an employee's account for a long-lived integration. Use a separate service account for each independent workload and assign only the roles it needs.

API keys use the user's current roles

A user API key acts as the user it belongs to, with that user's current roles. Changing the service account's roles changes what its existing keys can do immediately. A key stops working when it expires, when it is revoked, or when its user is deactivated.

Attribution ​

Factorial records every change made with an API key as the key's user acting through that key. A redline opened with Jane's key shows Jane as its author, with a key beside her name; the tooltip names the key, such as "via API key ‘CI’". An organization API key has no user, so its changes show the key's description instead. See Who did it.

Bulk Endpoints ​

Nearly all of Factorial API endpoints are bulk-friendly. For example, the API does not provide an endpoint that creates one procedure. It provides endpoints that create, update, or delete multiple procedures in one request.

Querying Records ​

Query endpoints return records that match a set of filters. Send a POST request with a JSON query body to the resource's query path, such as /api/v1/tasks/query. An empty body returns the first page of all records.

The following resources support queries:

  • POST /api/v1/tasks/query returns tasks from both projects and work orders.
  • POST /api/v1/queues/query returns queues.
  • POST /api/v1/tags/query returns tags.

Query Body ​

A query body may contain the following fields:

  • filters: a list of conditions. Each filter names a field f, an operator o, and a value v. A filter may also follow relationships with r, such as ["TAGS"] to filter tasks by tag name, ["QUEUE"] to filter tasks by queue name, or ["WORK_ORDER"] to filter tasks by their work order's status. A field that holds another record's id, such as a task's queue_id, isn't a filter field. Follow the relationship and filter on its id instead. To find records with no related record, see records without a related record.
  • matchesAny: when true, a record matches if any filter matches. By default, a record must match every filter.
  • search: text matched against the resource's searchable fields.
  • sortBy: a sort option, such as least_recent or most_recent.
  • pagination: next sets the page size, and cursor continues from a previous page. A page contains at most 300 records.

Operators are EQ, NEQ, IN, NOTIN, LIKE, ILIKE, NOTLIKE, NOTILIKE, GT, GTE, LT, LTE, ISNULL, and ISNOTNULL.

A query may contain at most 16 filters. A filter may follow at most 4 relationships, and IN and NOTIN accept at most 1000 values. Factorial rejects a query that exceeds a limit instead of running it.

Filtering by who made or finished a record ​

Work orders, tasks and procedures have relationships to the actor that made them and that moved them into a final state:

  • CREATED_BY on work orders, tasks and procedures.
  • COMPLETED_BY and CANCELED_BY on work orders and tasks.
  • RELEASED_BY and ARCHIVED_BY on procedures.

Procedures also have SUBMITTED_BY, the actor that sent the revision for its current or most recent review.

Two relationships reach users directly instead of actors. OWNER on work orders and procedures is the procedure's owner; a work order has no owner of its own. REQUESTED_REVIEWERS on procedures is everyone asked to review the revision, whether or not they have answered.

An actor has the fields id, client (UI, API, CLI, SYSTEM or LEGACY), executor (APP, or the plugin component that ran: HOOK, ACTION, RECEIVER or JOB), user_api_key_id and org_api_key_id. API keys have no relationship to follow, so the key ids are fields. It has a USER relationship to the person behind it, a PLUGIN relationship to the plugin it ran as, and a PARENT relationship to the actor that triggered it. Following CREATED_BY and then USER finds what a person made in the app, with their API keys, or through plugins acting for them:

json
{ "filters": [{ "r": ["CREATED_BY", "USER"], "f": "id", "o": "EQ", "v": "<user-id>" }] }

COMPLETED_BY and CANCELED_BY only match records currently in that state. A task that was completed and then reopened no longer has a completer; its history keeps the earlier change.

Reading who made or finished a record ​

Query responses for work orders, tasks and procedures include the same actors as objects. Work orders and tasks have created_by, completed_by and canceled_by. Procedures have created_by, released_by and archived_by. created_by is always set. The others are null unless the record is in that state, except that released_by stays set after a released revision is archived.

Each actor object has the following fields:

  • id: the actor's id.
  • label: who is accountable, as Factorial shows it: the user's name, an organization API key's description, or a plugin's namespace. Records made before Factorial recorded actors show Unknown (pre-audit).
  • user_id: the person behind the action, or null when there is none, such as for an organization API key or a scheduled plugin job.
  • client and executor: how the action reached Factorial, with the same values as the filter fields.
  • user_api_key_id, org_api_key_id and plugin_id: the key or plugin used, or null.
json
{
  "id": "0199b0c2-…",
  "label": "Jane Doe",
  "user_id": "0199a1f4-…",
  "client": "API",
  "executor": "APP",
  "user_api_key_id": "0199a7d3-…",
  "org_api_key_id": null,
  "plugin_id": null
}

Set not to true to exclude records that match a filter. A negated relationship filter excludes a record when any related record matches. For example, the following query returns tasks in a queue that are still to do and have neither an on-hold tag nor an awaiting-parts tag. Untagged tasks are included.

json
{
  "filters": [
    { "r": ["QUEUE"], "f": "id", "o": "EQ", "v": "<queue-id>" },
    { "f": "status", "o": "EQ", "v": "TODO" },
    { "r": ["TAGS"], "f": "name", "o": "IN", "v": ["on-hold", "awaiting-parts"], "not": true }
  ],
  "sortBy": "least_recent",
  "pagination": { "next": 50 }
}

The BLOCKED_BY relationship reaches the tasks a task depends on through its project's or work order's dependencies. A task can't start until every task blocking it is completed, so negating a filter for blockers that aren't completed finds the tasks that are ready to start:

json
{ "r": ["BLOCKED_BY"], "f": "status", "o": "NEQ", "v": "COMPLETED", "not": true }

Only tasks are reached this way. A task blocked by a nested work order or linked project still can't start, but this filter doesn't exclude it.

A relationship filter only matches records that have a related record, so ISNULL on a relationship's id can never match and the API rejects it. To find records with no related record, negate an ISNOTNULL filter on the relationship's id instead. For example, the following filter returns tasks that aren't in a queue:

json
{ "r": ["QUEUE"], "f": "id", "o": "ISNOTNULL", "not": true }

Query Response ​

Every query endpoint returns a page of records:

json
{
  "items": [],
  "next_cursor": "",
  "has_next_page": false,
  "previous_cursor": "",
  "has_previous_page": false
}

When has_next_page is true, send the same query again with pagination.cursor set to next_cursor. The sortBy value must not change between pages.

Each task includes its id, kind (PROJECT or WORK_ORDER), status, title, description, queue_id, assignee_id, work_order_id, effective_step_id, tags, created_by, completed_by, and canceled_by. The last three are actors. effective_step_id is the step the task runs: its procedure's step, or the step of the last redline applied to it. GET /api/v1/tasks/{id} returns the same fields for one task, along with its comments, a markdown field, and a files field.

markdown is the whole task as a Markdown document: the title, the description, and the content of each block on the task. The description field holds only the task's description, so use markdown to read everything someone wrote.

files lists the uploaded files the task's content shows, in the order they appear. Each entry has an id, name, content_type, size_bytes, url, and source. source says where the file appears: document for an image in a text block, gallery for a file in a file gallery, inline_pdf for an inline PDF, model_3d for the GLB a 3D model block displays, and model_3d_source for the file that GLB was converted from, such as an uploaded STEP file. The markdown field links each of these files by its /files/{id}/content path, so a client that downloads them can point those links at its copies.

Downloading Files ​

GET /api/v1/files/{id}/content returns a file's contents, with the file's content type and name in the response headers. The url of each entry in a task's files points to this endpoint. Unlike most endpoints, it is not a bulk endpoint: each request returns one file, so a client fetches several files with one request per file.

Responses include an ETag header. A client that already has a file can send it back in If-None-Match and receive 304 Not Modified instead of the contents.

Uploading Files ​

Upload endpoints are not bulk endpoints: each request uploads one file. Send the file as multipart/form-data in a part named file, whose file name becomes the file's name. Uploads do not support dry runs.

POST /api/v1/file-galleries/{id}/upload adds a file to a file gallery, and POST /api/v1/inline-pdfs/{id}/upload adds the PDF to an inline PDF block. Each file may be at most 95 MB. A gallery holds at most 30 files, and an inline PDF block holds one PDF. To upload to a redline's proposed step, use the same paths under /api/v1/redlines/.

POST /api/v1/model-3d/{id}/files adds a GLB to the shared 3D model library and attaches it to the 3D model block, which must not show a model yet. The GLB must fit the tablet budget and be at most 75 MB. A model with too many parts for Factorial to inspect, roughly 40,000, is refused; merge or simplify the assembly. If the library already has a model with the same bytes, Factorial attaches that model and returns it with reused: true instead of storing a copy. An optional part_id query parameter links the model to that part when the key may edit parts.

POST /api/v1/model-3d-files/{id}/source keeps the STEP or GLB file a library model was converted from, its original, so upload the model first. An original may be at most 95 MB. A model's original is set once, by the user who uploaded the model. A model that already has one returns 400.

POST /api/v1/model-3d-files/{id}/snapshot keeps a picture of a library model, a PNG, WebP or JPEG of at most 2 MB, which every block that shows the model uses on its card. It is set once: any user can add the picture of a model without one, and the first picture stays.

To set a block's step view, PATCH /api/v1/model-3d/view with [{"id": "<block id>", "view": {…}}], the view as the viewer saves it; "view": null removes it. A view replaces the block's last one. The block must show a model and be editable. For a redline's proposed step, use /api/v1/redlines/model-3d/view.

To show a model the library already has, PATCH /api/v1/model-3d/attach with [{"id": "<block id>", "model_file_id": "<model id>"}] attaches it to a block that has no model yet. A block keeps its model.

A redline can upload a model to its own block, but cannot link a part, since that changes the parts library.

POST /api/v1/parts/{id}/models-3d/files adds a GLB to the library, or takes the library's model with the same bytes, and links it to the part, for a key that may edit parts. The limits are the same as for a block.

POST /api/v1/parts/models-3d with [{"part_id": "<part id>", "model_file_id": "<model id>"}] links library models to parts, and DELETE /api/v1/parts/models-3d with [{"id": "<link id>"}] unlinks them. Unlinking leaves the model in the library and in the blocks that show it. Both need permission to edit parts.

Factorial limits how many uploads run at once, counting uploads over 8 MB, or sent without a Content-Length, apart from smaller ones, so large uploads never hold up small ones. A user may have two large and four small uploads in progress. Another upload returns 429 Too Many Requests. When the server is busy with other uploads, a request waits up to a minute and then returns 503 Service Unavailable. Both responses include a Retry-After header, and nothing is saved, so the client can retry the same request. An upload that sends less than about 64 KB per second over a minute is stopped and returns 400.

Updating Tasks ​

Tasks belong to either a project or a work order. The endpoints below accept both kinds of task in the same request. The API key must have permission to update project tasks, work order tasks, or both, depending on the tasks in the request.

PATCH /api/v1/all-tasks/update-queue moves tasks into a queue. Set queue_id to null to remove the tasks from their queue.

json
{ "ids": ["<task-id>"], "queue_id": null }

PATCH /api/v1/tasks/status sets the status of tasks. Status values are TODO, IN_PROGRESS, COMPLETED, and CANCELED.

json
{ "task_ids": ["<task-id>"], "status": "IN_PROGRESS" }

Both endpoints validate every task before making changes. If any task does not exist, the request fails and no tasks change; the status endpoint also rejects a request that lists the same task more than once. Updating a task the key may not change returns 403, and the message names the permission involved, such as Projects for project tasks or Work Orders for work order tasks.

Current User ​

GET /api/v1/me returns the user the request acts as: their id, name, email, active, and preferred_tz. A user API key returns its user; a custom Page returns whoever is viewing it. An organization API key has no user and gets a validation error.

json
{ "id": "<user-id>", "name": "Maya Okafor", "email": "[email protected]", "active": true, "preferred_tz": "America/Los_Angeles" }

Use the id to filter records about that person, such as tasks with {"r": ["ASSIGNEE"], "f": "id", "o": "EQ", "v": "<user-id>"}.

Notifications ​

Notifications belong to a person, so these endpoints need a user API key. An org key gets a validation error.

POST /api/v1/notifications/query lists the key user's notifications, newest first with sortBy most_recent. It takes the same query body as other queries, with filters on kind, is_read, read_at, subject_type, subject_label, and created_at. Boolean values are strings. For example, unread mentions:

json
{
  "filters": [
    { "f": "kind", "o": "EQ", "v": "comment_mention" },
    { "f": "is_read", "o": "EQ", "v": "false" }
  ]
}

Each notification has its kind, a subject with the type, id, and label of the record it is about (for a mention on a task, the task), the actor who caused it, read_at, and created_at. A comment_mention notification also has the comment_id of the comment that mentioned the user.

PATCH /api/v1/notifications/mark-read marks notifications read. Ids that belong to someone else or are already read are skipped, and rows_changed counts the rest.

json
[{ "id": "<notification-id>" }]

API Reference ​

The complete generated API reference is available at /api on your Factorial instance, under API → API Docs. Download the OpenAPI document at /api/openapi.json, or from Pages → All pages → Download API schema.