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:
GETPOSTHEADPUTDELETEPATCHSEARCHOPTIONS
Authentication Header
Authenticated endpoints accept the API key in either header:
keyx-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
/authand/deauth multipart/form-datawith field namefilefor 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 OKfor successful reads and updates201 Createdfor successful creates204 No Contentfor empty result sets and deletes304 Not Modifiedfor cachedGET/SEARCHresponses where the client'sIf-None-Matchmatches the current ETag400 Bad Requestfor malformed input or unsupported parameter combinations401 Unauthorizedmainly for failed credential-based/authrequests403 Forbiddenfor missing permissions and, in practice, many missing or invalid API-key cases404 Not Foundwhen a collection, item, or command target does not exist409 Conflictwhen a write would violate a unique constraint, for example registering a user whose e-mail is already taken429 Too Many Requestswhen API rate limits are reached500 Internal Server Errorfor processing failures501 Not Implementedfor 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:
| Header | Meaning |
|---|---|
ETag | Strong 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-revalidate | Tells 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-cacherequest header?_noCache=1query 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
| Route | Purpose |
|---|---|
POST /api/v1/auth | Create a new API key from user credentials |
GET /api/v1/auth | Validate the API key sent in request headers |
POST /api/v1/deauth | Revoke an API key |
| `GET | SEARCH /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 |
| `PUT | PATCH |
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:
| Field | Required | Meaning |
|---|---|---|
user | yes | User login name |
password | yes | User password |
valid | no | Expiration date, default 9999-12-31 |
description | no | Key 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:
| Field | Required | Meaning |
|---|---|---|
key | yes | API 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:
| Parameter | Meaning |
|---|---|
page | Page number, defaults to 1 |
perPage | Page size, capped by the tenant API max page length |
orderBy | Sort property |
orderDir | asc or desc |
propsMeta | Include property metadata for selected fields |
showFields | Explicit field allowlist |
hideFields | Explicit field denylist |
wrapFields | Wrap selected fields in metadata-rich output |
tags | Tag filter, as array or comma-separated IDs |
tagsMode | any or all |
includeTagDescendants | Whether descendant tags are included |
filters | Array of typed filter definitions |
Filter payloads follow the platform's standard filter definition model. At minimum, each filter object must identify:
propertytype
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:
idanduuidare ignored on input- file properties accept upload promise IDs
- sending
nullto 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 arraysfield+==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:
| Parameter | Meaning |
|---|---|
page | Page number |
perPage | Page size |
search | Text search |
parent | Parent tag ID |
showFields | Explicit field allowlist |
hideFields | Explicit 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:
| Parameter | Meaning |
|---|---|
unread_only | Set to "true" to limit to unread notifications |
limit | Maximum number of returned items, default 25 |
cursor | Cursor for pagination |
after | Return items after the given point |
recipient_custom_id | Look 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:
POSTis not supportedPUTis not supportedDELETEis 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:
| Field | Required | Meaning |
|---|---|---|
user_id | yes | User e-mail address |
user_name | yes | Display name |
Supported registration-related fields visible in the implementation:
rolesuser_grouppasswordverify_account— see below
verify_account
When verify_account is included in the request body, the API:
- Creates the user but blocks sign-in until verification completes (
login_expired = 1,valid_frompushed to year 9999). - Generates a verification challenge composed of one or more methods.
- Sends each method's notification immediately (e-mail and/or SMS).
- Returns the challenge identifier on the response under
challengeso the integration can correlate later steps. - 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
| Field | Required | Meaning |
|---|---|---|
title | no | Display title for the verification UI. Defaults to a localized "User account verification" string. |
redirect_url | no | Absolute 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_link | one of the three | Configure the e-mail link method. |
mail_code | one of the three | Configure the e-mail code method. |
sms_code | one of the three | Configure 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 ofverify_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'sredirect_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
302to the resolved URL withstatus=successappended. - API flow — the
user.challengeresponse carries aredirectfield with the resolved URL andstatus=successappended. 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.
| Option | Default | Meaning |
|---|---|---|
validity | +24 hours | Validity window, expressed as a relative datetime modifier (anything \\DateTime accepts: +15 minutes, +2 hours, +7 days). |
redirect_url | none | Absolute 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.
| Option | Default | Meaning |
|---|---|---|
length | 6 | Number of characters in the generated code. |
charlist | 0-9 | Character 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 hours | Validity window. |
redirect_url | none | Absolute 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).
| Option | Default | Meaning |
|---|---|---|
length | 6 | Number of characters in the code. |
charlist | 0-9 | Character set, same format as mail_code. |
validity | +24 hours | Validity window. |
send_link | false | When true, the SMS includes a clickable verification link in addition to the code so the user can tap rather than type. |
message | localized default | Custom SMS body. Supports placeholders %tenant%, %code%, %link%, %validity%. When omitted, a localized default text is used. |
redirect_url | none | Absolute 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:
apiKeyis 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_idis not a valid e-mail address,verify_account.redirect_urlor anyverify_account.{method}.redirect_urlis present but not a valid URL, orverify_account.sms_codeis requested but nophonevalue was supplied -
403— caller lacksapiUserRegistration, 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
409fromuser.registeris the typical signal to fall back touser.retrychallengefor a returning user whose original challenge was lost — or touser.cancelregistrationif 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:
- The integration's
apiUserRegistrationpermission (headerx-api-key). - A conditional per-user double-check on the request body's
apiKey:- If the user has an API key (i.e.
user.registerreturned anapiKeybecause the user hasuseApipermission and was registered with a password) the request body must include thatapiKeyand 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
apiKeyfield 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.
- If the user has an API key (i.e.
Request body:
| Field | Required | Meaning |
|---|---|---|
userId | yes | Numeric ID of the user to cancel |
apiKey | conditional | The 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:
400—userIdis missing, or the user has an issued API key butapiKeywas not supplied403— caller lacksapiUserRegistration, or the suppliedapiKeydoes not belong to the specifieduserId404— user not found409— registration is already completed (no outstanding verification challenge); cancel through the regular delete flow instead500— 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
challengeIdis 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
challengeIdfrom the originaluser.registerresponse. 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
challengeIdwas lost.
Request body
| Field | Required | Meaning |
|---|---|---|
userId | one of userId / userEmail | Numeric ID of the user that owns the challenge |
userEmail | one of userId / userEmail | E-mail address of the user, used when the numeric ID is not available client-side |
challengeId | no | Challenge 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. |
method | no | Limit 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— neitheruserIdnoruserEmailwas supplied400— retry cooldown is active (see Cooldown and lifetime cap below)400— lifetime retry cap has been reached for this challenge403— caller lacksapiUserRegistration404— user, challenge, or (whenmethodis 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
400withRetry 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
400carryingRetry 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:
| Field | Required | Meaning |
|---|---|---|
user_id | yes | E-mail of the user whose password is being reset. |
redirect_url | no | Absolute 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:
400—redirect_urlis 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:
| Parameter | Meaning |
|---|---|
type | Type ID or system name |
item | Object 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:
addremovereplace
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.