Skip to content

POST /accounts:invite

Invite a person into this workspace

Creates an account in INVITED status. Permissions can be granted to it immediately; it cannot act until the invitation is accepted.

Where this installation sends mail (features.mail in /meta/capabilities), the invitation mail is queued as a job - this call does not wait for it - and the answer carries no link.

Where it sends no mail, nothing is queued: the answer carries invitation_link, the same link the mail would have carried, shown here and never again, to be handed over by any other way. Only its hash is kept. The answer is marked Cache-Control: no-store, and a repeat with the same Idempotency-Key answers the status without the body. A lost link is replaced with POST /accounts/{accountId}:renew-invitation.

An address that already has an account here is 409 accounts.email_taken; one whose invitation has not been accepted yet is 409 accounts.already_invited. Nothing is written or sent for either. Without mail, an act an automation rule runs is refused with accounts.invitation_needs_a_person: a run reads no answer, so the link would reach nobody.

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Request body application/json — AccountInvite

NameTypeRequiredDescription
emailstring (email)
  • maxLength: 254
required
display_namestring, or null
  • maxLength: 200
optional

Responses

Example bash
curl -X POST "$HUBTASK_URL/accounts:invite" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "email": "anna@example.org",  "display_name": "string"}'

POST /accounts/{accountId}:renew-invitation

Give an invitation not yet accepted a new link

For an invitation whose link was lost, or ran out. The account must still be INVITED; any other is 409 accounts.not_invited.

Where this installation sends mail, the invitation mail is queued again and the answer carries no link; the earlier link stops working when the new mail goes out. Where it sends no mail, the answer carries a new invitation_link, shown here and never again, and the earlier link stops working at once. Without mail, an act an automation rule runs is refused with accounts.invitation_needs_a_person.

Asks MANAGE_MEMBERS at the workspace, as the invitation does. Auditable.

Path parameters

NameTypeRequiredDescription
accountIdstring (uuid) requiredAn account identifier, always a UUID.

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Responses

Codes this operation names

Example bash
curl -X POST "$HUBTASK_URL/accounts/{accountId}:renew-invitation" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)"

POST /accounts:create-managed

Give somebody an account without a mail address

Creates a managed account: a person who signs in with a sign-in name instead of an address, and the role they hold at one scope, in one step. The account is active at once - there is no invitation to redeem, because there is no mailbox to send one to.

The start password is drawn here and answered once, as start_password. Nobody chooses it and nothing can read it again: only its hash is kept. The first sign-in with it leads into choosing the person's own password under the workspace's rule.

Asks MANAGE_MEMBERS at the workspace and at the scope of the role, and refuses with accounts.managed_accounts_off where the workspace's sign-in rule does not allow managed accounts (sign_in_policy.managed_accounts). A sign-in name already taken here, in any spelling that folds to the same name, is 409 accounts.sign_in_name_taken. An act an automation rule runs is refused with accounts.managed_needs_a_person: a run reads no answer, so the start password would reach nobody. Granting OWNER demands the step-up, as the grant does.

There is deliberately no Idempotency-Key, and the answer carries Cache-Control: no-store: an answer kept for a replay would keep the start password with it. A repeat after a lost answer finds the name taken.

Headers

NameTypeRequiredDescription
X-Hubtask-Step-Upstring optionalThe proof a privileged operation demanded: the token POST /auth/step-up answered, consumed by this one call.

Request body application/json — ManagedAccountCreate

NameTypeRequiredDescription
display_namestring
  • minLength: 1
  • maxLength: 200
required
sign_in_namestring required3 to 32 characters after folding (compatibility form, lower case, trimmed); no @, no whitespace and no control character.
roleMembershipRole
  • OWNER
  • ADMIN
  • MEMBER
  • CONTRIBUTOR
  • VIEWER
  • GUEST
  • AUDITOR
requiredAUDITOR is the one that is not a rung on the same ladder: it reads the audit trail and the configuration and no content at all.
scope_typeMembershipScope
  • TENANT
  • HUB
  • COLLECTION
  • ITEM
required
scope_idstring (uuid), or null optionalAbsent for TENANT; the hub, collection or entry otherwise.

Responses

Example bash
curl -X POST "$HUBTASK_URL/accounts:create-managed" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/json" \  -d '{  "display_name": "string",  "sign_in_name": "string",  "role": "OWNER",  "scope_type": "TENANT",  "scope_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}'

GET /accounts/me

The account the caller is signed in as

Who am I, and how does this product speak to me. A client needs the answer before it can render anything: locale, time_zone and week_start are the second link of the resolution chain request → account → tenant → installation, and there is no other way to learn them — /accounts/{accountId}/preferences writes to an id the client never receives.

