OAuth 2.0 — authorization code flow
Use OAuth when your app is used by more than one UYSOT company — a product you distribute, not a script for your own company. Each company connects your app once, approves what it may do, and your backend receives its own tokens for that company.
If you are integrating only with your own company's data, you don't need any of this — use a personal access token (Authentication).
On this page:
- Before you start
- The flow at a glance
- Step 1 — send the user to the consent page
- Step 2 — handle the redirect back
- Step 3 — exchange the code for tokens
- Step 4 — call the API
- Refreshing an access token
- Revoking a token
- Errors
- Security checklist
- FAQ
1. Before you start
Register your application with UYSOT. You receive:
| Item | Note |
|---|---|
client_id | Public identifier, uysot_app_… |
client_secret | uysot_sec_… — shown once. Backend only |
| Registered redirect URIs | Every redirect_uri you will use, listed exactly |
| Allowed grants | The maximum set of permissions your app may ever request |
| The authorization page URL | Where you send company users to give consent |
:::info Ask for what you use The grants you register are the ceiling; the grants you request per connection are what the company sees on the consent screen. Requesting less gets approved more often. :::
Companies can connect only once your application is published.
:::warning The OAuth endpoints use a different host
POST /v1/open-api/oauth/token and POST /v1/open-api/oauth/revoke are served from
https://service.app.uysot.uz. Every other endpoint — leads, contracts, call history,
token/info, … — stays on https://api.service.app.uysot.uz.
:::
2. The flow at a glance
Two of these steps are yours to implement: the redirect handler and the token exchange. Everything between them happens in the user's browser, on UYSOT's side.
3. Step 1 — send the user to the consent page
Put a "Connect UYSOT" button in your app that opens the UYSOT authorization page with your parameters as a query string:
https://crm.uysot.uz/oauth/authorize
?response_type=code
&client_id=uysot_app_9f86d081884c7d659a2f
&redirect_uri=https://acme.example/callback
&scope=PERMISSION_OPEN_API_LEAD:READ%20PERMISSION_OPEN_API_CONTRACT:READ
&state=xyz123
| Parameter | Required | Meaning |
|---|---|---|
response_type | ✅ | Always code |
client_id | ✅ | Your application identifier |
redirect_uri | ✅ | Must match one of your registered URIs exactly |
scope | ✅ | Space-separated PERMISSION:SCOPE grants, within your allowed grants |
state | ➖ | Random anti-CSRF value; returned to you unchanged. Use it |
Grants are written as PERMISSION:SCOPE pairs and separated by spaces
(PERMISSION_OPEN_API_LEAD:READ PERMISSION_OPEN_API_CONTRACT:READ); the full list is in
Authentication § grants.
The user signs in to UYSOT if needed, sees your app's name, logo and the permissions you asked for, and approves or declines.
That screen is a CRM page: it reads its context from UYSOT's own
GET /v1/open-api/oauth/authorize and posts the approval back internally. That pair is not part
of your integration surface — your app never calls /authorize itself, it only builds the link
above and the two steps below.
:::note The exact authorization URL The host of the authorization page is given to you at registration — use that value rather than hard-coding the example above. :::
4. Step 2 — handle the redirect back
The browser comes back to your redirect_uri:
Approved
https://acme.example/callback?code=uysot_code_a1b2c3…&state=xyz123
Declined
https://acme.example/callback?error=access_denied&state=xyz123
Declining is not an error condition — show the user a friendly "not connected" state.
On arrival, compare state with the value you generated for this user's session. If it
doesn't match, stop: the request did not originate from your app.
:::warning The code is single-use and short-lived An authorization code is valid for 10 minutes and is consumed by the first successful token call. Exchange it right away from your backend; never send it to the browser again. :::
5. Step 3 — exchange the code for tokens
Call this from your backend only — it carries the client_secret. No user session is needed.
The body is application/x-www-form-urlencoded, per RFC 6749.
POST https://service.app.uysot.uz/v1/open-api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=uysot_code_…&redirect_uri=https://acme.example/callback&client_id=uysot_app_…&client_secret=uysot_sec_…
{
"access_token": "uysot_at_5f2c1e8b9a4d7c3e6b0f2a1d8c4e7b3a9f6d2c5e",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "uysotrt_3a9f6d2c5e1b8f4a7d0c6e2b9f5a3d8c1e7b4f0a",
"scope": "PERMISSION_OPEN_API_LEAD:READ PERMISSION_OPEN_API_CONTRACT:READ",
"webhook_secret": "9f3c1a7e4b2d806f5c1e9a3d7b0f4e2c8a6d1b5f3e7c9a0d2b4f6e8c1a3d5b7f"
}
| Field | Meaning |
|---|---|
access_token | Send it as X-Open-Api-Token on API requests (uysot_at_…) |
token_type | Always "Bearer" |
expires_in | Access token lifetime in seconds — currently 3600 (1 hour) |
refresh_token | Used to get a new access token (uysotrt_…, valid 30 days) |
scope | The grants actually approved — may be narrower than you requested |
webhook_secret | This connection's HMAC signing key — present only if your app has a webhook config. See Webhooks |
embedded_secret | This connection's embedded-page signing key — present only if your app has an embedded config. See Embedded access token |
:::danger The secrets appear once, on this grant only
webhook_secret and embedded_secret are returned only for grant_type=authorization_code,
never on a refresh — the key must not travel the network again and again. Store them keyed by the
installation this connection belongs to; if you lose one, regenerate it from app management in the
CRM (which invalidates the old one immediately). A field is omitted entirely when your app has no
config of that kind.
:::
:::note This endpoint is not enveloped
/oauth/token returns the RFC 6749 shape above directly, with no
response envelope — it is the only success response
in the API shaped that way. Its errors, however, do come back in the envelope (section 9), and
so does /oauth/revoke, which answers 200 with data: null inside the normal envelope.
:::
Store the tokens per company: read scope (and GET /v1/open-api/token/info) to know what
this particular connection may do, and don't assume every company approved the same set.
6. Step 4 — call the API
GET https://api.service.app.uysot.uz/v1/open-api/contract/1234
X-Open-Api-Token: uysot_at_5f2c1e8b9a4d7c3e…
:::warning Authorization: Bearer does not work
Despite token_type: "Bearer", the Open API reads the token only from X-Open-Api-Token.
:::
7. Refreshing an access token
POST https://service.app.uysot.uz/v1/open-api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=uysotrt_…&client_id=uysot_app_…&client_secret=uysot_sec_…
The response has the same shape as step 3.
- Refresh tokens rotate. Every refresh returns a new
refresh_tokenand invalidates the one you used. Persist the new value before you do anything else — losing it means the company has to reconnect. - Previously issued access tokens keep working until they expire on their own, so two instances of your app refreshing at the same time do not cut each other off.
- If the company disconnected your app, or the refresh token expired, the call fails with an invalid grant (section 9) — treat that as "reconnect required".
:::tip A practical strategy
Refresh about 5 minutes before expires_in runs out. Or, simpler: on a 401, refresh once and
retry the request; if the refresh also fails, ask the user to reconnect.
:::
8. Revoking a token
POST https://service.app.uysot.uz/v1/open-api/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=uysotrt_…&client_id=uysot_app_…&client_secret=uysot_sec_…
client_idandclient_secretare required — you can only revoke your own tokens.- Revoking a refresh token (
uysotrt_…) also invalidates the access tokens issued from it. - Revoking an access token (
uysot_at_…) affects only that token. - Per RFC 7009, unknown or foreign tokens also return
200— the endpoint never reveals whether a token exists.
A company can also disconnect your app from its own "Connected apps" list. Every token you hold
for that company then stops working and requests return 401 — handle that as a normal state, not
a crash.
9. Errors
OAuth errors come back in UYSOT's standard envelope, not RFC 6749's {"error": "..."} shape —
and the envelope's error object is not populated on this endpoint, so all you get is the
status and an Uzbek message:
{
"data": null,
"message": "client_id yoki client_secret noto'g'ri!",
"error": null,
"accept": false,
"errors": []
}
:::warning Branch on the HTTP status, not on the code
The codes below are the platform's internal identifiers — they appear in logs and support
tickets, but not in the response body today. Distinguish the cases you must handle by status
(401 = bad client credentials, 400 = everything else) and, if you need more detail, by which
parameter you just sent. See Errors § 3.
:::
| Internal code | Status | When |
|---|---|---|
6907 — invalid request | 400 | response_type ≠ code, missing code / refresh_token, empty client_id / redirect_uri |
6905 — invalid client | 401 | Unknown client_id, or a wrong/missing client_secret |
6906 — invalid grant | 400 | Expired or already-used code, mismatched redirect_uri, invalid or expired refresh token, disconnected company |
6904 — invalid scope | 400 | Malformed, empty, or outside your allowed grants |
6909 — unsupported grant_type | 400 | grant_type is neither authorization_code nor refresh_token |
6902 — invalid redirect_uri | 400 | redirect_uri is not one of your registered URIs |
6900 — application not found | 400 | No application matches the client_id |
An application that is suspended, or a company that disconnected your app, surfaces as
6906 — invalid grant (400) at the token step: there is nothing left to exchange or refresh
against.
Errors on ordinary API requests (401, 403, 429) are covered in Errors.
10. Security checklist
client_secretstays on the backend. Never in a frontend bundle, a mobile app, or a repo.- Always send and verify
state. That is your CSRF protection on the callback. - Register exact redirect URIs and never put an open redirect (
?next=) behind one. - Exchange the code server-side, immediately, once.
- Persist rotated refresh tokens — dropping the new value breaks the connection.
- Never log tokens. If you must, log the last 4 characters only.
- HTTPS everywhere.
- Least privilege — request only the grants you actually use.
- If a secret leaks: revoke the affected tokens, have the secret regenerated, and reconnect.
11. FAQ
Do I get one token per company? Yes. Each (application, company) pair is a separate connection with its own tokens. Store them keyed by company.
A company changed my permissions — do I need new tokens?
No. Permissions are attached to the connection, not baked into the token, so the change applies
immediately. Check the current set with GET /v1/open-api/token/info.
I reused an authorization code and got an invalid grant. Codes are single-use and last 10 minutes. Start the flow again.
Can I use OAuth for my own company only? You can, but a personal access token is simpler — no consent screen, no refresh loop. See Authentication.
Can I skip state?
It is optional in the protocol and a mistake in practice. Without it you cannot tell your own
callback from an attacker's.
How is the rate limit counted? Per token — 60 requests per minute. Each company's access token has its own budget (Rate limits).