Skip to content

IAM — Users

An account belongs to exactly one tenant, where its email address is unique and is what a login resolves. It begins as an invitation or a claim code — only the code's hash is stored — and comes into being when that code is redeemed with a password. Not every rule on the entry is written by hand: pairing a device adds one naming that device, and unpairing takes it away.

Rules also arrive through roles and user groups, which an invitation may attach up front. Credentials and second factors are handled in authentication. Creation and invitation raise IAM_USER_CREATE and IAM_USER_CREATE_PENDING; the Quickstart walks through redeeming an invite.

POSTInvite a user/api/v1/user
GETList users/api/v1/user
GETList outstanding invites/api/v1/user/pending
DELETERevoke an invite/api/v1/user/pending/:userId
POSTReissue an invite/api/v1/user/pending/:userId/resend
POSTRedeem a code and register/api/v1/user/register
POSTVerify an email address/api/v1/user/verify-email
POSTResend the verification mail/api/v1/user/resend-verification
POSTRequest a password reset/api/v1/user/forgot-password
POSTReset a password/api/v1/user/reset-password
GETRead a user/api/v1/user/:userId
PATCHUpdate a user/api/v1/user/:userId
DELETEDelete a user/api/v1/user/:userId
POSTMint a tenant claim code/api/v1/user/claim-codes

Invite a user

Creates no account yet — it mints an invitation the invitee redeems at /user/register, and the returned identifier is the account it will become. The code is in the response unless sendMail is set, in which case it travels by mail only and the account skips email verification; attached roles and usergroups each take share.

POST/api/v1/user

Body

NameDescription
emailstring
accessarrayoptionalA resource access policy entry
identifierThe resource this policy applies to
Alternative 1Resource identifier pattern (CRN), may include wildcards
Alternative 2Resource identifier pattern (CRN), may include wildcards
Alternative 3Resource identifier pattern (CRN), may include wildcards
Alternative 4Resource identifier pattern (CRN), may include wildcards
Alternative 5Resource identifier pattern (CRN), may include wildcards
Alternative 6Resource identifier pattern (CRN), may include wildcards
effectenumAccess policy effect — either allow or deny
Possible values: deny, allow
actionsarrayList of actions permitted by this policy
policyobjectoptionalAdditional constraints for this access policy
enforce2FAbooleanoptionalRequires the subject to have 2FA active before this access is granted
targetoptionalNarrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Alternative 1Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Possible values: user, role, group
Alternative 2Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
isOwnerbooleanoptionalMarks an owner-relative rule on a shared role: the crn#tenant:*.user:* head is rebound to the holder's own CRN before the rule is evaluated, so every member reaches only their own resources.
inviteCodeTtlnumberoptionalHow long an invite code remains valid, in seconds
> 0
tenantIdstringoptionalTenant identifier
sendMailbooleanoptionalMail the invitation to the address instead of returning the code. The code is then withheld from the response — the address being the only channel it travels on is what makes redemption evidence that the address is real.
rolesarraystringoptionalCRNs of the roles the invited account joins on redemption. Each one takes share on that role.
usergroupsarraystringoptionalCRNs of the user groups the invited account joins on redemption. Each one takes share on the group and never on the roles inside it.
localestringoptionalPattern: ^[a-z]{2}(-[A-Z]{2})?$

Response

NameDescription
resourceIdentifierstringUnique Coldwave resource name (CRN) that identifies a resource
inviteCodestringoptionalOne-time invite code for user registration

Errors

StatusDescriptionBody
403You do not have permission to perform this action
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
400The email address already has an account in this tenant — invite a different address, or reset the password of the existing account.code: USER_ALREADY_EXISTS
404No account with this id exists in the tenant.code: USER_NOT_FOUND
400The access rules asked for — on the account, or on the invitation that will create it — reach beyond the tenant's rootAccess. The response carries both the requested set and the tenant's ceiling, so drop or narrow the rules that exceed it.code: USER_NO_SUBSET
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

List users

The accounts of the caller's tenant, filtered one by one by read, so a caller scoped to single users still sees those. Password hash and salt never leave the backend.

GET/api/v1/user

Response

NameDescription
enforce2FAbooleanoptionalWhether two-factor authentication is required
Default: false
emailstringThe account's email address; within one tenant at most one account may hold it.
emailVerifiedbooleanoptionalWhether this email address has been verified
Default: false
accessarrayA resource access policy entry
identifierThe resource this policy applies to
Alternative 1Resource identifier pattern (CRN), may include wildcards
Alternative 2Resource identifier pattern (CRN), may include wildcards
Alternative 3Resource identifier pattern (CRN), may include wildcards
Alternative 4Resource identifier pattern (CRN), may include wildcards
Alternative 5Resource identifier pattern (CRN), may include wildcards
Alternative 6Resource identifier pattern (CRN), may include wildcards
effectenumAccess policy effect — either allow or deny
Possible values: deny, allow
actionsarrayList of actions permitted by this policy
policyobjectoptionalAdditional constraints for this access policy
enforce2FAbooleanoptionalRequires the subject to have 2FA active before this access is granted
targetoptionalNarrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Alternative 1Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Possible values: user, role, group
Alternative 2Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
isOwnerbooleanoptionalMarks an owner-relative rule on a shared role: the crn#tenant:*.user:* head is rebound to the holder's own CRN before the rule is evaluated, so every member reaches only their own resources.
resourceIdentifierstringUnique Coldwave resource name (CRN) that identifies a resource
saltstringoptionalRandom value used to salt the password hash
hashstringoptionalBcrypt password hash
pepperVersionstringoptionalWhich pepper the stored password hash was derived with; absent means the hash is unpeppered. Stripped from every response — it appears here only because the stored entity carries it.
twoFactorAutharrayoptionalTwo-factor authentication data for this user
Default: []
createdBystringUnique Coldwave resource name (CRN) that identifies a resource
invitedBystringoptionalWhat admitted the account — the user who sent the invitation, or the device whose claim code was redeemed. Absent when the invitation was addressed to nobody, as with a broadcast tenant claim code or an account created outright.

Errors

StatusDescriptionBody
403You do not have permission to perform this action
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

List outstanding invites

Invitations that are neither redeemed nor expired. The code and the hash it is stored under are withheld, so an invite is addressed everywhere else by futureUserRi — the account it will become.

GET/api/v1/user/pending

Response

NameDescription
tenantRistringUnique Coldwave resource name (CRN) that identifies a resource
futureUserRistringUnique Coldwave resource name (CRN) that identifies a resource
emailstring
accessarrayoptionalA resource access policy entry
Default: []
identifierThe resource this policy applies to
Alternative 1Resource identifier pattern (CRN), may include wildcards
Alternative 2Resource identifier pattern (CRN), may include wildcards
Alternative 3Resource identifier pattern (CRN), may include wildcards
Alternative 4Resource identifier pattern (CRN), may include wildcards
Alternative 5Resource identifier pattern (CRN), may include wildcards
Alternative 6Resource identifier pattern (CRN), may include wildcards
effectenumAccess policy effect — either allow or deny
Possible values: deny, allow
actionsarrayList of actions permitted by this policy
policyobjectoptionalAdditional constraints for this access policy
enforce2FAbooleanoptionalRequires the subject to have 2FA active before this access is granted
targetoptionalNarrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Alternative 1Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Possible values: user, role, group
Alternative 2Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
isOwnerbooleanoptionalMarks an owner-relative rule on a shared role: the crn#tenant:*.user:* head is rebound to the holder's own CRN before the rule is evaluated, so every member reaches only their own resources.
roleRisarraystringoptionalCRNs of the roles the account joins when the invitation is redeemed.
Default: []
usergroupRisarraystringoptionalCRNs of the user groups the account joins when the invitation is redeemed. A group is a bundle of roles and is granted whole — which is what lets an admin hand out a bundle they could not have assembled role by role.
Default: []
emailVerifiedbooleanoptionalWhether this email address has been verified
Default: false
expiresAtnumberWhen the code, token or invitation stops being redeemable, UTC epoch milliseconds. Past it the entry counts as absent rather than as expired: the pending list withholds it, and revoking or resending it answers 404.
createdBystringUnique Coldwave resource name (CRN) that identifies a resource

Errors

StatusDescriptionBody
403You do not have permission to perform this action
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Revoke an invite

{userId} is the id of the account the invite would create; an invite has no addressable id of its own. It takes delete on that future account, or on a group the invite would place it in, and the code stops working immediately.

DELETE/api/v1/user/pending/Yk3pL7rWq2

Path Parameters

NameDescription
userIdstringUser resource identifier

Errors

StatusDescriptionBody
403You do not have permission to perform this action
404No account with this id exists in the tenant.code: USER_NOT_FOUND
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Reissue an invite

Mints a fresh code and kills the old one — the plaintext of the previous code is stored nowhere, so it cannot be repeated. Roles and groups ride along unchanged and each takes share again; an address claimed in the meantime is refused.

POST/api/v1/user/pending/Yk3pL7rWq2/resend

Path Parameters

NameDescription
userIdstringUser resource identifier

Body

NameDescription
inviteCodeTtlnumberoptionalHow long an invite code remains valid, in seconds
> 0
sendMailbooleanoptionalMail the invitation to the address instead of returning the code. The code is then withheld from the response — the address being the only channel it travels on is what makes redemption evidence that the address is real.
localestringoptionalPattern: ^[a-z]{2}(-[A-Z]{2})?$

Response

NameDescription
resourceIdentifierstringUnique Coldwave resource name (CRN) that identifies a resource
inviteCodestringoptionalOne-time invite code for user registration

Errors

StatusDescriptionBody
403You do not have permission to perform this action
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
400The email address already has an account in this tenant — invite a different address, or reset the password of the existing account.code: USER_ALREADY_EXISTS
404No account with this id exists in the tenant.code: USER_NOT_FOUND
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Redeem a code and register

Public: turns a code into an account with a password. An INVITE_CODE carries its own address, so email is ignored; a CLAIM_CODE needs one, and a device's printed claim code also claims and pairs that device — emailVerified in the response says whether a verification code is on its way.

POST/api/v1/user/register

Body

NameDescription
codeA verification code entry
Alternative 1objectA verification code entry
typeType of verification code
codestringThe verification code value
Alternative 2objectA verification code entry
typeType of verification code
codestringThe verification code value
emailstringoptional
passwordstringAccount password: 16 to 64 characters, with no character-class rules on top. The same bound holds at registration, reset and change, so a shorter one is refused before it can ever be set.
min length 16 · max length 64
Pattern: ^[\s\S]{16,64}$

Response

NameDescription
resourceIdentifierstringUnique Coldwave resource name (CRN) that identifies a resource
emailVerifiedbooleanWhether this email address has been verified

Errors

Rate limited.
5 requests / 3600 s per ip (register-per-ip)

StatusDescriptionBody
400The invite or verification code does not match. Codes are single-use; a code already redeemed reads as invalid.code: USER_INVALID_CODE
400The invite or verification code has passed its lifetime. Ask an administrator for a fresh invitation.code: USER_CODE_EXPIRED
400The email address already has an account in this tenant — invite a different address, or reset the password of the existing account.code: USER_ALREADY_EXISTS
503The tenant's password secret could not be resolved, so no password hash could be computed. The account was not created; the condition is transient, so retry.code: USER_PEPPER_UNAVAILABLE
429The rate limit for this endpoint was exceeded. The Retry-After header gives the length of the current window in seconds.error: RATE_LIMITED
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Verify an email address

Consumes the code from the verification mail and marks the address verified. The code is good for ten minutes; it is matched case-insensitively and the usual O/0 and I/1 mix-ups are corrected before lookup.

POST/api/v1/user/verify-email

Body

NameDescription
codestringA verification code entry

Errors

Rate limited.
20 requests / 3600 s per ip (verify-email-per-ip)

StatusDescriptionBody
400The invite or verification code does not match. Codes are single-use; a code already redeemed reads as invalid.code: USER_INVALID_CODE
400The invite or verification code has passed its lifetime. Ask an administrator for a fresh invitation.code: USER_CODE_EXPIRED
429The rate limit for this endpoint was exceeded. The Retry-After header gives the length of the current window in seconds.error: RATE_LIMITED
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Resend the verification mail

Public, and always answers 200 — whether the address exists, and whether it is already verified, is deliberately not disclosed. The account is resolved from the address together with the request's host, and at most three mails per address go out in an hour.

POST/api/v1/user/resend-verification

Body

NameDescription
emailstring

Errors

Rate limited.
3 requests / 3600 s (mail-per-address)

10 requests / 3600 s per ip + route (mail-per-ip)

StatusDescriptionBody
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
429The rate limit for this endpoint was exceeded. The Retry-After header gives the length of the current window in seconds.error: RATE_LIMITED
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Request a password reset

Public, and always answers 200 — the existence of the address is not disclosed. An address holding accounts in several tenants on this host is mailed once per account, each mail naming its tenant and carrying a token good for one hour; the link points at the Origin the request came from.

POST/api/v1/user/forgot-password

Body

NameDescription
emailstring

Errors

Rate limited.
3 requests / 3600 s (mail-per-address)

10 requests / 3600 s per ip + route (mail-per-ip)

StatusDescriptionBody
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
429The rate limit for this endpoint was exceeded. The Retry-After header gives the length of the current window in seconds.error: RATE_LIMITED
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Reset a password

Consumes the token from the reset mail, sets the new password and signs every session of that account out. The token is single-use and lives one hour; an unknown and an expired one are answered with different codes.

POST/api/v1/user/reset-password

Body

NameDescription
tokenstringThe one-time token from the password-reset link. Only its hash is stored, so it can be neither looked up nor resent — a lost token means asking for a new reset.
passwordstringAccount password: 16 to 64 characters, with no character-class rules on top. The same bound holds at registration, reset and change, so a shorter one is refused before it can ever be set.
min length 16 · max length 64
Pattern: ^[\s\S]{16,64}$

Errors

StatusDescriptionBody
400The invite or verification code does not match. Codes are single-use; a code already redeemed reads as invalid.code: USER_INVALID_CODE
400The invite or verification code has passed its lifetime. Ask an administrator for a fresh invitation.code: USER_CODE_EXPIRED
503The tenant's password secret could not be resolved, so no password hash could be computed. The account was not created; the condition is transient, so retry.code: USER_PEPPER_UNAVAILABLE
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Read a user

The account with the rules written directly on it; hash and salt are stripped from the response. The roles and groups it holds are not fields of the entry — read those from the role or group side.

GET/api/v1/user/Yk3pL7rWq2

Path Parameters

NameDescription
userIdstringUser resource identifier

Response

NameDescription
enforce2FAbooleanoptionalWhether two-factor authentication is required
Default: false
emailstringThe account's email address; within one tenant at most one account may hold it.
emailVerifiedbooleanoptionalWhether this email address has been verified
Default: false
accessarrayA resource access policy entry
identifierThe resource this policy applies to
Alternative 1Resource identifier pattern (CRN), may include wildcards
Alternative 2Resource identifier pattern (CRN), may include wildcards
Alternative 3Resource identifier pattern (CRN), may include wildcards
Alternative 4Resource identifier pattern (CRN), may include wildcards
Alternative 5Resource identifier pattern (CRN), may include wildcards
Alternative 6Resource identifier pattern (CRN), may include wildcards
effectenumAccess policy effect — either allow or deny
Possible values: deny, allow
actionsarrayList of actions permitted by this policy
policyobjectoptionalAdditional constraints for this access policy
enforce2FAbooleanoptionalRequires the subject to have 2FA active before this access is granted
targetoptionalNarrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Alternative 1Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Possible values: user, role, group
Alternative 2Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
isOwnerbooleanoptionalMarks an owner-relative rule on a shared role: the crn#tenant:*.user:* head is rebound to the holder's own CRN before the rule is evaluated, so every member reaches only their own resources.
resourceIdentifierstringUnique Coldwave resource name (CRN) that identifies a resource
saltstringoptionalRandom value used to salt the password hash
hashstringoptionalBcrypt password hash
pepperVersionstringoptionalWhich pepper the stored password hash was derived with; absent means the hash is unpeppered. Stripped from every response — it appears here only because the stored entity carries it.
twoFactorAutharrayoptionalTwo-factor authentication data for this user
Default: []
createdBystringUnique Coldwave resource name (CRN) that identifies a resource
invitedBystringoptionalWhat admitted the account — the user who sent the invitation, or the device whose claim code was redeemed. Absent when the invitation was addressed to nobody, as with a broadcast tenant claim code or an account created outright.