A service account gets the same document rather than a refusal. It has an Account row with kind: SERVICE_ACCOUNT, and answering "you are a machine, and here is which one" is more useful to a caller than a 403 it has to special-case.

me is a reserved segment: no account identifier is ever the string me, because identifiers are UUIDs. The two paths cannot collide, and this is written down because a reader looking at /accounts/{accountId} beside it will otherwise wonder.

Responses

Example bash
curl -X GET "$HUBTASK_URL/accounts/me" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

GET /accounts/{accountId}

The name behind an account identifier

Every record that says who — an ActivityEntry.actor, an assignee, a member row — carries an identifier and no label, and says why: "the account is one request away and this record is deleted with its entry, so there is nothing for a copy of somebody's name to outlive". This is that request.

What comes back is deliberately less than `/accounts/me`. The display name and the kind, and not the email, the locale, the time zone or the first day of the week. Those are the caller's own business or an administrator's; a name is what a workspace has to show to be readable at all. What the other members of a workspace see of a profile is kept minimal - the display name and the avatar - and this is that minimum.

Any member of the tenant may make it. There is nothing narrower to authorise against: the identifiers a client holds arrived in records it was already allowed to read, and row level security bounds the answer to the caller's own tenant, so an identifier from elsewhere does not resolve here rather than being refused here.

An erased account answers with the marker its erasure wrote where the name was, because that is what the row holds — the reading is not special-cased, and there is nothing left to leak.

Path parameters

NameTypeRequiredDescription
accountIdstring (uuid) requiredAn account identifier, always a UUID.

Responses

Example bash
curl -X GET "$HUBTASK_URL/accounts/{accountId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

POST /accounts/{accountId}:issue-start-password

Give a managed account a new start password

For a person without a mail address who forgot their password: there is no mailbox a reset could reach, so an owner or administrator helps them back in. The one exception to "an administrator does not recover an account", and it exists only for managed accounts - an account with an address is refused with 409 accounts.not_managed, and recovers through its mailbox.

Hubtask draws a new start password and answers it once, as at creation; nobody chooses it and nothing can read it again. Every session of the account ends and every unfinished sign-in step is spent. The next sign-in leads into choosing a new password, after the second factor where the account has one - the factor and the recovery codes stay. Personal access tokens stay, as a password change leaves them.

Asks MANAGE_MEMBERS at the workspace and a fresh proof (auth.step_up_required otherwise). An act an automation rule runs is refused with accounts.managed_needs_a_person: a run reads no answer.

There is deliberately no Idempotency-Key, and the answer carries Cache-Control: no-store: an answer kept for a replay would keep the start password with it.

Path parameters

NameTypeRequiredDescription
accountIdstring (uuid) requiredAn account identifier, always a UUID.

Headers

NameTypeRequiredDescription
X-Hubtask-Step-Upstring optionalThe proof a privileged operation demanded: the token POST /auth/step-up answered, consumed by this one call.

Responses

Example bash
curl -X POST "$HUBTASK_URL/accounts/{accountId}:issue-start-password" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

POST /accounts/{accountId}:add-address

Give a managed account a mail address

Sends a confirmation link to the address. Nothing on the account changes until somebody holding that mailbox follows the link (POST /auth/address:confirm); then the account is an ordinary one - reset by mail, mailed notices - and keeps its sign-in name as a second identifier.

The person themselves may ask, and so may whoever holds MANAGE_MEMBERS at the workspace. Only a managed account - a sign-in name and no address - takes one: any other is refused with 409 accounts.not_managed. Where the workspace's sign-in methods leave the password out, an address would take the person's one way in, and the request is refused with 409 accounts.address_needs_password. A new request replaces the earlier link; one within ten minutes of the last is 429 accounts.address_requested_recently with retry_after_seconds.

Whether the address already belongs to somebody here is not said: the confirmation answers that, to whoever holds the mailbox.

Path parameters

NameTypeRequiredDescription
accountIdstring (uuid) requiredAn account identifier, always a UUID.

Request body application/json — AddressRequest

NameTypeRequiredDescription
emailstring (email)
  • maxLength: 254
required

Responses

Example bash
curl -X POST "$HUBTASK_URL/accounts/{accountId}:add-address" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/json" \  -d '{  "email": "anna@example.org"}'

PATCH /accounts/{accountId}/preferences

Set how the product speaks to an account

Locale, time zone and the first day of the week. An empty value clears the preference, which means the workspace default applies again — the second link of the resolution chain request → account → tenant → installation.

Path parameters

NameTypeRequiredDescription
accountIdstring (uuid) requiredAn account identifier, always a UUID.

Request body application/merge-patch+json — AccountPreferences

NameTypeRequiredDescription
localestring, or null optional
time_zonestring, or null optional
week_startstring, or null
  • MONDAY
  • SUNDAY
  • SATURDAY
  • null
