API reference · Hubtask API 0.1.0
auth
POST /auth/sessions
Sign in with email and password
The password sign-in: an email, a password, and the answer is an access/refresh pair plus the session row both hang off. The access token lives fifteen minutes and verifies by its signature; the refresh token lives thirty days, rotates on every use, and a rotated-out token replayed later invalidates the whole family, because a replay means two holders. Where the workspace has switched the password off (sign_in_policy.methods without PASSWORD), every password sign-in is refused with auth.password_not_offered, for every address alike and before any account is looked up; the stored password is kept, and works again when the password is switched back on. The one exception is a workspace left with no way in that works - no provider switched on there, whatever the cause - where the password opens as the fallback (password_fallback on the sign-in rules), and each sign-in through it is recorded in the workspace's trail as auth.password_fallback. The tenant is resolved before the credential is checked - from the subdomain or the X-Hubtask-Tenant header in multi mode, and in single mode there is only one - never from the request body. A contradiction between an authenticated caller and the host stays 403 tenant_mismatch. Every failure is one generic refusal, byte for byte the same for a wrong password and an address nobody holds: whether an account exists is exactly what a guessing client is trying to learn. Behind that one answer sit the progressive delay and the lockout, per account and per IP, and the auth rate-limit bucket in front of everything.
Request body application/json — SignIn
Responses
201Signed in, with the pair shown for the only time SessionTokens202The password was right and a second step is owed: a TOTP code where one is enrolled, or - under tenant enforcement, for an administrator not yet enrolled - the enrolment itself. MfaChallenge4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/sessions" \ -H "Content-Type: application/json" \ -d '{ "identifier": "string", "email": "anna@example.org", "password": "string"}' GET /auth/sessions
The caller's active sessions
One's own sessions, newest first: where each was opened (the user agent and the IP class recorded at sign-in - a hint for recognising one's own devices, never a precise address), when it was created, when it last acted, and which one is answering this very call. Never anybody else's, whatever the role: a session is the person's, and an administrator who suspects one acts by disabling the account, not by reading its sessions. Only open sessions are listed - those the next request accepts: one ended, run out, past the workspace's maximum age or idle time, or opened before it required a new password from everyone is absent.
Responses
200The sessions4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/auth/sessions" \ -H "Authorization: Bearer $HUBTASK_TOKEN" DELETE /auth/sessions
Sign out everywhere
Ends every session of the caller's account, this one included: every refresh family dies, and every access token still in flight refuses on its next request. The answer to "I left myself signed in somewhere" - and the reason the route needs nothing but the will to call it.
Responses
204Every session ended4XXAn error per RFC 9457 Problem
curl -X DELETE "$HUBTASK_URL/auth/sessions" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /auth/sessions:revoke-others
Sign out everywhere else
Ends every session of the caller's account except the one making this call: the answer to a lost phone, asked from the laptop that is still in hand. Every other refresh family dies, and every other access token still in flight refuses on its next request. Called with a personal access token there is no session of the caller's to keep, so every session ends. Journalled in the workspace's trail with the number ended.
Responses
204Every other session ended4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/sessions:revoke-others" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /auth/sessions:refresh
Exchange a refresh token for the next pair
Rotation, not renewal: the presented refresh token is retired in the same moment the new pair is minted, and presenting it again afterwards is treated as theft - two holders of one token - so the whole family is invalidated, the sign-in that opened it is over everywhere, and the reuse raises its own metric reason and an alert. The route is public the way sign-in is: the refresh token in the body is the whole credential, and demanding a bearer header beside it would demand the very thing this call exists to replace.
Request body application/json — SessionRefresh
Responses
200The next pair SessionTokens4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/sessions:refresh" \ -H "Content-Type: application/json" \ -d '{ "refresh_token": "string"}' DELETE /auth/sessions/{sessionId}
End one session
Ends one of the caller's own sessions: its refresh family dies and its access token refuses on its next request, immediately rather than at the end of the token's fifteen minutes. Somebody else's session is not found rather than forbidden - whether a session identifier exists is nobody's business but its holder's. Revoking twice is not an error.
Path parameters
Responses
204Ended4XXAn error per RFC 9457 Problem
curl -X DELETE "$HUBTASK_URL/auth/sessions/{sessionId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /auth/invitations:redeem
Redeem an invitation and set the first password
The moment an invited account becomes a person: the redemption token from the invitation mail, a password under the policy (twelve characters at least), and the account moves from INVITED to ACTIVE - signed in, with the same answer sign-in gives, because making somebody who just proved control of the mailbox type the password again teaches nothing. Where the workspace has switched the password off, no password is set: the redemption is refused with auth.password_not_offered, and the invited person accepts the invitation by signing in through the workspace's provider, which activates the account. Where no provider is switched on either, the password is open as the fallback (password_fallback on the sign-in rules): the invitation is accepted with a password, and the workspace's trail records it as auth.password_fallback. The token was shown once, in the invitation, and is stored only as a hash under its own purpose label; it dies on redemption, so a second redemption is refused however fresh the token looks. An expired or unknown token is one indistinguishable refusal - which addresses hold unredeemed invitations is not for a probe to enumerate. A redemption is a sign-in for the workspace's second-factor rule: where mfa_required_for covers the invited person, the answer is 202 with the ENROLL step rather than 201 with the pair. The invitation is accepted either way - the password is set and the token spent - and the session opens only once the factor is set up with the pending credential.
Request body application/json — InvitationRedemption
Responses
201Redeemed and signed in SessionTokens202Redeemed, and the workspace demands a second factor of this person before the first session: the step is ENROLL, and email names the invited address. MfaChallenge4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/invitations:redeem" \ -H "Content-Type: application/json" \ -d '{ "token": "string", "password": "string", "terms_url": "string", "terms_version": "string"}' POST /auth/invitations:inspect
Name the workspace and the address an invitation is for
What the invitation card says before anything is typed: the name of the workspace the person was invited to and the address the invitation went to. Answered only for a token that would redeem - an unknown, expired or accepted one is the redemption's one refusal, auth.redemption_failed - and nothing is spent: the token stays exactly as redeemable as it was. The token travels in the body, never in the address.
Request body application/json — InvitationInspection
Responses
200The invitation, named InvitationSummary4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/invitations:inspect" \ -H "Content-Type: application/json" \ -d '{ "token": "string"}' POST /auth/installation:set-up
Set up a fresh installation with the code its first start printed
The first start of an installation that has no workspace and nobody in its operator register prints a one-time setup code to the server's console, valid for thirty minutes. With it, this creates the first workspace with its defaults, its owner with the password, and the owner's entry in the operator register - in one step, recorded in the instance journal - and answers the same pair sign-in answers: the owner is signed in. In single mode the workspace's short name is not asked for and is derived from its name; in multi mode slug is required and the workspace answers at that subdomain. A wrong code is refused with setup.code_wrong, an expired one with setup.code_expired (restarting the server prints a new one). Once any workspace exists every code is spent and the call is refused with setup.already_done: the code is the only key, and after the first workspace there is nothing left for it to open. features.setup_open in /meta/capabilities says whether this is offered.
Request body application/json — InstallationSetUp
Responses
201Set up, and signed in as the workspace's owner SessionTokens4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/installation:set-up" \ -H "Content-Type: application/json" \ -d '{ "code": "string", "display_name": "string", "slug": "string", "owner_email": "string", "owner_display_name": "string", "password": "string", "default_locale": "string", "default_time_zone": "string"}' POST /auth/sessions:verify
Present the second factor and receive the pair
The second step of a two-step sign-in: the pending credential the password answered, plus a TOTP code or one recovery code. The pending credential can do nothing but be presented here - it opens no route, reads nothing, and dies on use or after its few minutes. A code verifies within one step of drift either side and never twice: the same step presented again is refused, because a code that worked twice is a code somebody shoulder-read. A recovery code works exactly once, its consumption is audited, and the answer says how many remain. Failures count against the same attempt ledger sign-in uses. Where the password the sign-in began with no longer meets the workspace's rule - or is a start password Hubtask drew - the change step still stands after the code: the answer is then 202 with PASSWORD_CHANGE, and the pair comes from /auth/sessions:set-password.
Request body application/json — SignInCompletion
Responses
201Signed in, with the pair shown for the only time SessionTokens202The code was right and the password the sign-in began with must be changed first: the PASSWORD_CHANGE step, with the rule the new one is judged against. MfaChallenge4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/sessions:verify" \ -H "Content-Type: application/json" \ -d '{ "pending_token": "string", "code": "string", "recovery_code": "string"}' POST /auth/sessions:link
Prove the account before a provider is connected to it
The LINK step. A provider vouched for an address whose account already holds a password, and it is connected to that account only once the password is proven here: the provider's word alone never opens an account that has its own credential. With no second factor on the account, the answer is the pair and the provider is connected. With one, the answer is the ordinary 202 with TOTP and RECOVERY, and the provider is connected when /auth/sessions:verify completes - never before the account's whole proof. A wrong password counts against the account like any other, and is refused as a sign-in is. The password is a proof here even where the workspace switched it off as a way in. Switching it off closes the sign-in by password, not the account's own proof that it is the person: a member who still knows the password connects the workspace's provider with it, once. Where the password is off, the other proof is the mailbox - the link Forgot your password? mails - with a fresh sign-in at the provider (connect_token at /auth/oidc:start).
Request body application/json — LinkCompletion
Responses
201Connected and signed in, with the pair shown for the only time SessionTokens202The password was right; the account's second factor is the rest of its proof MfaChallenge4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/sessions:link" \ -H "Content-Type: application/json" \ -d '{ "pending_token": "string", "password": "string"}' POST /auth/sessions:agree-terms
Agree to the terms of use and finish the sign-in
The TERMS step. Where terms of use are in force - a terms link set by the workspace or the installation - and the account has not agreed to the link and version in force, every sign-in ends here instead of in a session: after the password, the second factor, the change of an old password, a provider's return or a reset, whichever were owed. The step asks for nothing the account's proof would need; it comes after the whole proof. The request repeats the link and the version the person was shown. Where the terms changed while the step was open, the answer is 409 auth.terms_changed with the terms now in force as parameters, and the pending credential is not spent, so the card shows the new terms and the person agrees to those. Agreeing stores the link, the version and the moment on the account, records auth.terms_agreed in the workspace's trail with the version and the link - never the text, which Hubtask does not hold - and opens the session the sign-in would have opened, with the same method and provider. Declining needs no request: the pending credential can do nothing else and dies within five minutes. No session is opened.
Request body application/json — TermsAgreement
Responses
201Agreed, and signed in SessionTokens4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/sessions:agree-terms" \ -H "Content-Type: application/json" \ -d '{ "pending_token": "string", "terms_url": "string", "terms_version": "string"}' POST /auth/password
Change the password of the signed-in account
The password of the caller's own account, behind a step-up. The step-up is the proof of the old password, which is why there is no current-password field beside it: asking for both is asking twice for one thing (WCAG 2.2 SC 3.3.7). Every other session of the account is ended and this one is not - a person changing their password is saying the old one may be known, and the other sessions are what it opened. Personal access tokens keep working: they are their own credentials with their own expiry and their own list, and nobody mints one in a browser. A refused password answers 422 with one field_errors[] entry per rule it breaks, each carrying the auth.password_rule.* code the client predicted with - so one fact has one sentence whether the client saw it coming or the server sent it.
Headers
Request body application/json — PasswordChange
Responses
204Changed.4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/password" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "password": "string"}' POST /auth/password:forgot
Ask for a password reset link
Answers 202 for an address that holds an account and for one that does not, byte for byte: which addresses have accounts is exactly what a probe is after. There is no error state for "no such address", because there is no such answer. Behind that one answer sits a job on the queue the invitation already uses, so an unreachable mail server never fails the request - and never becomes the difference a probe was looking for. The link is a single-use token that lives half an hour, stored only as a hash under its own purpose label, and it arrives in the URL's fragment so that nothing between the mail client and the interface sees it. An account that signs in only through its organisation's provider gets a different mail - saying so - and the same answer.
Request body application/json — PasswordForgot
Responses
202The request arrived.4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/password:forgot" \ -H "Content-Type: application/json" \ -d '{ "email": "anna@example.org"}' POST /auth/password:reset
Spend a reset link and set the password
The token from the reset mail, and a password under this workspace's rule. The token works once and for half an hour; unknown, spent and expired are one indistinguishable refusal - which of the three applies is not for the holder of a link they found somewhere to learn. A link mailed before the workspace switched the password off is refused afterwards with auth.password_not_offered, and the mail a request sends there points to the provider. Every session of the account ends. Somebody asking for a reset is saying the old password may be known, and the sessions are what it opened. Where the account has a second factor the answer is 202 with the challenge rather than 201 with the pair: control of a mailbox is one proof, and it does not replace the one the account already demanded.
Request body application/json — PasswordReset
Responses
201The password is set and the session is open SessionTokens202The password is set and the account's second factor is still owed. MfaChallenge4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/password:reset" \ -H "Content-Type: application/json" \ -d '{ "token": "string", "password": "string"}' POST /auth/address:confirm
Confirm a mail address with the link that was sent to it
The token from the confirmation mail. Holding it proves the mailbox, and nothing else: it signs nobody in. The address becomes the account's, which is from then on an ordinary account that keeps its sign-in name. The token works once and for a day; unknown, spent, replaced and expired are one refusal, auth.address_link_invalid. An address that has meanwhile become another account's here is 409 accounts.address_taken.
Request body application/json — AddressConfirm
Responses
204The address is the account's4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/address:confirm" \ -H "Content-Type: application/json" \ -d '{ "token": "string"}' POST /auth/password:check
Ask what only the server knows about a candidate password
The four rules a client cannot answer itself: the offline lists, the breach corpus, the last few passwords of this account, and whether the candidate is the one in force. The answer is rule identifiers and never sentences - the client already holds the message code for each. It demands the same proof the setting demands. A bearer for one's own account, or the token of the flow a password is being set in: an invitation, a reset, or the pending credential of a sign-in the change step interrupted. Without one the route would be an oracle that told anybody whether a word is on a blocklist or in somebody's history. 200 with a list rather than a refusal, because it is a question and not an attempt: a client asks it while somebody is still typing, and a 422 would be an error state on a form nobody has submitted. It consumes no credential, for the same reason - a question that spent the pending token would end the sign-in it was asked during. Its rate limit is its own, beside the auth bucket, because it costs Argon2 comparisons.
Headers
Request body application/json — PasswordCheck
Responses
200The rules this candidate breaks, which may be none PasswordCheckResult4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/password:check" \ -H "Content-Type: application/json" \ -d '{ "password": "string", "pending_token": "string", "invitation_token": "string", "reset_token": "string"}' POST /auth/sessions:set-password
Set a new password and finish the sign-in
The fourth door a password is set through. The password was right and no longer meets the rule - too short for a tightened policy, older than the expiry, or older than the moment somebody asked everybody for a new one - so the sign-in continues by setting a new one. Confirming it is the sign-in, exactly as the enrolment step works. The pending credential can do this and nothing else, and it dies on use. Every session of the account ends: the rule refused the password that opened them. This is the whole of the enforcement. No job walks accounts and no job walks tenants - the rule is applied where the plaintext already is, which is the moment somebody uses it, so a workspace of ten and one of ten thousand cost the same.
Request body application/json — SignInPasswordChange
Responses
201Set, and signed in SessionTokens202Set; the terms of use in force are still to be agreed to (TERMS), and the session opens at /auth/sessions:agree-terms MfaChallenge4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/sessions:set-password" \ -H "Content-Type: application/json" \ -d '{ "pending_token": "string", "password": "string"}' POST /auth/sessions:elevate
Raise this session to the control plane for an hour
A registered operator raises their own session to admin:tenants by passing a fresh step-up. One hour, on that session and no other, gone with it, and both the act and the moment it falls back in the installation's journal. This deliberately weakens the rule that admin:tenants is never carried by a session. It weakens it to: only for a registered operator, only after a fresh proof, only for an hour, only on the session that proved it, and written down. What it buys is that nobody has to mint a long-lived all-powerful token and paste it into a browser to change a switch - which is the outcome the strict rule produces in practice, and which is worse. It does not slide: activity extends a session's own horizon and never this, and a second hour needs a second proof. A running hour is neither replaced nor extended: raising a session that is raised already is auth.session_already_elevated (409), before any proof is asked, with the running end in params.until - so a client that lost its copy of the hour learns it. The register is read again on every request, so an operator removed while a raised session is open loses the scope on their next call rather than at the end of the hour. The personal access token stays exactly as it is, for automation.
Headers
Responses
200Raised SessionElevation4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/sessions:elevate" \ -H "Authorization: Bearer $HUBTASK_TOKEN" GET /auth/sign-in-rules
What a sign-in screen may know before anybody has signed in
The four things a sign-in card needs and nothing else: which methods this workspace signs in with, which providers it offers, what a password has to meet, and the links its operator is obliged to show. Public the way sign-in is, and the workspace is resolved exactly as sign-in resolves it - from the subdomain or the X-Hubtask-Tenant header in multi mode, and in single mode there is only one. A host no workspace answers at gets the installation's own level, which is byte for byte what a workspace that has decided nothing answers: which hosts hold workspaces is exactly what a probe is after. Why a public route answers this at all: a configurable hint is otherwise wrong. A screen saying "at least twelve characters" in a workspace that demands fifteen is a screen that lies, and the password is refused after it was typed. What it deliberately does not answer is everything a guesser could use - the expiry, the history depth, the session timeouts, and the contents or sources of any list.
Responses
200The rules SignInRules4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/auth/sign-in-rules" POST /auth/mfa/totp:enroll
Begin TOTP enrolment
Mints the TOTP secret, sealed at rest through the envelope encryption every stored credential uses, and answers the provisioning URI and the ten recovery codes for the only time - the QR image is a client's job. The enrolment arms nothing yet: until a valid code confirms it, sign-in is unchanged, because an unconfirmed enrolment protects nobody and locks nobody out. Called with a bearer credential by a signed-in person, or with the pending credential a sign-in under tenant enforcement answered - the route an unenrolled administrator is routed into instead of into a session. Enrolling again before confirming replaces the unconfirmed secret; enrolling while armed is refused - disable first, with the password.
Request body application/json — TotpEnrollmentStart
Responses
201The secret, the URI and the recovery codes, shown for the only time TotpEnrollment4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/mfa/totp:enroll" \ -H "Content-Type: application/json" \ -d '{ "pending_token": "string"}' POST /auth/mfa/totp:confirm
Confirm enrolment with a first valid code
Arms the enrolment: the caller proves the authenticator holds the secret by presenting one valid code, and from this moment sign-in is two-step. Called with a bearer credential, or with the pending credential of an enforcement sign-in - in which case a successful confirmation also answers the session pair, because the person has by now proved both factors.
Request body application/json — TotpConfirmation
Responses
200Armed. TotpConfirmed4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/mfa/totp:confirm" \ -H "Content-Type: application/json" \ -d '{ "pending_token": "string", "code": "string"}' POST /auth/step-up
Prove yourself again, for the irreversible
A fresh re-authentication on the current session, with whatever the account holds: the password; the TOTP code where a factor is armed; a recovery code, consumed exactly as at sign-in; or a fresh sign-in at the provider the account is connected to. The proof is recorded on the session with its moment and its method, answered as a token that is valid for a short window and consumed by the one privileged action that demanded it - deleting a tenant, changing the OWNER role, minting a token with an admin scope, a destructive restore, turning the second factor off. A second privileged action needs a second proof. The operation that found the proof missing answered 403 with auth.step_up_required and, in params.methods, the methods this account can answer with, in this order: a space-separated list drawn from PASSWORD (the account holds one), TOTP (a factor is armed), RECOVERY (a factor is armed and a recovery code is left) and PROVIDER (an identity is connected at a provider switched on for this workspace; params.provider names it). An empty list is an account with none of them. A client builds its prompt from that list, never from a guess. Exactly one method per request. PROVIDER takes two calls: POST /auth/step-up:provider answers where to send the browser, and the state and authorization_code the provider sends it back with are presented here. The proof counts only when the provider's ID token names the identity already connected to this account (auth.step_up_provider_mismatch otherwise) and carries an auth_time inside the step-up's own window (auth.step_up_provider_not_fresh otherwise - a provider that ignores max_age proves nothing fresh). This is where it goes with the answer. The token travels back in the X-Hubtask-Step-Up header - or, for the restore, in the request's step_up_token field.
Request body application/json — StepUpRequest
Responses
201Proved, with the token shown for the only time StepUpGrant4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/step-up" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "password": "string", "code": "string", "recovery_code": "string", "state": "string", "authorization_code": "string"}' POST /auth/step-up:provider
Begin a step-up at the provider this account is connected to
The first of PROVIDER's two calls. An authorization request at the provider this account is connected to and that is switched on for its workspace - the first in the workspace's order where there are several, the one params.provider named - with prompt=login and max_age=0, so the provider asks the person again rather than answering from its own session. The flow is bound to the session that asked: its state finishes a step-up of this session at POST /auth/step-up and nothing else, and a sign-in callback refuses it. The browser comes back to this installation's own callback address, the one every registration already permits. Refused with auth.step_up_no_provider for an account with no such provider, and with auth.step_up_session_required for a caller without a session.
Responses
201Where to send the browser ProviderStepUpAuthorization4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/step-up:provider" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /auth/mfa/totp:replace
Begin replacing the authenticator
For a new phone, or an authenticator that is gone: a new secret, kept beside the armed one as a second, unconfirmed enrolment. Nothing changes yet - the armed factor and its recovery codes keep working until the new authenticator is confirmed, so there is never a moment without a factor, whatever the workspace's rule requires. Behind a step-up with whatever the account holds: replacing the factor is the same power as removing it. Offered while a factor is armed, also where the workspace requires one. Starting again replaces an unconfirmed replacement; one that is not confirmed within its window lapses. Bound to the session that began it: only that session confirms it. Refused with auth.mfa_not_enrolled where no factor is armed - there is nothing to replace, and setting one up is enrolment's.
Headers
Responses
201The new secret, for the only time it is shown AuthenticatorReplacement4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/mfa/totp:replace" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /auth/mfa/totp/replacement:confirm
Confirm the new authenticator, and swap
A code from the new authenticator proves it holds the new secret. Then, in one statement, the new secret becomes the armed one and the old stops working, and ten new recovery codes replace the old ten - answered here, for the only time they are shown. Audited as auth.mfa_replaced. Only the session that began the replacement confirms it, and only inside its window; otherwise auth.mfa_replacement_unknown. A wrong code counts against the second factor's ledger and changes nothing.
Request body application/json — AuthenticatorReplacementConfirmation
Responses
201Swapped; the new recovery codes, for the only time they are shown RecoveryCodes4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/mfa/totp/replacement:confirm" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "code": "string"}' POST /auth/mfa/recovery:regenerate
Replace the ten recovery codes
Ten new ones, behind a step-up. The old set stops working in the same moment the new one is answered - one statement, because a set answered without the old one burned would be twenty live codes, and a set burned without a new one answered would lock somebody out of their own escape hatch. Shown once, exactly as at enrolment, and stored only as hashes. The codes are the account's rather than the factor's: somebody who has burned eight of ten gets ten back without taking their working second factor off for a minute. Nothing about them mentions TOTP, so they stay right whatever the second factor is.
Headers
Responses
201The new set, for the only time it is shown RecoveryCodes4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/mfa/recovery:regenerate" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /auth/mfa:disable
Disable the second factor
Removes the TOTP enrolment and burns the remaining recovery codes. It demands a step-up like every privileged action - a stolen session removing the second factor is exactly the attack the factor exists against - and takes it with whatever the account holds: a code, a recovery code, its provider, or its password. Where the workspace's rule requires a factor of this person it cannot be disabled at all; the refusal names the rule. The body's password is the proof this route took before; it is still accepted for one release, deprecated, and goes then. A client sends the X-Hubtask-Step-Up header instead.
Headers
Request body application/json — MfaDisable
Responses
204Disabled4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/mfa:disable" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "password": "string"}' GET /auth/tokens
The caller's own personal access tokens
One's own tokens, newest first, with the scopes each carries, when it expires, when it was last used and whether it was revoked. Never anybody else's: a token is a credential its holder holds, and an administrator who could list one could not use it but could learn exactly which of somebody's automations to attack. The token itself is not among the fields. It is answered once, at creation, and stored only as a hash keyed on the installation secret; a token that was lost is revoked and minted again. A service account's tokens are listed here too, by naming it - which is the one exception, and needs the permission that manages members, because a service account is nothing but access and somebody has to answer for it.
Query parameters
Responses
200The tokens4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/auth/tokens" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /auth/tokens
Mint a personal access token
Answers the credential, in clear, for the only time. What is stored is its hash keyed on the installation secret under its own purpose label, so a hash from here can never be replayed as a signed cursor or a calendar feed token, and nothing can turn the stored value back into the token. The expiry is mandatory and at most a year out. There is no default: a caller has to say how long the credential should live, because the alternative is a credential nobody ever revokes. The scopes are requested explicitly and are never defaulted to everything. Each has to be one the installation declares - the manifest at /meta/capabilities lists them - and a token can never do more than its holder may, whatever it asks for: the scope is a second bound on top of the role, not a grant. Asking for an admin scope is a privileged action: it demands a fresh step-up, presented in the X-Hubtask-Step-Up header.
Headers
Request body application/json — AccessTokenCreate
Responses
201Minted, with the credential shown for the only time AccessTokenSecret4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/auth/tokens" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "string", "scopes": [ "string" ], "expires_at": "2026-09-16T10:00:00Z", "account_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}' DELETE /auth/tokens/{tokenId}
Revoke a personal access token
Takes effect on the next call: the hash is checked against the row on every request, so a revoked token refuses immediately rather than at the end of some cache's life. The row stays, stamped with the moment it was withdrawn, and that stamp is separate from the expiry - "it ran out" and "somebody pulled it" are different answers to "why did this stop working", and a reader of the trail is owed both. Revoking twice is not an error; somebody else's token is not found rather than forbidden.
Path parameters
Responses
204Revoked4XXAn error per RFC 9457 Problem
curl -X DELETE "$HUBTASK_URL/auth/tokens/{tokenId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN" GET /auth/service-accounts
The workspace's service accounts
The accounts that exist only to be acted through: an integration, a script, a rule that has to keep running after the person who wrote it has left. Needs the permission that manages members, because that is the person who answers for who holds access.
Responses
200The service accounts4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/auth/service-accounts" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /auth/service-accounts
Create a service account
Creates an account of kind SERVICE_ACCOUNT: no mail address, no sign-in anywhere, and tokens as its only credential. It holds rights the way a person does - through memberships granted to it - and it is what an automation rule runs as, so that a rule does not die with its author's departure.
Headers
Request body application/json — ServiceAccountCreate
curl -X POST "$HUBTASK_URL/auth/service-accounts" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "display_name": "string"}' POST /auth/oidc:start
Begin a sign-in through the workspace's identity provider
The first half of authorization code + PKCE. The workspace is resolved the way a password sign-in resolves it - the subdomain, or the tenant header - and its configured provider answers the URL to send the browser to. Nothing about where the code comes back is taken from the request: the redirect URI is this installation's own, derived from its base URL, so a caller cannot point the authorization answer anywhere else. The state this returns is the handle for the callback; the code verifier and the nonce stay on the server and are never shown. Refused with a clear code when the workspace has no provider configured or has switched it off, and when discovery cannot be reached - a degradation in which local accounts keep signing in. A sign-in begun on the invitation card carries the invitation's token, and the flow remembers the invited account it accepts; an invitation that cannot be redeemed is refused here with auth.redemption_failed, before the browser leaves. A sign-in begun from the link a workspace without the password mails to connect its provider carries that link's token (connect_token): the flow remembers the link and asks the provider for a fresh sign-in; a link that cannot be used is refused here with auth.reset_failed, before the browser leaves.
Request body application/json — OidcStart
Responses
201Where to send the browser OidcAuthorization4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/auth/oidc:start" \ -H "Content-Type: application/json" \ -d '{ "provider_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c", "login_hint": "string", "invitation_token": "string", "connect_token": "string", "terms_url": "string", "terms_version": "string"}' POST /auth/oidc:callback
Finish the sign-in with the code the provider issued
The second half: the authorization code is exchanged at the provider's token endpoint with the verifier this installation kept, and the ID token is verified in full - the signature against the provider's JWKS, iss, aud, exp, the nonce this flow minted, an algorithm allowlist that never contains none, and a clock skew of at most sixty seconds. Any of those failing is one refusal. What it answers is the same pair a password sign-in answers, because it is the same session: how somebody proved themselves is an attribute of the session, not a class of it. The account is provisioned on first arrival under the provider's subject, or linked to an existing local account when the verified address matches inside the configured domains - and that linking is audited. An existing account that already holds a password is not linked on the provider's word: the answer is 202 with the single method LINK, and the account's password is presented at /auth/sessions:link. An account with no password but another provider that lets it in here cannot give that proof here and is refused with identity_provider.link_needs_own_way_in; one whose only way in has gone - an identity at an offer that ended or a provider switched off, or a second factor and nothing else - is refused with identity_provider.link_needs_mailbox, which points at Forgot your password?. An invited account is activated and connected only with a second proof: the sign-in started from the invitation's own link (invitation_token at the start), or the provider is authoritative for the address. Through the link the invited address is admitted even outside a domains or directories list. The provider's verified address must equal the invited one either way. Without the proof nothing is connected or activated and the answer is identity_provider.invitation_needs_link; a different address is identity_provider.invitation_address_differs; an invitation that ran out or was accepted meanwhile is auth.redemption_failed through the link. A refused arrival never spends the invitation. Whatever was connected to the invited account before the proof is dropped when it is activated. Authority for the address, and a domains or directories list, decide only who comes in new on the provider's word. Under Only people invited here a provider that is not authoritative, and under Only these domains/directories an address outside the list, still bring an existing active account to the LINK step - connected only with its password and its second factor - or through its connect link; neither ever creates an account, and an address nobody here holds is refused. An existing account that holds no credential at all is connected only by a provider authoritative for its address, in every mode; otherwise nothing is connected and the answer is identity_provider.link_needs_mailbox, whose sentence points at Forgot your password?. A sign-in begun from the link a workspace without the password mails (connect_token at the start) connects that link's account and no other. The mailbox and a fresh sign-in at the provider are its proof, in place of the password: an auth_time older than the step-up's window, one from before the flow left for the provider (beyond a minute's clock skew), or none, is identity_provider.connect_not_fresh; the provider's verified address must equal the account's (identity_provider.connect_address_differs); admission is that of an arrival bringing its own proof (identity_provider.not_admitted); and an identity already connected to another account here is identity_provider.connect_identity_taken. None of these spends the link. An armed second factor is still asked - the answer is then 202 with the TOTP step, and the provider is connected when that step completes. The link is spent in the transaction that connects the provider or hands on to the factor, so a second return is auth.reset_failed, as is a link whose password was switched back on meanwhile. No password is stored. A state that is unknown, already spent or expired is refused indistinguishably: a flow handle is single use.
Request body application/json — OidcCallback
Responses
201Signed in SessionTokens202The address matched an account with a password, which is asked for first - or a connection by mail reached an account with a second factor, which is asked for before the provider is connected MfaChallenge4XXAn error per RFC 9457 Problem
Codes this operation names
identity_provider.link_needs_own_way_inidentity_provider.link_needs_mailboxidentity_provider.invitation_needs_linkidentity_provider.invitation_address_differsauth.redemption_failedidentity_provider.connect_not_freshidentity_provider.connect_address_differsidentity_provider.not_admittedidentity_provider.connect_identity_takenauth.reset_failed
curl -X POST "$HUBTASK_URL/auth/oidc:callback" \ -H "Content-Type: application/json" \ -d '{ "code": "string", "state": "string"}' GET /tenant/accounts-without-provider
How many people here no provider switched on here signs in
The number the password switch says before the password is switched off: the active people of this workspace who hold no identity at any provider that is a way in here now - the workspace's own switched on, the installation's taken and not past a withdrawal. Invited accounts have not chosen a way in yet and service accounts sign in with a token, so neither is counted. Where the password goes off, each of them connects a provider through Forgot your password? - or, holding a password, at the provider's first arrival with it. A number, never a list: the screen needs how many, not who. Needs the permission that manages structure, or the auditor's read-only configuration permission, as the list of providers does.
Responses
200The count AccountsWithoutProvider4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/tenant/accounts-without-provider" \ -H "Authorization: Bearer $HUBTASK_TOKEN" GET /identity-provider
The workspace's first identity provider
Kept, because nothing in this contract disappears. It was the whole of the provider surface before providers became plural, and a caller written against it keeps working: it reads the workspace's own first provider, in the order the collection lists them, and PUT writes that one - creating it where there is none. It cannot reach a second provider, it never answers one the installation offers, and it has no DELETE: removing one of several through a route that cannot name which would be a route that removes the wrong one. Use /identity-providers for all of that. Needs the permission that manages structure, or the auditor's read-only configuration permission. The client secret is never a member of the answer.
Responses
200The first provider IdentityProvider4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/identity-provider" \ -H "Authorization: Bearer $HUBTASK_TOKEN" PUT /identity-provider
Set the workspace's first identity provider
Sets the first provider whole, or creates it where the workspace has none. The same rules the collection applies - discovery before anything is stored, the mark held to its issuer, a public provider held to INVITED_ONLY. Auditable. The client secret is required when there is nothing to keep, and optional afterwards, as on the collection. And as there, enabled is not switched here: a created provider is off, and a changed value is refused with identity_provider.switch_in_list.
Headers
Request body application/json — IdentityProviderConfiguration
Responses
200Configured IdentityProvider4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X PUT "$HUBTASK_URL/identity-provider" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issuer": "https://example.org", "client_id": "string", "client_secret": "string", "display_name": "string", "kind": "GENERIC", "provisioning": "INVITED_ONLY", "position": 0, "enabled": true, "allowed_email_domains": [], "allowed_directories": [], "newcomer_role": "OWNER", "newcomer_group_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}' GET /identity-providers
The providers people can sign in to this workspace through
The workspace's own providers and the ones its installation offers every workspace, each with its issuer, the mark it draws, whom it lets in and whether it is switched on. scope says which level a row belongs to: an installation row is offered to this workspace and is not its to change. Needs the permission that manages structure, or the auditor's read-only configuration permission: which provider vouches for this workspace's people is configuration an auditor reads. The client secret is never a member of this answer. It is sealed at configuration time and read only by the token exchange.
Responses
200The providers in force4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/identity-providers" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /identity-providers
Add a provider this workspace signs its people in through
Discovery runs before anything is stored, through the guarded client, so an issuer this installation cannot reach or that disagrees with its own metadata is refused rather than saved and discovered later by a person trying to sign in. Two refusals belong to the preset rather than to the form. A public provider - one anybody in the world holds an account at - may only be INVITED_ONLY, because anything else provisions every person alive an account here. And a preset whose addresses this installation cannot vouch for may not be INVITED_ONLY at all, because that mode hands an account that already exists to whoever arrives with its address. The secret is sealed on the way in and appears in no answer afterwards. Auditable. A new provider is created switched off - configuring is not offering. It becomes a way in when the list of ways to sign in switches it on (:offer); enabled: true here is refused with identity_provider.switch_in_list.
Headers
Request body application/json — IdentityProviderConfiguration
Responses
201Added IdentityProvider4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/identity-providers" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issuer": "https://example.org", "client_id": "string", "client_secret": "string", "display_name": "string", "kind": "GENERIC", "provisioning": "INVITED_ONLY", "position": 0, "enabled": true, "allowed_email_domains": [], "allowed_directories": [], "newcomer_role": "OWNER", "newcomer_group_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}' PUT /identity-providers/{providerId}
Replace one of the workspace's identity providers
Set whole, not patched: a provider half-changed is a provider nobody can reason about. The client secret is the one exception - omitting it keeps the one that is sealed, which is the only way to change a name or a mode without retyping a value nothing can read back. A provider of the installation scope is answered 404 here: it is offered to this workspace and is not its to change. Auditable. Whether the provider is a way in is not set here: a changed enabled is refused with identity_provider.switch_in_list, and the list of ways to sign in switches it (:offer).
Path parameters
Headers
Request body application/json — IdentityProviderConfiguration
Responses
200Configured IdentityProvider4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X PUT "$HUBTASK_URL/identity-providers/{providerId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issuer": "https://example.org", "client_id": "string", "client_secret": "string", "display_name": "string", "kind": "GENERIC", "provisioning": "INVITED_ONLY", "position": 0, "enabled": true, "allowed_email_domains": [], "allowed_directories": [], "newcomer_role": "OWNER", "newcomer_group_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}' DELETE /identity-providers/{providerId}
Remove one of the workspace's identity providers
The configuration and its sealed secret go. Accounts provisioned through it keep their rows and their sessions - what they lose is the way to sign in again, which is why an account with no password is worth a thought before this call. Auditable.
Path parameters
Headers
Responses
204Removed4XXAn error per RFC 9457 Problem
curl -X DELETE "$HUBTASK_URL/identity-providers/{providerId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /identity-providers/{providerId}:offer
Switch a provider on or off as a way in here
One verb for two stores, because it is one question: is this a way into this workspace. For the workspace's own provider it is the row's enabled. For one the installation offers, it is this workspace's own switch - the row belongs to the installation and is not the workspace's to change, but whether it is offered here is: an offer of the installation reaches every workspace and is switched on in none, and each owner switches it on for their own. An installation's provider is off until a workspace switches it on. Offering every workspace a way in is the installation's decision; taking it is the workspace's. The last way in cannot be switched off: a workspace that turned off its only method would be a workspace nobody can reach. Auditable. A provider the installation's lock turns on cannot be switched off here. Where the installation locked the ways to sign in with providers among them, every provider it offers as Only people invited here is on in every workspace (installation_lock: TURNS_ON), and switching it off is refused with identity_provider.mandated, the lock's origin among its parameters. Switching it on is accepted and kept as this workspace's own choice, which applies again once the lock is lifted.
Path parameters
Headers
Request body application/json — ProviderOffer
Responses
200Switched IdentityProvider4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/identity-providers/{providerId}:offer" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "offered": true}' GET /identity-provider-presets
The providers Hubtask has a preset for, and what registering takes
Three rows and this installation's own callback address, which is the value every registration form at every provider asks for. instructions and particular are message codes rendered with redirect_uri; provisioning is the modes the preset permits, so a screen offers only what would be accepted rather than three of which two are refused. Behind the same permission as reading the providers. There is no reason for a signed-out visitor to read registration instructions.
Responses
200The presets4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/identity-provider-presets" \ -H "Authorization: Bearer $HUBTASK_TOKEN"