REST API Reference

What This Covers

This reference documents the version 1 REST API surface exposed under /api/v1/:

  • authentication and key lifecycle
  • generic collection CRUD
  • special collections for tags and notifications
  • command-style endpoints under /api/v1/app/...
  • file upload promises

Use REST API first if you want the conceptual overview. Use this page when you need concrete routes, request shapes, and response patterns.

Base Path And Shared Rules

The REST API is exposed under the tenant host at:

/api/v1/

The router accepts these methods:

  • GET
  • POST
  • HEAD
  • PUT
  • DELETE
  • PATCH
  • SEARCH
  • OPTIONS

Authentication Header

Authenticated endpoints accept the API key in either header:

  • key
  • x-api-key

The acting identity is the User that owns the API key. Permissions are then evaluated through that user context.

Request Body Conventions

The implementation uses three request styles:

  • JSON request bodies for most collection and command endpoints
  • regular POST parameters for /auth and /deauth
  • multipart/form-data with field name file for uploads

For browser-based clients, the presenter also handles OPTIONS preflight requests and allows the headers Content-Type, Authorization, key, x-api-key, and x-custom-auth-headers.

Datetime Conventions

Storage is UTC end-to-end. The API accepts and emits ISO 8601 with explicit timezone offset as the canonical shape:

2026-05-22T09:00:00+02:00    sent in Prague summer wall-clock (request)
2026-05-22T07:00:00Z         same instant in UTC (request)
2026-05-22T09:00:00+02:00    same instant, response shape for a Prague-tenant user

Requests. Send datetime values with an explicit offset. Naive datetime strings (2026-05-22 09:00:00) are interpreted in the acting user's display timezone (per-user timezone field if set, otherwise tenant defaultTimeZone).

Responses. Datetime values come back in the acting user's display timezone with the offset attached (e.g. 2026-05-22T09:00:00+02:00). The instant is identical to what was stored; the offset reflects the API key user's tz, so two different users may see different offsets for the same record. Parse with a tolerant ISO 8601 parser — never assume a specific offset string.

pDate and pMonth fields are calendar values without time-of-day and without timezone semantics: send and receive YYYY-MM-DD (or YYYY-MM). Any time/offset on input is discarded.

The full rules and rationale are in REST API → Timezones.

Common Response Patterns

Generic collection list endpoints return:

{
  "stats": {
    "page": 1,
    "itemsPerPage": 25,
    "fetched": 25,
    "total": 143
  },
  "data": []
}

Generic collection single-item create, read, update, and patch operations return one resource object.

Common Status Behavior

The API uses these status patterns consistently:

  • 200 OK for successful reads and updates
  • 201 Created for successful creates
  • 204 No Content for empty result sets and deletes
  • 304 Not Modified for cached GET/SEARCH responses where the client's If-None-Match matches the current ETag
  • 400 Bad Request for malformed input or unsupported parameter combinations
  • 401 Unauthorized mainly for failed credential-based /auth requests
  • 403 Forbidden for missing permissions and, in practice, many missing or invalid API-key cases
  • 404 Not Found when a collection, item, or command target does not exist
  • 409 Conflict when a write would violate a unique constraint, for example registering a user whose e-mail is already taken
  • 429 Too Many Requests when API rate limits are reached
  • 500 Internal Server Error for processing failures
  • 501 Not Implemented for reserved but unsupported routes

Rely on the HTTP status first. Some error responses also carry a JSON body with a message.

Response Caching Headers

Successful GET and SEARCH responses on the generic collection API carry these headers:

HeaderMeaning
ETagStrong validator over the response body, formatted as "<hex>". Always emitted when the response was eligible for caching, regardless of cache hit/miss.
Cache-Control: private, must-revalidateTells intermediate caches to keep the response per-user and to revalidate before reuse.

Clients can opt into conditional requests by sending the previously received ETag back in If-None-Match. The server responds 304 Not Modified with an empty body when the value still matches.

To bypass the cache for a single request, send either:

  • Cache-Control: no-cache request header
  • ?_noCache=1 query parameter

