Docs / API reference / Using the HTTP API
API reference

Using the HTTP API

Updated October 2026 8 min read

Everything the web app does goes through an HTTP API that your own scripts and programs can use too, with the same permissions as the account they sign in as. This page covers the address, signing in, personal access tokens and their scopes, a few real calls, errors, and the realtime connection.

What you will need

  • A workspace you can sign in to, and its sign-in name (the part before .groupforce.org)
  • A login and password for an account in it. Everything the API lets you do is limited to what that account can do in the workspace
  • curl and jq, if you want to follow the examples as written

Base URL and shape

Each workspace answers on its own address: https://<slug>.groupforce.org/v1. Requests and responses are JSON. Almost everything is under one path shape:

https://<slug>.groupforce.org/v1/tenants/<workspace-id>/<resource>[/<id>[/...]]

<workspace-id> is the workspace's UUID, not its sign-in name. The next section shows how to get it. The first segment after it, the resource, decides which part of groupforce answers:

Area Resources
Platform links, tags, tag-colors, workspace-settings, custom-field-defs, audit, notifications, search, object-shares
Contacts contacts, orgs, contact-lists, addressbook-permissions, addressbook-settings
Calendar events, event-tags, resources, busy, calendar-permissions
Documents nodes, share-links, shared, webdav-locks, editors, print
Work items work-items, projects, project-roles, kanban-boards, statuses
Mail mail-accounts, messages, mail-signature-default
Chat and calls channels, dms, entity-channels, chat, calls
Notes notes, note-templates, notebooks
Apps apps, bundled-apps, shared-models, component-registry, app-records, extension-points
People and groups accounts, groups, pats, and /v1/me for the signed-in account
Billing billing, for administrators of the workspace

An unknown resource is a 404. This page shows the shapes that are stable and used by groupforce itself; it does not describe every resource. For one you need, look at what the web app sends and receives in your browser's network panel: it uses these same routes.

Signing in

There are two sign-ins. The one the web app uses sets a cookie and is meant for browsers. The one for programs returns a session token that you send as a bearer token. A session lasts 24 hours from sign-in and does not extend while you use it.

To sign in as a program you first need the workspace id. The browser sign-in answers with it. The second call, to the id's own sign-in route, returns the token:

