API reference · Hubtask API 0.1.0
identity
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
Request body application/json — AccountInvite
Responses
201Invited InvitedAccount4XXAn error per RFC 9457 Problem
Codes this operation names
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
Headers
Responses
200Renewed InvitedAccount4XXAn error per RFC 9457 Problem
Codes this operation names
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
Request body application/json — ManagedAccountCreate
Responses
201Created, with the start password shown for the only time ManagedAccountCreated4XXAn error per RFC 9457 Problem
Codes this operation names
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.
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
Responses
200The account, as another member may see it AccountSummary4XXAn error per RFC 9457 Problem
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
Headers
Responses
200The new start password, shown for the only time StartPasswordIssued4XXAn error per RFC 9457 Problem
Codes this operation names
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
Request body application/json — AddressRequest
Responses
202The confirmation is on its way AddressRequested4XXAn error per RFC 9457 Problem
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
Request body application/merge-patch+json — AccountPreferences
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
Responses
200The preferences, defaults included and marked NotificationPreferenceList4XXAn error per RFC 9457 Problem
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
Request body application/json — NotificationPreferenceUpdate
Responses
200The preference as stored NotificationPreference4XXAn error per RFC 9457 Problem
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
200The notices4XXAn error per RFC 9457 Problem
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
Responses
200The memberships at that scope, newest first MembershipPage4XXAn error per RFC 9457 Problem
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
Request body application/json — MembershipGrant
Responses
201Granted Membership4XXAn error per RFC 9457 Problem
Codes this operation names
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
Headers
Responses
204Revoked4XXAn error per RFC 9457 Problem
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
curl -X GET "$HUBTASK_URL/groups" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /groups
Create a group
Headers
Request body application/json — GroupCreate
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
Responses
200The group and its members GroupDetail4XXAn error per RFC 9457 Problem
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
Headers
Request body application/merge-patch+json — GroupUpdate
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
Headers
Responses
204Deleted4XXAn error per RFC 9457 Problem
curl -X DELETE "$HUBTASK_URL/groups/{groupId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN"