---
title: "Exchange a code, refresh token, or subject token for an access token"
method: POST
path: "/v1/oauth/token"
tags: ["OAuth"]
---

# Exchange a code, refresh token, or subject token for an access token

`POST /v1/oauth/token`

## Request

- Base URL: `https://api.assinafy.com.br`
- URL: `https://api.assinafy.com.br/v1/oauth/token`
- Auth: none required

Implements the RFC 6749 §5.1/§5.2 token-endpoint body contract in
     *         both directions: a successful exchange returns a flat JSON object with
     *         `access_token` at the top level, and a failure returns a flat
     *         `{error, error_description}` object — neither is wrapped in this API's
     *         usual response envelope, since no standard OAuth client library (or MCP
     *         connector) would find `access_token` or `error` inside a `data` key.
     *
     *         The third grant, `urn:ietf:params:oauth:grant-type:token-exchange` (RFC 8693),
     *         is restricted to a confidential internal-service client (see `/oauth/introspect`)
     *         and trades a front-end resource's access token (the `subject_token`) for one
     *         minted for THIS API — the crossing an MCP server makes after a user consents,
     *         since its own token can never be forwarded here directly. The issued token is
     *         never wider than the subject: `scope`, when sent, must be a subset of the
     *         subject's own scopes, and `resource` must equal this API's own resource
     *         identifier exactly. No refresh token is issued; the caller re-exchanges from
     *         the user's own token instead of holding unattended API access.

[Copy as Markdown](https://api.assinafy.com.br/v1/docs/markdown?method=post&path=%2Fv1%2Foauth%2Ftoken) [Open in ChatGPT](https://chatgpt.com/?q=Read%20the%20API%20documentation%20at%20https%3A%2F%2Fapi.assinafy.com.br%2Fv1%2Fdocs%2Fmarkdown%3Fmethod%3Dpost%26path%3D%252Fv1%252Foauth%252Ftoken%20and%20help%20me%20integrate%20this%20endpoint.) [Open in Claude](https://claude.ai/new?q=Read%20the%20API%20documentation%20at%20https%3A%2F%2Fapi.assinafy.com.br%2Fv1%2Fdocs%2Fmarkdown%3Fmethod%3Dpost%26path%3D%252Fv1%252Foauth%252Ftoken%20and%20help%20me%20integrate%20this%20endpoint.)

## Request body

- OAuthTokenRequest: Body of `POST /v1/oauth/token`. Sent form-encoded per RFC 6749, or as JSON.
  - `grant_type` 'authorization_code' | 'refresh_token' | 'urn:ietf:params:oauth:grant-type:token-exchange', required: `urn:ietf:params:oauth:grant-type:token-exchange` is for internal service clients only (Assinafy's own MCP server) — an ordinary confidential or public client authenticates with it and always gets `invalid_client`, exactly as an unrecognized client would. Everyday integrators use `authorization_code` and `refresh_token`.
  - `code` string
  - `redirect_uri` string, uri
  - `code_verifier` string: RFC 7636: 43-128 characters from [A-Za-z0-9-._~]. Shorter values are rejected with `invalid_grant`.
  - `refresh_token` string
  - `client_id` string, required
  - `client_secret` string: Confidential clients only. Public clients authenticate with PKCE and are never issued a secret; the token-exchange grant requires a confidential, internal-service client and therefore always requires this.
  - `resource` string, uri: RFC 8707 resource indicator. For `authorization_code`/`refresh_token`, optional; when present it must be the `resource` value published by /.well-known/oauth-protected-resource and must match the one sent to /authorize, otherwise `invalid_target`. For the token-exchange grant it is REQUIRED and must equal this API's own resource identifier exactly (never a front-end resource such as the MCP server), otherwise `invalid_target`.
  - `subject_token` string: Token-exchange grant only. The front-end resource's access token being traded in. Must be a live, original (never itself exchanged) token minted for a resource this server issues tokens for, other than this API's own audience.
  - `subject_token_type` 'urn:ietf:params:oauth:token-type:access_token': Token-exchange grant only. Required; only `urn:ietf:params:oauth:token-type:access_token` is supported.
  - `requested_token_type` 'urn:ietf:params:oauth:token-type:access_token': Token-exchange grant only. Optional; when present it must agree with the only type this server issues.

## Response `200`

The token response

- object
  - `access_token` string
  - `issued_token_type` string: Present only for the token-exchange grant, per RFC 8693 §2.2.1.
  - `token_type` string
  - `expires_in` integer: For the token-exchange grant, clamped to the subject token's own remaining lifetime as well as the exchanged-token TTL — never longer than either.
  - `refresh_token` string, nullable: Present only when the `offline_access` scope was requested AND consented on the authorization_code/refresh_token grants. Never present for the token-exchange grant. Without it a client must send the user through the authorization flow again once the access token expires.
  - `scope` string: The scope of the ACCESS token. `offline_access` is a request-time signal rather than a permission, so it never appears here even when it was requested.
  - `id_token` string, nullable: A signed OIDC id_token (RS256). Present only when the openid scope was granted.

## Other responses

- `400`: `invalid_grant` (bad, expired, replayed, or wrong-client authorization code; a `code_verifier` outside the RFC 7636 grammar of 43-128 unreserved characters; redirect_uri mismatch; a refresh token whose authorization no longer includes `offline_access`; for token-exchange, a `subject_token` that is unusable, itself an exchanged token, not visible to service clients, or expired), `invalid_target` (a `resource` this server does not issue tokens for, one disagreeing with the authorized value, or — for token-exchange — one other than this API's own resource identifier), `invalid_scope` (token-exchange only: the requested `scope` is not a subset of the subject token's own scopes), `invalid_request` (token-exchange only: an unsupported `subject_token_type`/`requested_token_type`), or `unsupported_grant_type`
- `401`: `invalid_client` — unknown/disabled client or failed client authentication. The description never reveals whether client_id exists.
- `429`: Rate limit exceeded; wait the `Retry-After` seconds. Limited per caller IP.
- `500`: Unexpected server error.

## Changes

- **2026-10-09** `c4913641cd48`: 1 info
  - added the non-success response with the status `429`
- **2026-09-28** `68e588d7c7b6`: 1 info
  - added the media type `application/x-www-form-urlencoded` to the request body
- **2026-09-23** `4c4752c512b6`: 3 warning, 5 info
  - added the new `invalid_request` enum value to the `error` response property for the response status `400`
  - added the new `invalid_scope` enum value to the `error` response property for the response status `400`
  - added the new `invalid_target` enum value to the `error` response property for the response status `400`
  - added the new optional request property `requested_token_type`
  - …4 more
- **2026-09-17** `6b55ce24462c`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/assinafy/apis/assinafy-api-reference/changes/v1/oauth/token/post.md)

---

[API](https://skmtc.dev/assinafy/apis/assinafy-api-reference.md) - [All operations](https://skmtc.dev/assinafy/apis/assinafy-api-reference/llms.txt) - [OpenAPI document](https://skmtc.dev/assinafy/apis/assinafy-api-reference/revisions/f7da81a5567e?raw)
