Skip to main content

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:


1. Before you start​

Register your application with UYSOT. You receive:

ItemNote
client_idPublic identifier, uysot_app_…
client_secretuysot_sec_… — shown once. Backend only
Registered redirect URIsEvery redirect_uri you will use, listed exactly
Allowed grantsThe maximum set of permissions your app may ever request
The authorization page URLWhere 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.


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
ParameterRequiredMeaning
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"
}
FieldMeaning
access_tokenSend it as X-Open-Api-Token on API requests (uysot_at_…)
token_typeAlways "Bearer"
expires_inAccess token lifetime in seconds — currently 3600 (1 hour)
refresh_tokenUsed to get a new access token (uysotrt_…, valid 30 days)
scopeThe grants actually approved — may be narrower than you requested
webhook_secretThis connection's HMAC signing key — present only if your app has a webhook config. See Webhooks
embedded_secretThis 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_token and 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_id and client_secret are 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 codeStatusWhen
6907 — invalid request400response_type ≠ code, missing code / refresh_token, empty client_id / redirect_uri
6905 — invalid client401Unknown client_id, or a wrong/missing client_secret
6906 — invalid grant400Expired or already-used code, mismatched redirect_uri, invalid or expired refresh token, disconnected company
6904 — invalid scope400Malformed, empty, or outside your allowed grants
6909 — unsupported grant_type400grant_type is neither authorization_code nor refresh_token
6902 — invalid redirect_uri400redirect_uri is not one of your registered URIs
6900 — application not found400No 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_secret stays 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).