Formats and conventions
These rules hold across the whole API. Learn them once and every new endpoint behaves the way you expect.
1. The response envelope
Every response is a ResponseData<T>. A bare array or scalar is never returned.
{
"data": { },
"message": "Success",
"error": null,
"accept": true,
"errors": [],
"requestId": null
}
| Field | Type | Meaning |
|---|---|---|
data | T | null | The payload; null on error |
message | string | "Success" on success, a human-readable message (usually Uzbek) on error. Not always "Success" — see the warning below |
error | object | null | { messageCode, args } when present. Most errors leave it null — see Errors § 3 |
accept | boolean | true on success, false on error |
errors | array | Field-level validation errors; [] on success (null in the filter-produced 401/429) |
requestId | string | null | Set by asynchronous write operations (section 6) |
:::tip Branch on the status, never on text
200 plus accept: true means success. Base your logic on the HTTP status code; treat
error.messageCode as extra detail that is often absent, and never parse message.
:::
:::warning message is not a constant on success
Within 7 days of a token's expiry the platform replaces "Success" on every successful
response with an English warning — "Token will expire in 5 days. Please renew it.". It is a
deliberate nudge, not an error (accept stays true, data is unaffected), and it is another
reason not to compare message against "Success". See
Authentication § 5.1.
:::
Two endpoints step outside the envelope: POST /v1/open-api/oauth/token returns the RFC 6749
token shape directly (its errors still use the envelope), and GET /v1/open-api/address/{uuid}
returns the resolved address object on its own. Everything else — including
POST /v1/open-api/oauth/revoke — is wrapped.
2. Pagination
Paginated responses put a PageableData<T> inside data:
{
"data": {
"totalPages": 27,
"currentPage": 1,
"totalElements": 398,
"data": [ ]
},
"accept": true
}
| Field | Type | Meaning |
|---|---|---|
totalPages | integer | Total number of pages |
currentPage | integer | Current page — 1-based |
totalElements | integer (int64) | Total number of rows |
data | array | The rows on this page |
:::warning Pages start at 1
Many APIs start at 0; this one starts at 1. Sending page: 0 returns 400.
:::
There is no last flag — detect the end with currentPage >= totalPages.
3. Filtering and sorting
Every POST .../filter endpoint accepts the same trio:
| Field | Type | Meaning |
|---|---|---|
page | integer | Page number, min 1. Default 1 |
size | integer | Page size, min 1. The maximum and the default differ per endpoint — see 3.2 |
orders | object | <Domain>OrderField → ASC | DESC |
{
"page": 1,
"size": 100,
"statuses": ["ACTIVE"],
"orders": { "CONTRACT_DATE": "DESC", "ID": "ASC" }
}
Key order in orders is the ORDER BY column order. Above, rows are sorted by
CONTRACT_DATE DESC first, then ID ASC. Keys come from each domain's allow-listed enum
(for contracts: ID, NUMBER, CONTRACT_DATE, AMOUNT, …); anything else returns 400. With
an empty orders, the domain's historical default applies (usually ID DESC).
3.1 Filter semantics
| Rule | Meaning |
|---|---|
null, a missing key and an empty array | All mean "do not filter". An empty array does not mean "match nothing" |
| Values within one field | OR ("statuses": ["ACTIVE","CANCELED"] matches either) |
| Different fields | AND |
Ranges (...From / ...To) | Inclusive on both ends |
| Deleted rows | Excluded unless the filter asks for them (includeDeleted / deletedOnly) |
| Company scope | Everything is scoped to the token's company — you never pass a company id |
3.2 Page size is per endpoint
There is no single global maximum. Sending size: 100 to an endpoint that caps at 50 is a
400 (errors: [{ "field": "size", "reason": "MAX" }]), so read the cap for the endpoint you are
calling:
| Endpoint | size max | Default |
|---|---|---|
POST /v1/open-api/lead/filter | 50 | 10 |
POST /v1/open-api/lead-task/filter | 30 | 15 |
POST /v1/open-api/lead-event/filter | 100 | 20 |
POST /v1/open-api/contract/filter | 100 | 15 |
POST /v1/open-api/flat/filter | 100 | 15 |
POST /v1/open-api/monthly-payment/filter | 100 | 15 |
POST /v1/open-api/payment/filter | 100 | 10 |
POST /v1/open-api/call-history/filter | 100 | 10 |
POST /v1/open-api/employee/filter | 50 | 10 |
GET /v1/open-api/contract/{id}/payment | 50 | 20 |
GET /v1/open-api/contract-payment/{contractId} (deprecated) | — | none: page and size are required, and size must be at least 20 |
Two shapes of pagination input exist: the POST .../filter endpoints take page / size in the
JSON body, while the two contract-payment reads take them as query parameters.
:::note Not every list is paginated
Reference reads return a plain array inside data, with no PageableData and no paging at all:
GET /v1/open-api/pipe/all, /crm-field, /contract-field, /currency, /house/all,
/reason-for-refusal/all, /lead-task/types, /lead-note/{leadId}/list,
/contract/{id}/monthly-payment, the four /address/* endpoints and the deprecated
GET /v1/open-api/employee.
:::
3.3 Stable pagination
To keep rows from being skipped or repeated across pages, the server always appends an internal
primary-key tie-breaker to ORDER BY. You never see it in the response, but it is why paging
stays consistent when a sort column has ties.
For bulk exports, the most reliable approach is to sort by a stable field ({"ID": "ASC"}) and
walk the pages in order.
4. Dates and times
| Concept | Naming | Format | Example |
|---|---|---|---|
| Point in time | *Timestamp, *At | epoch seconds (not milliseconds) | 1766131200 |
| Calendar day | *Date | dd.MM.yyyy | 31.12.2026 |
The two forms are never mixed for the same concept.
:::danger Seconds, not milliseconds
Date.now() in JavaScript returns milliseconds. Divide before sending:
Math.floor(Date.now() / 1000). Otherwise your timestamp lands somewhere in the year 55,000 and
the filter comes back empty.
:::
5. Types and naming
| Type | Rule |
|---|---|
| Enum | UPPER_SNAKE_CASE, serialized by name ("ACTIVE"). Unknown values return 400 |
| Currency | ISO code ("UZS", "USD") |
| Money | A JSON number, unrounded. Use a decimal type in your code, not a binary float |
| Field names | camelCase |
| Absent fields | Some fields are omitted entirely when null — treat "key missing" the same as null |
| Identifiers | id is an integer or int64; leads and contract payments use int64. In JavaScript, keep large ids as strings |
5.1 New enum values are not a breaking change
Enums can gain new values. Always write a default branch and never crash on an unknown value.
Existing value names do not change within v1.
6. Writes are asynchronous
Write endpoints (for example POST /v1/open-api/lead, POST /v1/open-api/payment) accept the
request and queue it. The response carries a requestId instead of data:
{ "data": null, "accept": true, "requestId": "1f0c7a5e-3b62-4a91-8f0d-2e6b9c4a7d13" }
Poll for the outcome:
curl -s https://api.service.app.uysot.uz/v1/open-api/request/1f0c7a5e-3b62-4a91-8f0d-2e6b9c4a7d13 \
-H "X-Open-Api-Token: uysot_pat_..."
{
"accept": true,
"data": {
"requestId": "1f0c7a5e-3b62-4a91-8f0d-2e6b9c4a7d13",
"status": "SUCCESS",
"exception": null,
"createdAt": 1766131200,
"failedAt": null
}
}
status | Meaning |
|---|---|
PENDING | Still being processed — wait a moment and poll again |
SUCCESS | Applied |
FAILED | Failed — see exception for the reason and failedAt for the time |
| Field | Meaning |
|---|---|
requestId | The id you polled |
status | PENDING | SUCCESS | FAILED |
exception | Why it failed; null otherwise |
createdAt | When the request was accepted, epoch seconds |
failedAt | When it failed, epoch seconds; null otherwise |
:::note Statuses live for 24 hours
A request status is kept for 24 hours after it was accepted, then disappears — polling it
later returns 404. That is a generous window, but it is not a job history: read the outcome soon
after the write, and don't poll every 100 ms (each poll spends
rate limit budget).
:::
:::danger A requestId belongs to the token that created it
The status is readable only with the exact same token value. Any other token — including a
refreshed OAuth access token for the same company, or a newly issued PAT for the same
application — gets a plain 404, indistinguishable from an expired status. So finish polling
before you rotate or refresh the token, and never hand a requestId to a different worker
holding a different token.
:::
:::warning An accepted write is not a successful write
200 + requestId means "validated and queued". Business failures — an unknown leadId, an
employee that doesn't exist, a value the CRM rejects — surface only as status: FAILED on the
requestId. Code that ignores the requestId will silently lose writes.
:::
7. Versioning and compatibility
What v1 guarantees:
- Fields may be added — ignore unknown fields rather than failing on them.
- Enums may gain new values.
- Existing field names are never renamed or removed; deprecated ones keep working.
- Breaking changes ship under
/v2.
So keep your JSON parser non-strict: an unfamiliar field must not break your client.
7.1 How a deprecation is signalled
An endpoint on its way out is marked in three places, and it keeps working the whole time:
- its reference page says Deprecated and names the replacement;
- every response carries
Deprecation: trueand, when a removal date is set,Sunset: <HTTP-date>(RFC 8594) — for exampleSunset: Fri, 12 Feb 2027 00:00:00 GMTonGET /v1/open-api/contract-payment/{contractId}; - a deprecated field stays in the payload and keeps being filled until
/v2.
Log the Sunset header when you see one — that date is your migration deadline, and it is the
only place the API states it. Currently deprecated: GET /v1/open-api/employee (use
POST /v1/open-api/employee/filter), GET /v1/open-api/contract-payment/{contractId} (use
GET /v1/open-api/contract/{id}/payment) and the misspelled GET /v1/open-api/contrac-field
alias.