Working with tokens
Every flow ends at the same place: an access_token you send with each API call, and a refresh_token you use to get a new pair when it expires.
Exchanging the authorization code
Identical for Flow A and Flow B.
Request
POST /auth/oauth/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "abc123def456",
"client_id": "69c50a2d50f7b939923a469b",
"client_secret": "4f3c1a9e8b27d605f1ac93e7d284b6510ff7a3c9e15d8b2470a6cd91e83f4b27"
}| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be "authorization_code" |
code | string | Yes | The authorization code Bito gave you |
client_id | string | Yes | Your OAuth client id |
client_secret | string | Yes | Your client secret |
Response
{
"code": 0,
"message": "Success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 86400,
"token_type": "Bearer",
"scope": "read write"
}
}expires_in is in seconds, counted from the moment of issue.
WARNING
An authorization code is single-use and dies after 20 minutes. Exchanging it twice returns Invalid credentials, not a second token pair.
Refreshing tokens
Request
POST /auth/oauth/token
Content-Type: application/json
{
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"client_id": "69c50a2d50f7b939923a469b",
"client_secret": "4f3c1a9e8b27d605f1ac93e7d284b6510ff7a3c9e15d8b2470a6cd91e83f4b27"
}The response shape is identical to the code exchange.
WARNING
Both tokens rotate on every refresh. The refresh token you just used stops working — persist the new pair atomically, or the next refresh will fail and the merchant will have to reinstall.
Refreshing does not re-install an integration the merchant removed. If a merchant uninstalls you, refresh keeps working against a not-installed record; treat your uninstall hook as the authoritative signal to stop calling.
Client credentials grant
For server-to-server work with no user present. It goes to a different endpoint — /auth/oauth/auth, not /auth/oauth/token — and returns tokens immediately, with no authorization code in between.
WARNING
Prerequisite: the merchant must already have installed your integration. This grant authenticates against the per-install integration record, so there is nothing to match before the first install.
Request
POST /auth/oauth/auth
Content-Type: application/json
{
"response_type": "client_credentials",
"client_id": "69c50a2d50f7b939923a469b",
"client_secret": "4f3c1a9e8b27d605f1ac93e7d284b6510ff7a3c9e15d8b2470a6cd91e83f4b27",
"redirect_uri": "https://example-app.com/callback",
"scope": ["read", "write"],
"state": "optional_state"
}| Field | Type | Required | Description |
|---|---|---|---|
response_type | string | Yes | Must be "client_credentials" |
client_id | string | Yes | Your OAuth client id |
client_secret | string | Yes | The secret of the integration install |
redirect_uri | string | Yes | Must be a valid URL. Validated even though this grant never redirects — send your registered callback. |
scope | string[] | Yes | Requested scopes, as an array |
state | string | No | Opaque value, echoed back |
Response
{
"code": 0,
"message": "Success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 86400,
"token_type": "Bearer",
"scope": "read write"
}
}Using access tokens
Send the token in the Authorization header on every call.
POST /integration/api/v2/product/get-paging
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
Accept-Language: en
{
"limit": 20,
"page": 1
}| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <access_token> |
Accept-Language | No | Response language: uz, uz_cyrillic, ru, en, kk, tg, ky. Default uz. |
organization_id | No | Target organization inside the account. Used only when the integration's settings (fields) include the organization_id field — otherwise the header is ignored. When the header is absent, that field's value is used. |
warehouse_id | No | Target warehouse. Used only when the integration's settings (fields) include the warehouse_id field — otherwise the header is ignored. When the header is absent, that field's value is used. |
price_id | No | Price list to read prices from. Used only when the integration's settings (fields) include the price_id field — otherwise the header is ignored. When the header is absent, that field's value is used. |
role_id | No | The role the request runs with. Used only when the integration's settings (fields) include the role_id field — otherwise the header is ignored. When the header is absent, that field's value is used. |
user_id | No | Run the request as this user. The user must belong to this account and not be blocked, otherwise the request is refused. When absent, the request runs as the account owner. |
You never send an account id — the token already carries it.
Token lifetimes
| Token | Lifetime | Notes |
|---|---|---|
| Authorization code | 20 minutes | Single use, must be exchanged inside the window |
| Access token | 24 hours | Sent in the Authorization header |
| Refresh token | 90 days | Exchanged for a new pair; rotates on every use |
| One-time token | 24 hours | Single use, signed with your client secret — see One-time tokens |
WARNING
Rotating your client secret kills every access and refresh token issued under the old one, in every merchant account at once.
Rate limits
/auth/oauth/auth and /auth/oauth/token allow 20 requests per minute per IP. Refresh on expiry, not on a timer, and never per request.