---
title: "Request an access token"
method: POST
path: "/oauth/token"
tags: ["Authentication"]
---

# Request an access token

`POST /oauth/token`

Exchanges your service account credentials for a short-lived Bearer access token (OAuth 2.0 client credentials grant, RFC 6749 section 4.4). Send the token in the `Authorization: Bearer <token>` header on API calls.

The request body must be `application/x-www-form-urlencoded`. Always send credentials in the body, never in the URL. A request that puts `client_secret` or `client_assertion` in the query string is rejected, and you should treat that credential as exposed and rotate it. HTTP Basic authentication is not supported: an `Authorization: Basic` header is refused with `401 invalid_client`, even when the body also carries credentials. Each parameter may appear only once, and the body may not exceed 16 KiB.

**Client authentication.** Your service account uses exactly one of these methods. A credential of the other kind is rejected with `invalid_client`. Send exactly one credential per request.

`client_secret_post`: send `client_id` and `client_secret`.

`private_key_jwt`: send `client_assertion` and `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. `client_id` is optional, but if you send it, it must equal the assertion's `iss`. The assertion is a JWT signed with the private key whose public key is registered for your service account (RFC 7523). It must meet these requirements:
- Signed with `RS256`, `PS256` or `ES256`, matching the algorithm registered for the key. The header `kid` is optional and selects among your registered keys. The header `typ`, if present, must be `JWT` or `client-authentication+jwt`. The `crit` header is not accepted.
- `iss` and `sub` must both equal your `client_id`.
- `aud` must be exactly one value: `https://api.resourcly.com`. Do not use the token endpoint URL, which many client libraries default to. It is rejected.
- `exp` is required and must be no more than 5 minutes in the future. Clock skew of up to 30 seconds is tolerated. `iat` and `nbf`, if present, must not be in the future.
- `jti` is required, 1 to 256 characters, and unique per assertion.

**Single use.** Each assertion can be used once. Presenting the same assertion again returns `invalid_client`, so generate a fresh assertion (new `jti`) for every token request. An assertion is not consumed when the request fails without issuing a token (for example `invalid_scope`, `unauthorized_client` or a `503`), so you may retry with the same one until it expires.

**Scope.** `scope` is an optional space-delimited list. If omitted, the token receives every scope granted to your service account. Requesting a scope your service account has not been granted returns `invalid_scope`. Available scopes: `items:read`.

**Lifetime.** The token is valid for the number of seconds in `expires_in` (currently 15 minutes). No refresh token is issued: request a new token before or after expiry. Tokens stop working immediately if the service account is revoked or its access is changed.

**Errors** use the RFC 6749 section 5.2 format (`error`, `error_description`), except `429`:
- `400 invalid_request`: malformed request, wrong content type, a repeated parameter, a missing `grant_type`, `client_secret` without `client_id`, `client_assertion` without `client_assertion_type`, both credentials sent, or credentials sent in the URL.
- `400 unsupported_grant_type`: `grant_type` is anything other than `client_credentials`.
- `400 unauthorized_client`: API access is not enabled for your organization. Contact support@resourcly.com.
- `400 invalid_scope`: a requested scope has not been granted to your service account.
- `401 invalid_client`: no credential was sent, an `Authorization: Basic` header was sent, or client authentication failed. A failed authentication is deliberately generic and does not say why (unknown client, wrong secret, bad signature, expired or replayed assertion, revoked account). It carries `WWW-Authenticate: Bearer realm="oauth"`.
- `429`: too many requests. The body is `{"error": "rate limit exceeded", "code": "RATE_LIMITED"}`; retry after the number of seconds in the `Retry-After` header.
- `503 temporarily_unavailable`: retry shortly.

## Response `200`

OK

- ModelsOAuthTokenResponse
  - `access_token` string — Bearer token to send in the Authorization header
  - `expires_in` integer — seconds until the token expires
  - `scope` string — space-delimited scopes the token carries
  - `token_type` string — always "Bearer"

## Other responses

- `400` — invalid_request, unsupported_grant_type, unauthorized_client or invalid_scope
- `401` — invalid_client
- `429` — Rate limit exceeded; see the Retry-After header
- `503` — temporarily_unavailable

## Changes

- **2026-10-02** `9de8eb88444b` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/resourcly/apis/resourcly-api/changes/oauth/token/post.md)

---

[API](https://skmtc.dev/resourcly/apis/resourcly-api.md) · [All operations](https://skmtc.dev/resourcly/apis/resourcly-api/llms.txt) · [OpenAPI document](https://skmtc.dev/resourcly/apis/resourcly-api/revisions/9de8eb88444b?raw)