optional
celebrationsboolean, or null optionalWhether the moments are marked.
onboarding_completed_atstring (date-time), or null optionalWhen the tour ended or was skipped, written by the client.

Responses

Example bash
curl -X PATCH "$HUBTASK_URL/accounts/{accountId}/preferences" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/merge-patch+json" \  -d '{  "locale": "string",  "time_zone": "string",  "week_start": "MONDAY",  "celebrations": true,  "onboarding_completed_at": "2026-09-16T10:00:00Z"}'

GET /accounts/{accountId}/notification-preferences

What an account wants to be told about

One row per category and channel this installation knows, with the effective value — a pair nobody has written is answered with the default and is_default: true, because "not stored" and "off" are different facts. The categories are notification_categories in /meta/capabilities; a client renders the form from that list rather than compiling it in.

The caller's own are read with the token scope alone; somebody else's asks for the member management permission, the reason PATCH /accounts/{accountId}/preferences gives.

Path parameters

NameTypeRequiredDescription
accountIdstring (uuid) requiredAn account identifier, always a UUID.

Responses

Example bash
curl -X GET "$HUBTASK_URL/accounts/{accountId}/notification-preferences" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

PUT /accounts/{accountId}/notification-preferences/{category}/{channel}

Say what to be told about, and whether the title travels

Writes one pair whole: enabled and include_title both, because a row is a statement about a category rather than two switches that could drift. Switching a category off makes the next notification of that category SUPPRESSED, with the record saying why; the invitation is the one category no preference can switch off, and a write against it is stored and never consulted. include_title: false produces an email that says something concerns you without saying what. An unknown category or channel is refused by name.

Path parameters

NameTypeRequiredDescription
accountIdstring (uuid) requiredAn account identifier, always a UUID.
categorystring required
channelstring required

Request body application/json — NotificationPreferenceUpdate

NameTypeRequiredDescription
enabledboolean required
include_titleboolean required

Responses

Example bash
curl -X PUT "$HUBTASK_URL/accounts/{accountId}/notification-preferences/{category}/{channel}" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/json" \  -d '{  "enabled": true,  "include_title": true}'

GET /accounts/me/emergency-notices

The emergency accesses the caller was told of in the last 30 days

Each time the workspace's owner opened a private hub the caller is a member of, within the last 30 days, whatever the mail about it did - so that a person without a mailbox, or one whose mail did not arrive, is told in the app. Newest first. Not a list of notifications: it answers this one kind of notice and nothing else.

Responses

  • 200 The notices
  • 4XX An error per RFC 9457 Problem
Example bash
curl -X GET "$HUBTASK_URL/accounts/me/emergency-notices" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

GET /memberships

Who holds a role at a scope

The memberships granted at the scope named — an account or a group, a role, an identifier — and none granted elsewhere. A membership applies downwards, so what is in force at a collection is this list plus what its hub and the workspace grant; the client composes that path itself, because it knows the path it is on. An effective listing that walked it server-side is deliberately not offered until a second caller wants one.

A row carries an identifier and no label, for the reason every record that says who does: the name is one request away (GET /accounts/{accountId}, GET /groups/{groupId}).

Who may read is READ at the scope: a member of a hub seeing who else is in it is what makes a shared workspace legible, and what a name reveals is already kept minimal. A scope the caller holds nothing on is not found, in the words a missing one produces. The workspace itself (scope_type: TENANT) is the one scope whose existence is no secret, so a refusal there is a refusal.

Query parameters

NameTypeRequiredDescription
scope_typeMembershipScope
  • TENANT
  • HUB
  • COLLECTION
  • ITEM
required
scope_idstring (uuid) optionalThe hub, collection or entry.
cursorstring optional
sizeinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional

Responses

Example bash
curl -X GET "$HUBTASK_URL/memberships" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

POST /memberships

Grant a role at a scope

To an account or to a group, never both. The role applies downwards from the scope: a role at a hub applies to its collections and their items. A subject holds a role at a scope once: granting it again changes nothing and answers the membership already there.

Nobody grants above their own role: the caller needs MANAGE_MEMBERS at the scope and a role along its path at least as strong as the one granted - an owner gives any role, an administrator any but OWNER. A role above is refused with access.role_above_own (403), before any step-up is asked. GET /memberships answers the roles the caller may grant at a scope as grantable_roles.

Granting OWNER is a privileged action: it demands a fresh step-up, presented in the X-Hubtask-Step-Up header, and refuses with auth.step_up_required without one.

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.
X-Hubtask-Step-Upstring optionalThe proof a privileged operation demanded: the token POST /auth/step-up answered, consumed by this one call.

Request body application/json — MembershipGrant

NameTypeRequiredDescription
account_idstring (uuid), or null optional
group_idstring (uuid), or null optional
scope_typeMembershipScope
  • TENANT
  • HUB
  • COLLECTION
  • ITEM
