API reference · Hubtask API 0.1.0
admin
GET /admin/tenants
The installation's workspaces
The one legitimate tenant enumerator: provisioning and lifecycle are the control plane's job, and the control plane must see its rows. It reads through a deliberate installation-scoped path behind the admin:tenants scope - which no session carries: the credential is a personal access token minted for exactly this, behind a step-up. "Nothing enumerates tenants" remains the rule for jobs; this is not a job.
Responses
200The workspaces4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/admin/tenants" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/tenants
Provision a workspace
Creates the tenant with its defaults: a default hub, an example collection, standard buckets and labels, the locale and time zone, and the owner - an invited account whose redemption token is answered once, so the person the workspace is for can set their password and sign in. Idempotent under Idempotency-Key: provisioning twice under one key creates one.
Headers
Request body application/json — TenantProvision
Responses
201Provisioned, with the owner's redemption token shown for the only time ProvisionedTenant4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/admin/tenants" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "slug": "string", "display_name": "string", "default_locale": "string", "default_time_zone": "string", "owner_email": "anna@example.org", "owner_display_name": "string"}' POST /admin/tenants/{tenantId}:suspend
Suspend a workspace
Flips the middleware: every API call of the tenant's own people answers 403 tenant_suspended on their very next request. The data remains, and the read export still works - the suspended are exactly who needs it. Refused with admin.multi_mode_required in single mode, where the one workspace is the installation.
Path parameters
Responses
204Suspended4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/tenants/{tenantId}:suspend" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/tenants/{tenantId}:resume
Reactivate a workspace
One write. The next request of the tenant's people works again.
Path parameters
Responses
204Active again4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/admin/tenants/{tenantId}:resume" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/tenants/{tenantId}:cancel-deletion
Call off a workspace's pending deletion
Returns a workspace pending deletion to where it stood when the deletion was requested - active or suspended; suspended where that is not known - and clears the deadline. The automation rules the request switched off are switched back on, a schedule from now on, without the occurrences it missed. Possible until the hard delete begins, also past the deadline while a legal hold keeps the workspace; refused with admin.tenant_deletion_underway once it has begun (purge_started_at on the listing). Cancelling a workspace that is not pending changes nothing. Legal holds stay as they are.
Path parameters
Responses
204The deletion is called off4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/tenants/{tenantId}:cancel-deletion" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/tenants/{tenantId}:delete
Request a workspace's deletion
The most irreversible act this API has, and it behaves like it: it demands a fresh step-up in the X-Hubtask-Step-Up header and the workspace's display name, typed - the restore precedent. Refused with admin.multi_mode_required in single mode, where the one workspace is the installation. The tenant moves to PENDING_DELETION: access blocked, automations disabled, the export still answering. Until the hard delete begins, :cancel-deletion calls it off. The hard delete runs after the 30-day grace as a job seeded by this very write - it cascades across database rows, media bytes, search, outbox and queue, and leaves an installation-level evidence entry, because the tenant's own trail dies with it.
Path parameters
Headers
Request body application/json — TenantDeletionRequest
Responses
202Scheduled, naming the moment the grace runs out TenantDeletionScheduled4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/tenants/{tenantId}:delete" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "confirmation": "string"}' POST /admin/tenants/{tenantId}:open-password
Open the password for one workspace
For a provider that is switched on but broken - unreachable, or admitting nobody - the operator opens the password for this one workspace, for a limited time: 24 hours unless hours says otherwise, at most 168. Every account there that holds a password signs in with it, under the workspace's own rules and second factor, whatever the workspace's own switch and any installation lock say; an account without one is mailed a link to set one when it asks (POST /auth/password:forgot). It ends on its own at password_opening.until, and :close-password ends it early. Demands admin:tenants, the operator register and a fresh step-up in the X-Hubtask-Step-Up header. The requester and the reason are recorded with the end in the workspace's trail (tenant.password_opened) and in the installation's journal, and the workspace's administrators are told. A second opening replaces the first. It reads and changes nothing of the workspace's content. Only an active workspace is opened: a suspended one refuses its people before any password is asked for (admin.password_opening_suspended), and one pending deletion is leaving (admin.tenant_leaving).
Path parameters
Headers
Request body application/json — PasswordOpeningRequest
Responses
200Opened; the workspace, with the opening in force AdminTenant4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/tenants/{tenantId}:open-password" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "hours": 24, "requester": "string", "reason": "string"}' POST /admin/tenants/{tenantId}:close-password
Close an operator's opening of the password early
Ends the opening :open-password made before its time. The workspace's own rules decide again from the next request on. Recorded in the workspace's trail (tenant.password_closed) and the installation's journal, and the workspace's administrators are told. No step-up: closing narrows the way in rather than widening it. Closing where nothing is open changes nothing and records nothing.
Path parameters
Responses
200Closed, or there was nothing to close; the workspace AdminTenant4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/tenants/{tenantId}:close-password" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/tenants/{tenantId}:replace-legal-holds
Place the legal holds of a rewound period again
After a point-in-time recovery of the installation, before traffic is admitted, the operator places again the legal holds the rewind removed from this workspace. Each hold keeps its identifier, scope, reason, placer and moment; one released in the rewound period is placed in force all the same, and its owner releases it again. A hold the workspace already has is left as it is, so a second run changes nothing. A hold whose target the recovery removed is placed and reported. Demands admin:tenants. Each hold placed writes lifecycle.hold_replaced into the workspace's trail.
Path parameters
Request body application/json — LegalHoldReplaceRequest
Responses
200What happened to each hold LegalHoldReplaceResult4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/tenants/{tenantId}:replace-legal-holds" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "recovery_point": "2026-09-16T10:00:00Z", "holds": [ { "id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c", "scope": {}, "reason": "string", "placed_by": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c", "placed_at": "2026-09-16T10:00:00Z" } ], "archive": { "target_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c", "path": "string" }}' POST /admin/tenants/{tenantId}:export
Export a workspace whole
A complete, documented archive of one workspace - JSON Lines per entity, media by reference with checksums, a manifest naming counts and the product version - written as a job to a configured backup target. It is the Hubtask archive format, documented well enough to build an importer against the documentation alone: the basis for access requests, provider migration, and the "no lock-in" promise. It works for ACTIVE, SUSPENDED and PENDING_DELETION workspaces alike - the suspended and the leaving are exactly who needs it - and it is audited with its target, never its content. The export runs through the same row-level-security path as the API, and the answer is a JobRef; the archive itself is at the target, so there is no result_url.
Path parameters
Request body application/json — TenantExportRequest
curl -X POST "$HUBTASK_URL/admin/tenants/{tenantId}:export" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "target_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}' GET /admin/settings
What this installation has decided for every workspace on it
The level above the workspaces: the sign-in switches this installation set, which of them are locked, the operator's legal links, and the path to its own list of refused passwords. Only what was decided. A level that answered the product's defaults for everything else would be one nobody could tell apart from an operator who had chosen them - and "the operator decided nothing here" is a value the resolver acts on. Behind admin:tenants and the operator register, both checked: the scope says what a credential may reach and the register says whose credential it may be, and either alone is a hole.
Responses
200The level InstanceSettings4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/admin/settings" \ -H "Authorization: Bearer $HUBTASK_TOKEN" PUT /admin/settings
Replace the installation's level
A PUT rather than a merge: a merge over eighteen switches that can each be absent has no way to say "unset this one", and being able to say it is the whole point of a level that distinguishes a decision from a default. A locked switch applies to every workspace and switches that workspace's control off - with the reason and with who set it, rather than hiding it. A setting that simply is not there is a setting somebody opens a support ticket about. One journal entry, naming the switches that moved and never their values: a journal carrying a blocklist's path or a legal URL would be configuration written into a place nothing ever deletes from.
Request body application/json — InstanceSettings
Responses
200The level as it now stands InstanceSettings4XXAn error per RFC 9457 Problem
curl -X PUT "$HUBTASK_URL/admin/settings" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "sign_in": {}, "legal": {}, "localisation": {}, "quotas": {}, "ai": {}, "blocklist_file": "string", "source": "string", "is_enforced_from_file": true}' GET /admin/operators
Who operates this installation
The operator register. An empty one is the private installation: nothing was configured, one workspace, and its owner is the operator exactly as they were before the register existed. Each row names its account - a name, the address or sign-in name, the kind and the workspace - so that nobody has to read an identifier. Only accounts in the register are ever named; a workspace's other accounts are not listed anywhere at this level.
Responses
200The register4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/admin/operators" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/operators
Put an account in the register
A service account may be an operator: a purchase platform that provisions workspaces needs a credential that does not belong to a person who may leave, and the first day of a platform is the day that becomes true. An account that is already an operator is not an error - the caller asked for somebody to be one, and they are. Only an active account can be added (admin.operator_not_active). Adding the first entry adds the caller too. On an empty register the only workspace's owner is the operator, and the first row ends that; so where the register was empty, the caller is entered in the same statement, and both entries are in the journal.
Request body application/json — OperatorAdd
Responses
204In the register4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/operators" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "account_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c", "workspace": "string", "email": "anna@example.org"}' DELETE /admin/operators/{accountId}
Take an account out of the register
The last operator cannot be removed: an installation with no operators is an installation nobody can operate. The refusal is in the statement rather than in a read, because two operators removing each other at the same moment would both read "there are two". The account alone identifies the row - an identifier is unique across the installation, and a caller that had to name the workspace too would have to read the register to find out which one it is. An account that is not in the register is admin.operator_not_registered (404); the last one is admin.operator_last (409).
Path parameters
Responses
204Out of the register4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X DELETE "$HUBTASK_URL/admin/operators/{accountId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN" GET /admin/overview
How big this installation is and how its workspaces stand
The dashboard's first screen: the workspaces by state, and how many accounts they hold between them. Counts and states, never rows. The tenant boundary is a database policy rather than a role, and this answer does not go around it - what it reads is a function that can answer five integers and nothing else. What an operator needs to run an installation is counts, states and limits; the health of the machinery is GET /meta/health, which is where it already lives. Behind admin:tenants and the operator register, both.
Responses
200The census InstanceOverview4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/admin/overview" \ -H "Authorization: Bearer $HUBTASK_TOKEN" GET /admin/journal
The installation's own record
What was provisioned, suspended, resumed and deleted, and what the operator changed - newest first. Evidence of acts whose per-tenant trail cannot hold them: after a hard delete the workspace's own audit chain is gone by design, which is the reason this record exists. Identifiers, slugs, counts and moments. Never anybody's content, which is what makes it readable at all after the workspace it names is gone.
Query parameters
Responses
200One page, newest first InstanceJournalPage4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/admin/journal" \ -H "Authorization: Bearer $HUBTASK_TOKEN" GET /admin/ai-providers
The AI models this installation offers every workspace
A workspace sees only each model's name, processor and jurisdiction, and switches one on itself - offering a model switches nothing on. Behind admin:tenants and the operator register, both. The key is not among the fields.
Responses
200The offered models4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/admin/ai-providers" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/ai-providers
Offer an AI model to every workspace
The same rules as a workspace's own model, plus a name and a processor every workspace is shown before it agrees. A model outside the EEA is refused with ai.third_country_not_confirmed unless the installation confirmed such transfers. Journalled in the installation's own evidence.
Request body application/json — OfferedAiModelConfiguration
Responses
201Offered OfferedAiModel4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/ai-providers" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "display_name": "string", "processor": "string", "kind": "NOOP", "base_url": "https://example.org", "completion_model": "string", "embedding_model": "string", "api_key": "string", "jurisdiction": "SELF_HOSTED"}' PUT /admin/ai-providers/{providerId}
Change an offered AI model
Set whole, with the key's one exception: omitting it keeps the one that is sealed, an empty string clears it. Journalled.
Path parameters
Request body application/json — OfferedAiModelConfiguration
Responses
200Changed OfferedAiModel4XXAn error per RFC 9457 Problem
curl -X PUT "$HUBTASK_URL/admin/ai-providers/{providerId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "display_name": "string", "processor": "string", "kind": "NOOP", "base_url": "https://example.org", "completion_model": "string", "embedding_model": "string", "api_key": "string", "jurisdiction": "SELF_HOSTED"}' DELETE /admin/ai-providers/{providerId}
Withdraw an offered AI model
Removes the model and its sealed key. Every workspace that chose it gets no AI answer until it chooses again; nothing a workspace configured itself is touched. Journalled.
Path parameters
Responses
204Withdrawn4XXAn error per RFC 9457 Problem
curl -X DELETE "$HUBTASK_URL/admin/ai-providers/{providerId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN" GET /admin/identity-providers
The providers this installation offers every workspace
Every workspace on this installation reads these and draws their buttons; none may change them, which is the row-level policy and not a check in a handler. Behind admin:tenants and the operator register, both.
Responses
200The installation's providers4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/admin/identity-providers" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/identity-providers
Offer every workspace a way in
The same rules as a workspace's own provider - discovery before anything is stored, the mark held to the issuer it belongs to, a public provider held to INVITED_ONLY - because it is the same use case underneath. Journalled in the installation's own evidence, where no workspace's trail could hold it.
Headers
Request body application/json — IdentityProviderConfiguration
Responses
201Added IdentityProvider4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/admin/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 /admin/identity-providers/{providerId}
Replace one of the installation's providers
Set whole, with the secret's one exception: omitting it keeps the one that is sealed. Journalled. Whether the installation offers the provider is not changed here: an offer ends through :withdraw, announced for a date or confirmed with the count, and is kept with :cancel-withdrawal. A changed enabled is refused with identity_provider.withdraw_instead; the same value, or none, changes nothing.
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/admin/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 /admin/identity-providers/{providerId}
Remove a provider whose offer has ended
Removes the installation's provider and its sealed secret - once its offer has ended (:withdraw, its day reached) or where no workspace uses it. While the offer stands and a workspace uses it, removal is refused with identity_provider.withdraw_first, its count the number of workspaces - or, where a withdrawal is already announced, with identity_provider.remove_after_withdrawal, its date the moment the offer ends. An offer ends announced, and Withdraw now is the answer to a compromised provider. The connections between people and the provider are deleted and are not restored by offering it again; the accounts keep their rows and their live sessions. Journalled.
Path parameters
Headers
Responses
204Removed4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X DELETE "$HUBTASK_URL/admin/identity-providers/{providerId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/identity-providers/{providerId}:withdraw
Announce the withdrawal of an offered provider, or withdraw it now
A withdrawal is announced for a date - fourteen days ahead where none is given - and until then the provider keeps working everywhere while every workspace that has it switched on tells its administrators when it ends; it can be cancelled. On the date the offer ends: off everywhere, the connected identities left in place. A date that is now or past is Withdraw now, the answer to a compromised provider: it additionally asks for confirm_count, the number of workspaces that have it switched on as the operator just read it - refused with identity_provider.withdraw_count_mismatch when it does not match. A date less than a day ahead is no notice either, and asks for the count the same way. A workspace left with no way in afterwards falls back to the password for the accounts that hold one. An offer that has already ended is not withdrawn again - refused with identity_provider.already_withdrawn, so a call cannot revive it; it is offered again only through :cancel-withdrawal. Journalled.
Path parameters
Headers
Request body application/json — ProviderWithdrawal
Responses
200The provider with its withdrawal date IdentityProvider4XXAn error per RFC 9457 Problem
Codes this operation names
curl -X POST "$HUBTASK_URL/admin/identity-providers/{providerId}:withdraw" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "withdraw_at": "2026-09-16T10:00:00Z", "confirm_count": 0}' POST /admin/identity-providers/{providerId}:cancel-withdrawal
Keep offering a provider whose withdrawal was announced
Clears the withdrawal. Before the date nothing changes for anybody; after it, the offer is back and the workspaces that had it switched on sign in through it again, because the connected identities were never removed. Journalled.
Path parameters
Headers
Responses
200The provider, offered without an end IdentityProvider4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/admin/identity-providers/{providerId}:cancel-withdrawal" \ -H "Authorization: Bearer $HUBTASK_TOKEN" GET /admin/encryption
The keyring, and what still names each key
What a rotation needs before it can end: the master key new values are sealed under, and for every key the ring holds - and every key a stored value still names - how many values name it. Removing a key while a row still names it turns that row into a refusal, so the count is the gate rather than the calendar. Installation-scoped, behind the admin:tenants scope, and it names no workspace: a count per key and kind is what an operator needs, and which workspace holds a webhook secret is not. The installation's own secrets - its sign-in providers' and its offered AI models' - are counted once, beside the workspaces'.
Responses
200The keyring EncryptionStatus4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/admin/encryption" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /admin/encryption:reseal
Re-seal what older keys still hold
Rewraps every stored value that names a key other than the current one, so that the older key can finally leave the ring. The plaintext is untouched: the data key is unwrapped under the key its row names and wrapped again under the current one, and nothing above the port can tell the difference. One job per workspace rather than one sweep over the installation - nothing enumerates tenants for its own purposes, and this is an operator asking rather than a schedule - and one more for the installation's own sign-in providers and offered AI models. The call returns once the work is queued; progress is read back from GET /admin/encryption until every count but the active key's is zero. Asking twice before the first round has run queues nothing new.
Responses
202The re-sealing was queued ResealAccepted4XXAn error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/admin/encryption:reseal" \ -H "Authorization: Bearer $HUBTASK_TOKEN" PATCH /admin/tenants/{tenantId}/quotas
Set a workspace's quotas
The operator's write: each provided key becomes the workspace's ceiling, an explicit null clears the override back to the mode's default, and absent keys stay untouched. 0 means unlimited. Audited - a quota decides what a workspace may hold, and "who moved the wall" is a question with an answer.
Path parameters
Request body application/merge-patch+json — TenantQuotas
Responses
200The workspace's quotas after the write, resolved4XXAn error per RFC 9457 Problem
curl -X PATCH "$HUBTASK_URL/admin/tenants/{tenantId}/quotas" \ -H "Authorization: Bearer $HUBTASK_TOKEN" \ -H "Content-Type: application/merge-patch+json" \ -d '{ "api_requests_per_minute": 0, "items": 0, "media_bytes": 0, "automation_runs_per_hour": 0, "webhook_targets": 0, "export_jobs": 0, "ai_tokens_per_day": 0}'