Cache-eligible request shapes:

  • GET /api/v1/collection/{collection} (list)
  • SEARCH /api/v1/collection/{collection} (list with body filters)
  • GET /api/v1/collection/{collection}/{item} (get one)
  • GET /api/v1/collection/{collection}/get-by/{property}/{item} (get one by property)

Single-resource fetches are not cached for types that have read-access logging enabled — those reads must always reach the database so the audit log fires.

The cache is keyed per authenticated user and effective permission scope; entries are invalidated automatically when the underlying resource is created, updated, or deleted through the platform.

Route Summary

RoutePurpose
POST /api/v1/authCreate a new API key from user credentials
GET /api/v1/authValidate the API key sent in request headers
POST /api/v1/deauthRevoke an API key
`GETSEARCH /api/v1/collection/{collection}`
GET /api/v1/collection/{collection}/{item}Get one resource by ID or UUID
GET /api/v1/collection/{collection}/get-by/{property}/{item}Get the first matching resource by alternate property
POST /api/v1/collection/{collection}Create a resource
`PUTPATCH
POST /api/v1/upload/{typeId}/{propertyId}Upload a file and receive promise IDs
/api/v1/app/{module}.{action}Execute command-style module endpoints

Authentication

POST /api/v1/auth

Creates a new API key from user credentials.

Use this when an integration needs its own long-lived API key tied to a dedicated application user.

Request parameters are read as normal POST fields:

FieldRequiredMeaning
useryesUser login name
passwordyesUser password
validnoExpiration date, default 9999-12-31
descriptionnoKey description

Response shape:

{
  "authToken": {
    "id": 12,
    "userId": 34,
    "keyString": "...",
    "validFrom": "...",
    "validTo": "...",
    "description": "Login via API",
    "permissions": []
  }
}

Notes:

  • the user must be allowed to use the REST API
  • authentication failures return 401
  • login-attempt throttling can also return 429

GET /api/v1/auth

Validates the API key sent in the key or x-api-key header.

Response shape:

{
  "status": "valid",
  "validTo": "2026-12-31 00:00:00"
}

If the key is invalid, status is invalid and validTo is an empty string.

POST /api/v1/deauth

Revokes an API key.

Request parameters are read as normal POST fields:

FieldRequiredMeaning
keyyesAPI key string to revoke

Successful response:

{
  "message": "API key deleted."
}

Generic Collection Endpoints

The generic collection API works against a type. The {collection} placeholder accepts the type ID or type system name.

Where an {item} path segment is used, the implementation accepts a numeric ID and also resolves UUID values.

List Resources

GET    /api/v1/collection/{collection}
SEARCH /api/v1/collection/{collection}

Supported query parameters:

ParameterMeaning
pagePage number, defaults to 1
perPagePage size, capped by the tenant API max page length
orderBySort property
orderDirasc or desc
propsMetaInclude property metadata for selected fields
showFieldsExplicit field allowlist
hideFieldsExplicit field denylist
wrapFieldsWrap selected fields in metadata-rich output
tagsTag filter, as array or comma-separated IDs
tagsModeany or all
includeTagDescendantsWhether descendant tags are included
filtersArray of typed filter definitions

Filter payloads follow the platform's standard filter definition model. At minimum, each filter object must identify:

  • property
  • type

Recommended shape:

{
  "filters": [
    {
      "property": "status",
      "type": "in",
      "param": ["open", "in_progress"]
    }
  ]
}

For query-string usage, filters is expected as a JSON-encoded array. In practice, SEARCH with a JSON body is the cleaner option for complex filters.

Use SEARCH when you want a search-style list request with a request body. Do not combine SEARCH with:

  • /get-by/...
  • an {item} path

Get One Resource

GET /api/v1/collection/{collection}/{item}

Returns one resource object.

Get One Resource By Alternate Property

GET /api/v1/collection/{collection}/get-by/{property}/{item}

Returns the first resource matching the property value. This route does not accept filter definitions.

Create A Resource

POST /api/v1/collection/{collection}

Send a JSON body using property identifiers as keys.

Notes:

  • id and uuid are ignored on input
  • file properties accept upload promise IDs
  • sending null to a file property erases the current value

Successful create returns 201 Created and the created resource object.

Replace A Resource

PUT /api/v1/collection/{collection}/{item}

Sends a full replacement update. The API validates and saves the resource, then returns the resulting resource object.

Patch A Resource

PATCH /api/v1/collection/{collection}/{item}

Partial updates support normal field replacement and some additive operators:

  • field+= appends to strings, adds to numbers, or merges arrays
  • field+== adds only missing values to arrays

For multiple file properties, patch requests also support:

{
  "attachments": {
    "add": ["promise-id-1"],
    "remove": ["existing-file-name.ext"]
  }
}

Delete A Resource

DELETE /api/v1/collection/{collection}/{item}

Deletes the resource and returns 204 No Content.

Special Collections

Tags Collection

The tags collection is implemented specially rather than through generic typed-object CRUD.

List Tags

GET    /api/v1/collection/tags
SEARCH /api/v1/collection/tags

Supported parameters:

ParameterMeaning
pagePage number
perPagePage size
searchText search
parentParent tag ID
showFieldsExplicit field allowlist
hideFieldsExplicit field denylist

Any authenticated user can list tags. Users with manageTags receive broader visibility, including full tag information and private-tag access.

Response shape follows the generic list format with stats and data.

Get One Tag

GET /api/v1/collection/tags/{item}

Returns one tag object. The tag identifier is resolved through the tag manager, so integrations should treat it as the platform tag identifier rather than only a numeric ID.

Create, Update, And Delete Tags

POST        /api/v1/collection/tags
PUT|PATCH   /api/v1/collection/tags/{item}
DELETE      /api/v1/collection/tags/{item}

Write access requires manageTags or createPrivateTags.

If the caller has createPrivateTags but not manageTags, created tags are forced to private.

Notifications Collection

The notifications collection is also implemented specially.

List Notifications

GET /api/v1/collection/notifications

Supported query parameters:

ParameterMeaning
unread_onlySet to "true" to limit to unread notifications
limitMaximum number of returned items, default 25
cursorCursor for pagination
afterReturn items after the given point
recipient_custom_idLook up notifications for a custom recipient identifier

Access requires either:

  • system permission notifications
  • system permission getNotificationsPerCustomId

Successful response:

{
  "items": [],
  "nextCursor": null,
  "hasMore": false
}

If the result set is empty, the endpoint returns 204.

Get One Notification

GET /api/v1/collection/notifications/{item}

Returns one notification object.

Patch A Notification

PATCH /api/v1/collection/notifications/{item}

Supported patch operations:

  • { "read": true } marks the notification as read and returns the updated notification
  • { "archive": true } archives the notification and returns { "message": "Notification archived." }

Other notification write operations are rejected:

  • POST is not supported
  • PUT is not supported
  • DELETE is not supported

Command Endpoints

Command endpoints live under:

/api/v1/app/{module}.{action}

backend.version

GET /api/v1/app/backend.version

Returns backend version information for users allowed to use the REST API.

Response shape:

{
  "id": 123,
  "code": "2026.04",
  "notes": "...",
  "release_time": "2026-04-01 10:00:00"
}

user.current

GET /api/v1/app/user.current

Returns the currently authenticated user as a processed resource object.

Requires the system permission profile.

user.register

GET  /api/v1/app/user.register
POST /api/v1/app/user.register

Requires the system permission apiUserRegistration.

GET user.register

Returns the allowed registration envelope configured for the tenant:

{
  "allowedDomains": [],
  "allowedRoles": [],
  "allowedGroups": []
}

POST user.register

Creates a user account.

Minimum required fields from the implementation:

FieldRequiredMeaning
user_idyesUser e-mail address
user_nameyesDisplay name

Supported registration-related fields visible in the implementation:

  • roles
  • user_group
  • password
  • verify_account — see below

verify_account

When verify_account is included in the request body, the API:

  1. Creates the user but blocks sign-in until verification completes (login_expired = 1, valid_from pushed to year 9999).
  2. Generates a verification challenge composed of one or more methods.
  3. Sends each method's notification immediately (e-mail and/or SMS).
  4. Returns the challenge identifier on the response under challenge so the integration can correlate later steps.
  5. Once the user satisfies every method on the challenge through user.challenge, the platform clears the login block, activates the account, and signs the user in on the same session that completed the last method.

If verify_account is omitted entirely, the API substitutes the default { "mail_link": {} } — a single e-mail link with default settings. To create an unverified account that the integration will activate through some other path, send an empty object ("verify_account": {}); the user is created with sign-in disabled but no notification is dispatched.

Top-level fields

FieldRequiredMeaning
titlenoDisplay title for the verification UI. Defaults to a localized "User account verification" string.
redirect_urlnoAbsolute URL the user should be forwarded to once the challenge fully completes. Must be a valid URL (rejected with 400 otherwise). The platform appends ?status=success (or &status=success if the URL already carries a query) before redirecting. See Redirect after completion below.
mail_linkone of the threeConfigure the e-mail link method.
mail_codeone of the threeConfigure the e-mail code method.
sms_codeone of the threeConfigure the SMS code method.

The three method blocks may be combined freely. Every method on the challenge must be satisfied for the account to activate, so requesting both mail_code and sms_code is effectively two-factor verification.

Redirect after completion

There are two redirect slots:

  • Per-method redirect_url — declared inside an individual method block (mail_link, mail_code, sms_code). Fires after that method is verified but the challenge is not yet fully complete. Useful for stepping the user through a multi-method flow (e.g. send them to a "now enter the SMS code" page after they click the e-mail link).
  • Challenge-level redirect_url — declared at the top of verify_account. Fires when the challenge fully completes (every configured method satisfied).

Resolution rules when a method is verified:

  • If the method is verified but the challenge is not yet complete → use that method's redirect_url.
  • If the method completes the challenge → use the challenge-level redirect_url. If none is configured, fall back to the just-completed method's redirect_url. When both are configured, the challenge-level URL wins — this matters for single-method challenges, where the only method's verification is also the final completion.
  • If verification fails or the method is already done → no redirect.

Both URLs are consumed the same way:

  • Web flow — the platform issues a 302 to the resolved URL with status=success appended.
  • API flow — the user.challenge response carries a redirect field with the resolved URL and status=success appended. The integration navigates the user; the API itself never returns a redirect status.

When an integration redirect URL is configured, the platform does not automatically log the user into the platform UI on completion — the integration owns the post-verification session in that case. When no redirect URL is configured, the legacy behaviour applies: the e-mail-link web flow logs the user in and forwards them to Homepage with a success flash. If you want the user signed into the platform after verification, omit the redirect URL; if you want them sent to your own app, set it.

URLs are stored server-side on the challenge struct and are never embedded in the e-mailed link. Validation: every URL slot must pass Validators::isUrl() at registration time, otherwise user.register rejects with 400.

Method: mail_link

Sends a one-time activation link by e-mail. The user clicks it; the link routes to user.challenge server-side and verifies the method automatically.

OptionDefaultMeaning
validity+24 hoursValidity window, expressed as a relative datetime modifier (anything \\DateTime accepts: +15 minutes, +2 hours, +7 days).
redirect_urlnoneAbsolute URL to navigate to once this method is verified but the challenge is not yet fully complete. See Redirect after completion for the resolution rules.

Method: mail_code

Sends a code by e-mail. The user reads the code and submits it through the integration UI, which calls user.challenge with the value.

OptionDefaultMeaning
length6Number of characters in the generated code.
charlist0-9Character set for code generation, in the format accepted by the random generation method (0-9, 0-9A-Z, a-zA-Z0-9, etc.).
validity+24 hoursValidity window.
redirect_urlnoneAbsolute URL to navigate to once this method is verified but the challenge is not yet fully complete. See Redirect after completion.

Method: sms_code

Sends a code by SMS. The phone field on the request body is mandatory when sms_code is requested — otherwise the registration call rejects with 400 before the user record is created (no orphaned user, no notifications dispatched).

OptionDefaultMeaning
length6Number of characters in the code.
charlist0-9Character set, same format as mail_code.
validity+24 hoursValidity window.
send_linkfalseWhen true, the SMS includes a clickable verification link in addition to the code so the user can tap rather than type.
messagelocalized defaultCustom SMS body. Supports placeholders %tenant%, %code%, %link%, %validity%. When omitted, a localized default text is used.
redirect_urlnoneAbsolute URL to navigate to once this method is verified but the challenge is not yet fully complete. See Redirect after completion.

Examples

E-mail link only — equivalent to omitting verify_account:

{
  "verify_account": {
    "mail_link": {}
  }
}

E-mail code with custom expiry and an alphanumeric character set:

{
  "verify_account": {
    "title": "Confirm your e-mail",
    "mail_code": {
      "length": 8,
      "charlist": "0-9A-Z",
      "validity": "+15 minutes"
    }
  }
}

Two-factor verification — the user must complete both an e-mail code and an SMS code before the account activates:

{
  "verify_account": {
    "mail_code": { "length": 6, "validity": "+30 minutes" },
    "sms_code": {
      "length": 6,
      "validity": "+10 minutes",
      "message": "%tenant%: your verification code is %code% (valid for %validity%)."
    }
  }
}

Multi-step flow with intermediate and final navigation — the user is sent to a "now enter the SMS code" page after the e-mail link is clicked, and to the post-verification dashboard once the SMS code is also accepted:

{
  "verify_account": {
    "mail_link": {
      "validity": "+1 hour",
      "redirect_url": "https://app.example.com/verify/sms"
    },
    "sms_code": {
      "length": 6,
      "validity": "+10 minutes"
    },
    "redirect_url": "https://app.example.com/welcome"
  }
}

The challenge identifier returned in the challenge field of the registration response is the input to user.challenge for verification and to user.retrychallenge for re-sending the notification.

Successful response shape:

{
  "object": {},
  "apiKey": null,
  "challenge": false
}

Notes:

  • apiKey is returned only when the created user can use the API and the request included credentials needed for default key creation
  • requested domains, roles, and groups are checked against tenant allowlists

Error responses:

  • 400 — required fields are missing, user_id is not a valid e-mail address, verify_account.redirect_url or any verify_account.{method}.redirect_url is present but not a valid URL, or verify_account.sms_code is requested but no phone value was supplied

  • 403 — caller lacks apiUserRegistration, the e-mail's domain is not on the tenant allowlist, or a requested role or group is not on the tenant allowlist

  • 409 — the e-mail is already registered. The response body identifies the conflicting property:

    {
      "message": "Value 'person@example.com' for property E-mail already used."
    }
    

    Use this status to differentiate "user exists, recover the verification" from "user does not exist, retry the create". A 409 from user.register is the typical signal to fall back to user.retrychallenge for a returning user whose original challenge was lost — or to user.cancelregistration if the integration wants to abandon the pending registration and start over.

user.cancelregistration

POST /api/v1/app/user.cancelregistration

Cancels a pending registration by deleting the user. Use this when an integration started a user.register flow but the user did not complete the verification challenge — for example because the user abandoned the flow, registered to the wrong tenant, or the integration wants to revert before its own onboarding fails.

Cancellation is only permitted between registration initiation and challenge completion. Once the verification challenge has fully completed (account is "valid"), this endpoint refuses with 409. Use the regular DELETE /collection/User/{id} flow with appropriate permissions to remove a verified user.

Requires the system permission apiUserRegistration — the same permission used for user.register.

The pending user normally has no useful permissions of their own (they are blocked from signing in until verification completes), so the integration cannot delete them through the regular collection API on the user's behalf. This endpoint performs the deletion under elevated authority on the integration's behalf, gated by:

  1. The integration's apiUserRegistration permission (header x-api-key).
  2. A conditional per-user double-check on the request body's apiKey:
    • If the user has an API key (i.e. user.register returned an apiKey because the user has useApi permission and was registered with a password) the request body must include that apiKey and it must belong to this user. This prevents a compromised integration key alone from deleting arbitrary pending users that hold their own API key.
    • If the user has no API key, the body's apiKey field can be omitted. The integration's permission alone is treated as sufficient — a user without any API key could not have authenticated to the platform anyway, so the threat surface for accidental or malicious cancellation is smaller.

Request body:

FieldRequiredMeaning
userIdyesNumeric ID of the user to cancel
apiKeyconditionalThe user's API key as returned in the apiKey field of the original user.register response. Required when the user has any API key issued; can be omitted otherwise.
{
  "userId": 123,
  "apiKey": "8duqxsiv6yizhgnz3dugkbyo0dghed"
}

Successful response:

{
  "message": "Registration cancelled and user deleted.",
  "userId": 123
}

The user record is deleted, then all of their API keys are revoked.

Error responses:

  • 400userId is missing, or the user has an issued API key but apiKey was not supplied
  • 403 — caller lacks apiUserRegistration, or the supplied apiKey does not belong to the specified userId
  • 404 — user not found
  • 409 — registration is already completed (no outstanding verification challenge); cancel through the regular delete flow instead
  • 500 — the deletion failed at the storage layer (the call is safe to retry; no API keys were revoked)

user.challenge

POST /api/v1/app/user.challenge

Processes an account-verification challenge.

This action does not apply a separate permission gate in the REST module. The challenge payload itself is the control point.

Request body:

{
  "userId": 123,
  "challengeId": "abc",
  "method": "mail_link",
  "secret": "..."
}

Possible responses:

{ "methodDone": true, "challengeCompleted": true }
{ "methodDone": true, "challengeCompleted": false }
{ "methodDone": false, "challengeCompleted": false }
{ "message": "Method already done." }

Whenever a method is successfully verified — whether the challenge as a whole is now complete or only this single step is — the response carries a redirect field if a redirect URL applies. The URL is resolved server-side (per-method vs. challenge-level) and is returned with status=success already appended:

{
  "methodDone": true,
  "challengeCompleted": false,
  "redirect": "https://app.example.com/verify/sms?status=success"
}
{
  "methodDone": true,
  "challengeCompleted": true,
  "redirect": "https://app.example.com/welcome?status=success"
}

The integration should navigate the user to that URL. The API itself never returns an HTTP redirect — the URL is delivered as data so the integration's own client code stays in control of the navigation. When neither a per-method nor a challenge-level URL applies, the redirect field is omitted.

See also user.retrychallenge when the original notification needs to be re-sent because it did not arrive or expired.

user.retrychallenge

POST /api/v1/app/user.retrychallenge

Re-sends the notification for an outstanding account-verification challenge. Use this when:

  • the original e-mail or SMS did not reach the user
  • the link or code expired before the user could act on it
  • the user explicitly asks the integration for a fresh code
  • the user returns from a closed tab or a different device and the original challengeId is no longer available to the integration

Each retry rotates the underlying secret. The previously delivered link or code is invalidated and stops working as soon as the retry is processed, so a leaked or stale value cannot be combined with the new one. Methods that the user has already verified are skipped — only outstanding methods are re-sent.

Requires the system permission apiUserRegistration — the same permission used for user.register.

The endpoint operates in two modes depending on what the caller can supply:

  • Targeted retry — the caller has the challengeId from the original user.register response. The endpoint reports precise outcomes (404 for missing user/challenge/method, 400 for cooldown or cap).
  • Recovery retry — the caller has only the user identifier. The endpoint locates the user's outstanding registration challenge automatically and replies with a generic message regardless of outcome, so it cannot be used as a user-existence oracle. This is the path to use from browser-side flows where the original challengeId was lost.

Request body

FieldRequiredMeaning
userIdone of userId / userEmailNumeric ID of the user that owns the challenge
userEmailone of userId / userEmailE-mail address of the user, used when the numeric ID is not available client-side
challengeIdnoChallenge identifier originally returned by user.register. When omitted, the endpoint targets the user's outstanding registration challenge and switches to the opaque response shape described below.
methodnoLimit retry to a single method (mail_link, mail_code, or sms_code). Omit to retry every outstanding method on the challenge.

Targeted retry

Used when the integration has the challengeId.

{
  "userId": 123,
  "challengeId": "abc",
  "method": "mail_link"
}

Successful response:

{
  "challengeId": "abc",
  "retried": ["mail_link"],
  "skipped": []
}

The retried array lists the method identifiers whose secrets were regenerated and whose notifications were dispatched. The skipped array lists methods that were already verified and therefore not re-sent.

Error responses for targeted retry:

  • 400 — neither userId nor userEmail was supplied
  • 400 — retry cooldown is active (see Cooldown and lifetime cap below)
  • 400 — lifetime retry cap has been reached for this challenge
  • 403 — caller lacks apiUserRegistration
  • 404 — user, challenge, or (when method is supplied) the requested method does not exist on the challenge

Recovery retry

Used when the integration only has the user's identifier (typically the e-mail entered in the verification UI). The endpoint resolves the outstanding registration challenge for that user and re-sends its outstanding methods. Cooldown and cap still apply server-side; the response does not signal which case applied.

{
  "userEmail": "person@example.com"
}

Response — always the same shape and HTTP status, regardless of whether the user exists, has an outstanding registration challenge, hit the cooldown, or was retried successfully:

{
  "message": "If a pending verification exists, a new code has been sent."
}

Integrations should treat this response as fire-and-forget. The user-facing UI typically says "Check your inbox" and lets the user attempt verification on the next code that arrives. If verification still fails after a reasonable period, fall back to a non-API recovery path (operator support, a manually triggered reset).

Cooldown and lifetime cap

Two server-side controls bound abuse:

  • Cooldown — a minimum interval between successful retries on the same challenge (60 seconds by default). Calls inside the window are no-ops; in targeted mode they return a 400 with Retry cooldown active., in recovery mode they return the generic message.
  • Lifetime cap — a maximum number of retries per challenge (10 by default). Once reached, further retries on that challenge are rejected; in targeted mode with a 400 carrying Retry limit reached for this challenge., in recovery mode silently. The user must then complete verification with a previously delivered code or be recovered through an alternative path.

These controls are evaluated per challenge, not per API key, so rotating keys or alternating methods does not bypass them.

user.resetpassword

POST /api/v1/app/user.resetpassword

Requires:

  • system permission apiUserPasswordReset
  • tenant setting allowApiPasswordReset

Request body:

FieldRequiredMeaning
user_idyesE-mail of the user whose password is being reset.
redirect_urlnoAbsolute URL the user should be forwarded to once the password-reset flow inside the platform completes. The platform appends status=success (on success) or status=error&reason=... (on failure) to it. The URL is encrypted with the tenant's security key and embedded in the e-mailed link so it cannot be tampered with.
{
  "user_id": "person@example.com",
  "redirect_url": "https://app.example.com/after-reset"
}

Successful response:

{
  "message": "Password reset initiated. E-mail sent to the user."
}

Validation errors:

  • 400redirect_url is present but not a valid URL.

tags.object

GET /api/v1/app/tags.object?type={type}&item={item}

Returns the tags currently assigned to one object.

Required query parameters:

ParameterMeaning
typeType ID or system name
itemObject ID or UUID

The caller must have show permission on the target type.

Successful response:

{
  "typeId": 1000002,
  "objectId": 15,
  "tags": []
}

tags.assign

POST /api/v1/app/tags.assign

Assigns tags to an object.

The caller must have edit permission on the target type.

Request body:

{
  "type": "project",
  "item": 15,
  "operation": "replace",
  "tags": [12, 18]
}

Supported operations:

  • add
  • remove
  • replace

Successful response:

{
  "typeId": 1000002,
  "objectId": 15,
  "operation": "replace",
  "tags": []
}

File Uploads

POST /api/v1/upload/{typeId}/{propertyId}

Uploads one or more files for a file property and returns promise IDs.

Request requirements:

  • send multipart/form-data
  • use field name file
  • the target property must be a file property

Example response:

[
  "promise-id-1",
  "promise-id-2"
]

Those promise IDs are then used as field values in subsequent create or update requests for the target resource.

Reserved Route

The router exposes:

/api/v1/collection/{collection}/{item}/relation/{relationType}/{relationRole}

The current implementation returns 501 Not Implemented.