required
scope_idstring (uuid), or null optional
roleMembershipRole
  • OWNER
  • ADMIN
  • MEMBER
  • CONTRIBUTOR
  • VIEWER
  • GUEST
  • AUDITOR
requiredAUDITOR is the one that is not a rung on the same ladder: it reads the audit trail and the configuration and no content at all.

Responses

Example bash
curl -X POST "$HUBTASK_URL/memberships" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "account_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "group_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "scope_type": "TENANT",  "scope_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "role": "OWNER"}'

DELETE /memberships/{membershipId}

Take a role away

Takes effect on the next request. What the account holds through a group is unaffected — that is the group's membership, revoked separately.

Revoking an OWNER membership is changing the OWNER role, and demands a fresh step-up in the X-Hubtask-Step-Up header. Revoking the last way a person who can act holds OWNER on the workspace is refused with 409 memberships.last_owner: the workspace keeps an owner.

Path parameters

NameTypeRequiredDescription
membershipIdstring (uuid) required

Headers

NameTypeRequiredDescription
X-Hubtask-Step-Upstring optionalThe proof a privileged operation demanded: the token POST /auth/step-up answered, consumed by this one call.

Responses

  • 204 Revoked
  • 4XX An error per RFC 9457 Problem
Example bash
curl -X DELETE "$HUBTASK_URL/memberships/{membershipId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

GET /groups

The workspace's groups

Every group of the workspace, by name. Any member may read it, for the reason any member may resolve an account identifier to a name: a membership granted to a group is unreadable until the group can be shown as the people it reaches, and a member scoped to one hub holds nothing at the workspace to be judged against. What a group discloses is its name and its members' identifiers, and the identifiers resolve through the same minimal read.

Query parameters

NameTypeRequiredDescription
cursorstring optional
sizeinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional

Responses

Example bash
curl -X GET "$HUBTASK_URL/groups" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

POST /groups

Create a group

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Request body application/json — GroupCreate

NameTypeRequiredDescription
namestring
  • minLength: 1
  • maxLength: 200
required
descriptionstring, or null
  • maxLength: 2000
optional
membersarray of string (uuid) optionalAccounts to put in the group straight away.

Responses

Example bash
curl -X POST "$HUBTASK_URL/groups" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "name": "string",  "description": "string",  "members": [    "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"  ]}'

GET /groups/{groupId}

One group, with who is in it

The group and the identifiers of its members. Any member of the workspace may read it — see GET /groups. The ETag is the version a PATCH presents in If-Match.

Path parameters

NameTypeRequiredDescription
groupIdstring (uuid) required

Responses

Example bash
curl -X GET "$HUBTASK_URL/groups/{groupId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

PATCH /groups/{groupId}

Rename a group or change its description

members, when sent, is the complete membership afterwards. A changed member list of a group that holds OWNER anywhere places or takes that role, so it demands a fresh step-up in the X-Hubtask-Step-Up header, as granting or revoking OWNER does. A change that would leave the workspace without an owner is refused with 409 memberships.last_owner.

Path parameters

NameTypeRequiredDescription
groupIdstring (uuid) required

Headers

NameTypeRequiredDescription
If-Matchstring optionalThe ETag of the state last read (optimistic locking).
X-Hubtask-Step-Upstring optionalThe proof a privileged operation demanded: the token POST /auth/step-up answered, consumed by this one call.

Request body application/merge-patch+json — GroupUpdate

NameTypeRequiredDescription
namestring
  • minLength: 1
  • maxLength: 200
optional
descriptionstring, or null
  • maxLength: 2000
optional
membersarray of string (uuid) optionalThe complete membership after the change; omitted leaves it as it is.

Responses

Example bash
curl -X PATCH "$HUBTASK_URL/groups/{groupId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H 'If-Match: "<the ETag you read>"' \  -H "Content-Type: application/merge-patch+json" \  -d '{  "name": "string",  "description": "string",  "members": [    "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"  ]}'

DELETE /groups/{groupId}

Delete a group

Its memberships go with it: what the group granted, nobody holds afterwards. Members of the group keep whatever they hold in their own right.

Deleting a group that holds OWNER anywhere takes that role away, so it demands a fresh step-up in the X-Hubtask-Step-Up header. A deletion that would leave the workspace without an owner is refused with 409 memberships.last_owner.

Path parameters

NameTypeRequiredDescription
groupIdstring (uuid) required

Headers

NameTypeRequiredDescription
X-Hubtask-Step-Upstring optionalThe proof a privileged operation demanded: the token POST /auth/step-up answered, consumed by this one call.

Responses

  • 204 Deleted
  • 4XX An error per RFC 9457 Problem
Example bash
curl -X DELETE "$HUBTASK_URL/groups/{groupId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"