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

# Exchange a code, or refresh

`POST /oauth/token`

## Request

- Base URL: `https://scanduro.com`
- URL: `https://scanduro.com/oauth/token`
- Auth: HTTP bearer

`authorization_code` requires the `code_verifier`, compared in constant time against the
stored challenge. Redemption is a conditional UPDATE, so two concurrent exchanges of one code
cannot both succeed. A code presented twice revokes the entire grant family: we cannot
distinguish a broken client from an intercepted code, and the safe reading is the second.

`refresh_token` rotates. The presented token and its sibling access token are revoked as the
replacement pair is issued, and presenting an already-revoked refresh token — the signature
of a stolen copy — revokes the whole family, which makes the theft visible instead of silent.

Public clients only: send `client_id` in the body and no secret.

## Request body

- object
  - `grant_type` 'authorization_code' | 'refresh_token', required
  - `code` string
  - `code_verifier` string
  - `redirect_uri` string, uri
  - `refresh_token` string
  - `client_id` string

## Response `200`

A fresh token pair. Never cached — the response carries a no-store cache directive.

- object
  - `access_token` string, required: Of the form `sndr_at_<prefix>_<secret>`. Valid on `/mcp` and on `/api/v1`, always in live mode.
  - `token_type` 'Bearer', required
  - `expires_in` integer, required
  - `refresh_token` string: Of the form `sndr_rt_<prefix>_<secret>`. Rotated on every use. Refused as a bearer credential.
  - `scope` string

## Other responses

- `400`: RFC 6749 §5.2 error object. Every authorization-code failure — unknown code, wrong client, mismatched redirect, expired, replayed — answers the same `invalid_grant`, because distinguishing them tells an attacker which half of a guess was right.
- `429`: RFC 6749 §5.2 error object. Every authorization-code failure — unknown code, wrong client, mismatched redirect, expired, replayed — answers the same `invalid_grant`, because distinguishing them tells an attacker which half of a guess was right.

---

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