# the workspace id comes back in the "tenant" part of the answer
T=$(curl -s -X POST https://acme.groupforce.org/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"workspace":"acme","login":"alice","password":"..."}' | jq -r .tenant.id)

# a session token for scripts: sent as "Authorization: Bearer ..."
SESSION=$(curl -s -X POST https://acme.groupforce.org/v1/tenants/$T/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"login":"alice","password":"..."}' | jq -r .session_token)

A wrong password, an unknown login, an inactive account and a deactivated account all give the same 401 with the message "invalid credential". Failed sign-ins are throttled: 8 failures for one login from one address, and 60 from one address, per 15 minutes, after which you get 429 with a Retry-After header. A suspended workspace lets only administrators sign in, and then only to reach billing.

POST /v1/auth/logout ends a session. Changing your password with POST /v1/me/password ends all your sessions, this one included.

Personal access tokens

A session expires in a day. For anything that runs unattended, use a personal access token (PAT): a long-lived secret that starts with gfpat_, belongs to one account, and carries the scopes you give it. It can have an expiry (the optional expires_at, an RFC 3339 time in the future; Settings → Tokens offers one year, 90 days, 30 days or never), and it acts as you and never with more rights than you have.

curl -s -X POST https://acme.groupforce.org/v1/tenants/$T/pats \
  -H "Authorization: Bearer $SESSION" \
  -H 'Content-Type: application/json' \
  -d '{"name":"nightly export","scopes":["read","write"]}'
# 201 {"id":"...","token":"gfpat_...","name":"nightly export","scopes":["read","write"]}

The token is shown once, in this answer. Store it like a password. A token is made for your own account only, and only from a signed-in session: a request made with a token cannot create another token (403). Choose at least one scope; any other name is refused. The same can be done without the API under Settings → Tokens.

Scope What the token may do
read GET, HEAD and OPTIONS on the HTTP API, and open the realtime connection.
write Every other method on the HTTP API. It does not include reading, so a token that must do both has both scopes.
webdav Mount your files over WebDAV. Nothing on the HTTP API.
carddav Sync contacts with an address-book app. Nothing on the HTTP API.
caldav Sync events with a calendar app. Nothing on the HTTP API.

A call outside the token's scopes gets 403 with "this token lacks the read scope" (or write). A token for another workspace gets 401. Revoke a token with POST /v1/tenants/<id>/pats/<token-id>/revoke, as its owner or as an administrator. There is no call that lists tokens, so keep the id you were given. Tokens also stop working when the account is deactivated or deleted, when an administrator resets its password, and for the whole workspace while it is suspended.

The file and calendar scopes are used with HTTP Basic, not bearer: the password is the token, and the user name is ignored for CardDAV and CalDAV. Use the workspace's own host name, for example https://acme.groupforce.org/carddav/, /caldav/ and /webdav/.

Making calls

Send the token on every request. This lists work items and then changes one. The list answers a JSON array. limit is 50 unless you set it, up to 500, and offset skips that many; project_id, status, assignee_id and type filter. You see only the work items you are allowed to see.

Work items can also be filtered and sorted by a custom field, on the server: cf.<key>=value keeps the items whose custom field holds that value (text by default; cf.<key>:n=5 is the number 5 and cf.<key>:b=true the boolean). Up to ten such filters may be combined and all must hold. sort=cf.<key> (or -cf.<key>, or cf.<key>:n for numbers) orders by one, and the total comes back in the X-Total-Count header. A key of the form <app id>.<name> belongs to that app: an app acting on its own may write only keys that start with its own id, and anyone can read them. People are not restricted.

PAT=gfpat_...

curl -s "https://acme.groupforce.org/v1/tenants/$T/work-items?type=task&limit=20" \
  -H "Authorization: Bearer $PAT"
# [{"id":"...","type":"task","number":12,"title":"...","status":"...",
#   "priority":"medium","version":3, ...}]

curl -s -X PATCH "https://acme.groupforce.org/v1/tenants/$T/work-items/$ID" \
  -H "Authorization: Bearer $PAT" \
  -H 'Content-Type: application/json' \
  -H 'If-Match: "3"' \
  -d '{"title":"Printer on the second floor, again"}'
# 200 the item as it is now, with version 4

A change is a PATCH with only the fields you want to change (JSON merge patch: a field you leave out stays, null clears it). If-Match: "<version>" says which version you edited. If somebody changed the item since, nothing is written and you get 412 with the item as it is now under current; read it, reapply your change and try again. Without the header, your change applies to whatever is stored. A change that alters nothing writes nothing and leaves the version as it was. The older PUT on a work item still works but is deprecated and answers a Deprecation: true header.

Pagination is not the same everywhere. Work items and the record history under audit take limit (up to 500); work items also take offset. Other resources have their own rules. Several, including work items and groups, also have a POST .../changes route that returns what changed since a cursor, which is what the web app uses to stay current. Check a resource before you rely on a paging scheme.

Two other calls you will use: GET /v1/me (the signed-in account, its groups and workspace; it works with a cookie or a token, not with a session bearer) and GET /v1/tenants/<id>/accounts (the people in the workspace).

Errors and limits

Every error is JSON with one field: {"error": "a sentence you can show"}. The status says what kind.

Status Meaning
400 The request is malformed or breaks a rule. The message names the problem.
401 No credential, a bad or revoked one, or a token for another workspace.
403 You are known but may not: another workspace's session, a token without the needed scope, a cookie request from a site that is not allowed, or a suspended workspace. A record you may not see is usually 404, so its existence is not confirmed.
404 No such resource or record, or one you cannot see.
409 A conflict, such as a name already taken, or a state that forbids the change.
412 Your If-Match version is out of date. The body has current.
429 Too many failed sign-ins. Wait for Retry-After.
502 A part of groupforce did not answer ("the <service> service is unavailable"). Try again.

Request bodies can be up to 64 MiB. There is no general rate limit on authenticated calls today; only failed sign-ins are throttled. Be considerate anyway: poll rarely and use the realtime connection below for live data. Browsers are held to the usual rules: a cookie-authenticated write must come from an allowed origin, and the API sends no CORS headers, so call it from a server or a script, not from another website's page. Bearer tokens have no such restriction.

Realtime

To hear about changes as they happen, open a WebSocket at wss://<slug>.groupforce.org/v1/tenants/<id>/ws. Authenticate with Authorization: Bearer (a session token, or a token with the read scope); a browser uses its cookie. You get a hello message, then you subscribe to topics:

-> {"op":"sub","topics":["work_item"]}
<- {"op":"subscribed"}
<- {"op":"ev", ...}        an event, for a record you may see
<- {"op":"resync"}         your access changed: read again

A topic is a kind of record, such as work_item, note, event or contact, or the address of one record. You only receive events about records you are allowed to see, and when your groups or roles change you are told to read again. If the connection closes with code 4401 your access has ended (signed out, deactivated, token revoked); 1013 means you were too slow, so reconnect and read the current state. Limits: 20 connections per account, 64 topics per connection, client messages up to 4 KiB. The server pings every 25 seconds. Where realtime is not switched on, the endpoint answers 503 and you poll.

The exact fields inside an ev message are not described here, because they are not documented as stable yet. Treat an event as a hint to read the record, and rely on the REST calls above for the data.

Was this helpful?