Errors

StatusDescriptionBody
403You do not have permission to perform this action
404No account with this id exists in the tenant.code: USER_NOT_FOUND
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Update a user

Writes access and enforce2FA, nothing else. access replaces the rule set as a whole and is capped by the tenant's rootAccess — a rule written straight onto an account is worth exactly as much at request time as one on a role.

PATCH/api/v1/user/Yk3pL7rWq2

Path Parameters

NameDescription
userIdstringUser resource identifier

Body

NameDescription
accessarrayoptionalA resource access policy entry
identifierThe resource this policy applies to
Alternative 1Resource identifier pattern (CRN), may include wildcards
Alternative 2Resource identifier pattern (CRN), may include wildcards
Alternative 3Resource identifier pattern (CRN), may include wildcards
Alternative 4Resource identifier pattern (CRN), may include wildcards
Alternative 5Resource identifier pattern (CRN), may include wildcards
Alternative 6Resource identifier pattern (CRN), may include wildcards
effectenumAccess policy effect — either allow or deny
Possible values: deny, allow
actionsarrayList of actions permitted by this policy
policyobjectoptionalAdditional constraints for this access policy
enforce2FAbooleanoptionalRequires the subject to have 2FA active before this access is granted
targetoptionalNarrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Alternative 1Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
Possible values: user, role, group
Alternative 2Narrows a pair grant to where a claimed device may land: a kind — user, role or group — or one owner's CRN outright.
isOwnerbooleanoptionalMarks an owner-relative rule on a shared role: the crn#tenant:*.user:* head is rebound to the holder's own CRN before the rule is evaluated, so every member reaches only their own resources.
enforce2FAbooleanoptionalWhether two-factor authentication is required

Errors

StatusDescriptionBody
403You do not have permission to perform this action
404No account with this id exists in the tenant.code: USER_NOT_FOUND
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
400The access rules asked for — on the account, or on the invitation that will create it — reach beyond the tenant's rootAccess. The response carries both the requested set and the tenant's ceiling, so drop or narrow the rules that exceed it.code: USER_NO_SUBSET
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Delete a user

Removes the account, releases its email address so it can be invited again, and drops it out of every role it held. Nothing beneath the tenant is cascaded.

DELETE/api/v1/user/Yk3pL7rWq2

Path Parameters

NameDescription
userIdstringUser resource identifier

Errors

StatusDescriptionBody
403You do not have permission to perform this action
404No account with this id exists in the tenant.code: USER_NOT_FOUND
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50

Mint a tenant claim code

Returns an opaque code — good for 30 days unless expiresIn says otherwise — that turns into an account at /user/register. Redeeming it does not spend it, so everyone it was handed to can use it; roleId makes every account created with it join that role and takes share on the role.

POST/api/v1/user/claim-codes

Body

NameDescription
expiresInnumberoptionalTime until expiry, in seconds
> 0
tenantIdstringoptionalTenant identifier
roleIdstringoptionalId of the role a redeemed claim code joins — the bare id, not a CRN. Minting a code bound to a role takes share on that role.

Response

NameDescription
codestringA verification code entry

Errors

StatusDescriptionBody
403You do not have permission to perform this action
409A tenant with the given name already existscode: IAM_TENANT_CONFLICT_ERROR
404No account with this id exists in the tenant.code: USER_NOT_FOUND
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
401No valid access token was presented, or the DPoP proof accompanying it was missing, expired or bound to a different key.error: string
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50