Skip to content

REST API

Base address: https://work.nod3.se/api/v1. JSON in and out.

AI assistants (Claude, ChatGPT) use the MCP server at /api/mcp with OAuth instead of API keys: see MCP.md.

  • Created under Settings → Integrations → REST API by people with the integrations permission. Shown once; only a SHA-256 hash is stored (api_tokens). Keys start with w3_.
  • Sent as Authorization: Bearer w3_…. The session cookie is never accepted here, so /api/v1/* is exempt from the same-origin check.
  • A key belongs to the workspace, not a person. It acts through its own service user (“API · ”) with a role picked when it is made (an admin- or member-based role, never more than its maker may hand out). RLS and permissions apply exactly as for a member with that role, and what it does shows under its name in comments, activity and the audit log. It is not a member: it never shows in member lists, pickers or seat counts. Migration 0034_api_key_roles.sql; the role functions fall back to app.current_api_token().
  • Optional limits: only some projects (every endpoint then sees only those) and an end date (30, 90 or 365 days, default 90, or never).
  • read keys may only GET; anything else answers 403 read_only_key.
  • A key stops working when revoked, when it expires, or when the workspace is suspended or deleted; it keeps working when people leave. Revoked keys stay in the table so their name still shows on old work.
  • Personal keys (api_tokens.personal, 0061, docs/PERMISSIONS.md): made by a person under My integrations when the workspace’s api_keys rule allows them (off by default). They act as that person (their user, role and RLS), always end (30/90/365 days), at most 10 per person, and stop working when the rule turns off or the person leaves. GET /me shows personal: true.
  • 120 requests per minute per key (429 rate_limited, Retry-After: 60). Creating and revoking keys is in the audit log.
  • JSON bodies up to 1 MB, uploads up to the file size limit; larger requests get 413 too_large (or file_too_large) before anything is read further.
Method and path What it does
GET /me The workspace, and the key: name, whether it is personal, role, permissions and scope
GET /workflow Statuses (with category), types, priorities and labels, with ids
GET /members People in the workspace: id, name, email (only the part before @ without the members permission), role
GET /projects All projects the key can see, each with its labels (tag names)
POST /projects Create: name, optional key, description, leadId (none if left out), client, valueAmount, valueCurrency, contactName, contactEmail, startDate, dueDate, template, labels (names). Not for project-limited keys (403 project_limited_key)
GET /projects/{key} One project
PATCH /projects/{key} Change any create field except key and template, plus archived; labels replaces the tags
POST /projects/{key}/messages Post in the project’s chat: body. The key need not be a member and can never read the chat
POST /projects/{key}/files Upload a file to the project: multipart/form-data with a file field. Same types, size limit and quota as the app
GET /items Items, oldest first. Query: project (key), status, assignee (ids), updatedSince (ISO time), q (every word in the title or description, or a reference such as WEB-12), archived (false by default: live items only; true: only archived; all), limit (1–100, default 50), cursor
POST /items Create: project (key), title, optional description, typeId, statusId, priorityId, assigneeId, parentRef, dueDate (YYYY-MM-DD), labels (names)
GET /items/{ref} One item, such as WEB-12
PATCH /items/{ref} Change any of the create fields except project; force: true closes an item that still waits on others where the workspace only warns; archived: true archives the item and its sub-items, false restores what was archived with it. There is no delete: the API archives
GET /items/{ref}/comments Comments, oldest first
POST /items/{ref}/comments Add a comment: body
POST /items/{ref}/files Upload a file to the item, as for projects
GET /webhooks The webhooks this key registered
POST /webhooks Register a webhook for this key: url, events, optional description and dueSoonHour (0–23, default 8: when item.due_soon goes). Answers id and the secret (once). Needs a key whose role has integrations and no project limit
DELETE /webhooks/{id} Remove one of this key’s webhooks (204)

Lists answer { "data": [...], "nextCursor": "..." | null }; pass nextCursor as cursor for the next page. An item:

{
"id": "…", "ref": "WEB-12", "project": "WEB", "projectName": "Website", "title": "…", "description": "…",
"typeId": "…", "type": "Task", "statusId": "…", "status": "In progress", "statusCategory": "in_progress",
"priorityId": null, "priority": null, "assigneeId": "…", "assignee": "Anna Svensson", "parentRef": null, "dueDate": "2026-11-30", "labels": ["Design"], "archived": false,
"url": "https://work.nod3.se/w/nod3/items/WEB-12", "createdAt": "…", "updatedAt": "…"
}

The names (projectName, type, status, priority, assignee) are for reading and filtering; send the ids when you write. assignee is the person’s name, or the first part of their email when they have none.

A project:

{
"id": "…", "key": "WEB", "name": "Website", "description": "…", "client": "Acme AB",
"clientUrl": "https://app.hubspot.com/contacts/…/record/0-2/…", "valueAmount": 120000, "valueCurrency": "SEK",
"contactName": "Anna Svensson", "contactEmail": "anna@acme.se", "startDate": "2026-11-01", "dueDate": "2026-12-15",
"leadId": "…", "lead": "Anna Svensson", "archived": false, "createdAt": "…"
}

clientUrl, the value, contact and start date come from the deal when a CRM rule made the project, or are set in Edit project; each may be null.

Writes go through the same functions as the app, so events, automation rules, notifications and the audit log behave the same.

{ "error": "<code>" } with the HTTP status: 401 unauthorized, 403 read_only_key | forbidden, 404 not_found | item_not_found | project_not_found, 400 invalid_input (with issues), invalid_json, invalid_cursor, 409 for workflow conflicts (for example item_blocked), 413 too_large, 429 rate_limited.

402 when the workspace’s plan does not allow it: plan_limit_<key> (for example plan_limit_projects, plan_limit_members, plan_limit_storage_gb) or plan_feature_<feature> (for example plan_feature_api). Nothing was changed; an owner or admin can change plan under Settings → Plan and billing. See BILLING.md.

Settings → Integrations → Webhooks.

  • Every body has actorName next to actorId (who did it; an API key shows as “API · ”).
  • In item.updated, changes to statusId, typeId, priorityId, assigneeId and parentId (its reference) also carry fromName and toName.
  • project.updated carries changes with only the fields whose stored value changed, as their new values (labels as names, archived as true/false); the edit form sends every field, so updateProject compares before and after, and sends no event when nothing changed. previous has the same fields with their old values; a changed leadUserId also gives lead (the name) on both sides.
  • item.updated and project.updated also carry diff: the same changes per field under plain names (status, type, priority, assignee, parent, lead, title, …), each as { from, into } where a value with an ID is { id, name } and a side without a value is left out. Make shows it as Changes.
  • Changing an item’s tags is an item.updated with changes.labels: { from, to } (tag names). Tags given when an item is created are part of item.created.
  • Archiving or restoring an item is an item.updated with changes.archived: { from, to }, one per item including each sub-item archived with it. item.due_soon skips archived items.
  • An endpoint is a public https:// address plus the events it wants: item.created, item.updated, item.deleted, comment.created, project.created, project.updated, project.deleted, and item.due_soon.
  • item.due_soon comes once per item and due date, the day before an open item is due in the workspace’s time zone (app.queue_due_soon_events, run with the due reminders, 0047). It is only made when some endpoint asks for it, or an active workflow listens for it (0086). Each endpoint gets it at its due_soon_hour (default 08:00, migration 0072) in the workspace’s time zone, or at once if that hour has passed; if the item is done, archived or moved by then, the delivery is dropped unsent (deliverWebhooks).
  • Automation apps (n8n, Make, Zapier) register their own endpoints through POST /webhooks when a flow is turned on. Those belong to the key (webhook_endpoints.api_token_id): only that key lists or deletes them, they show under Webhooks in the app too, and they stop sending when the key is revoked or expires (revoking also deletes them). Apps: integrations/ in the repo, see AUTOMATION.md.
  • The worker turns each outbox event into one delivery per subscribed endpoint (webhook_deliveries), with the body built once, so retries send the same bytes. Then it POSTs JSON:
{ "id": "evt_123", "type": "item.updated", "createdAt": "…", "workspace": { "id": "…", "slug": "nod3" },
"actorId": "…", "data": { "item": { …same shape as the API… }, "changes": { "statusId": { "from": "…", "to": "…", "fromName": "To-do", "toName": "Done" } } } }
  • Headers: WORK3-Event, WORK3-Delivery (id, for de-duplication), WORK3-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Receivers should compare in constant time and reject a t older than 5 minutes.
  • The secret (whsec_…) is shown once, stored encrypted with ENCRYPTION_KEY, and can be rotated.
  • A 2xx within 10 seconds counts as delivered. Otherwise it is tried again after 1 min, 5 min, 30 min, 2 h and 6 h, then marked failed; admins can send a failed one again. Deliveries are kept 30 days.
  • Safety: https only, no redirects, and private, loopback, link-local and other internal ranges are refused at connect time after DNS (so a hostname cannot point inward), and when the address is saved.
  • “Send test” queues a webhook.test event for one endpoint.
  • Loops (a webhook into n8n that calls the API, which fires the webhook again) are the receiver’s to avoid: check actorId against the key’s service user.

WORK³ · powered by NOD³