OAuth
4 operations under /oauth (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Base URL: https://api.codespar.dev
None of the operations below takes a credential: the published document declares them open. See Authentication for the rest of the API.
GET /oauth/authorize
https://api.codespar.dev/oauth/authorizeConsent page (RFC 6749 §4.1.1 authorization request)
Returns text/html, NOT JSON: this is the leg a human completes in a browser, so a generated client should open it rather than call it. response_type=code, a registered client_id, a redirect_uri matching one registered for that client exactly, and code_challenge with code_challenge_method=S256 are all required.
Errors are rendered as HTML with status 400 and are NOT redirected: per RFC 6749 §4.1.2.1 an invalid client_id/redirect_uri must not be redirected to, and this endpoint keeps that posture for every error so a human sees the reason on the page. A scope request this server cannot satisfy is refused here, before the form asking for a key is drawn.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
client_id | string | yes | — |
code_challenge | string | yes | — |
code_challenge_method | "S256" | yes | — |
redirect_uri | string | yes | — |
response_type | "code" | yes | — |
scope | string | no | Space-separated. Requesting more than the pasted key holds is refused. |
state | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | — | The consent page. |
400 | — | Rendered as HTML, never redirected. |
curl -X GET https://api.codespar.dev/oauth/authorize \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /oauth/authorize HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/oauth/authorize",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/oauth/authorize", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/oauth/authorize", {
query: {
response_type: "code",
client_id: "client_0000000000000000",
redirect_uri: "https://example.com/hook",
code_challenge: "string",
code_challenge_method: "S256"
}
});POST /oauth/authorize
https://api.codespar.dev/oauth/authorizeConsent submit — exchanges a pasted API key for an authorization code
application/x-www-form-urlencoded, submitted by the page above, not by a generated client. The form carries the same authorization-request parameters plus the API key the human pasted.
On success: 302 to redirect_uri with code and state. The code is single-use, short-lived, and bound to the tenant, the client, the redirect URI, the PKCE challenge AND the API key that authorized it — so revoking that key invalidates every token the code goes on to mint, and the token's scopes can never exceed the key's own. On refusal the page re-renders as HTML with 400.
Responses
| Status | Body | Description |
|---|---|---|
302 | — | Location: <redirect_uri>?code=…&state=… (RFC 6749 §4.1.2). |
400 | — | Rendered as HTML, never redirected. |
curl -X POST https://api.codespar.dev/oauth/authorize \
-H "Authorization: Bearer $CODESPAR_API_KEY"POST /oauth/authorize HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.post(
"https://api.codespar.dev/oauth/authorize",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/oauth/authorize", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.post("/oauth/authorize", {
body: {
response_type: "code",
client_id: "client_0000000000000000",
redirect_uri: "https://example.com/hook",
code_challenge: "string",
code_challenge_method: "S256",
state: "string",
scope: "string",
api_key: "string"
}
});POST /oauth/register
https://api.codespar.dev/oauth/registerRegister a client (RFC 7591 dynamic client registration)
Open registration, on purpose: it is what lets an MCP client obtain a client_id with no pre-existing CodeSpar credential. redirect_uris must be absolute https, except loopback (127.0.0.1, [::1], localhost), where http is allowed for a native client catching the redirect on a random port (OAuth 2.1 / RFC 8252 §7.3).
A client_id is an identifier, not a credential. It carries no token, no tenant and no reach; every path from it to a token runs through the consent page, where a live API key has to be pasted, and the resulting token is bounded by that key's own scopes.
Bounded two ways: per client address, a budget of 10 registrations an hour (429 rate_limit_exceeded with Retry-After past it), and a global cap on registered clients (503 client_registration_cap_reached past it). client_name is the application's own claim; the consent page shows it marked unverified.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
client_name | string | no | Truncated to 256 characters. Rendered on the consent page, escaped and marked unverified. |
grant_types | array of string | no | — |
redirect_uris | array of string | yes | Absolute https URIs; http only for loopback hosts. |
response_types | array of string | no | — |
token_endpoint_auth_method | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | Created |
400 | object | invalid_redirect_uri when a URI is missing, relative, or http on a non-loopback host. |
429 | object | rate_limit_exceeded: this address spent its registration budget. Retry-After and retry_after_seconds say when the next one is accepted. |
503 | object | client_registration_cap_reached: the server holds as many registered clients as it accepts. Nothing was registered; retry later. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
client_id | string | yes | — |
client_name | string | no | — |
grant_types | array of string | yes | — |
redirect_uris | array of string | yes | — |
response_types | array of string | yes | — |
token_endpoint_auth_method | "none" | yes | — |
curl -X POST https://api.codespar.dev/oauth/register \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": [
"https://example.com/hook"
],
"client_name": "Example",
"token_endpoint_auth_method": "https://example.com/hook",
"grant_types": [
"string"
],
"response_types": [
"string"
]
}'POST /oauth/register HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"redirect_uris": [
"https://example.com/hook"
],
"client_name": "Example",
"token_endpoint_auth_method": "https://example.com/hook",
"grant_types": [
"string"
],
"response_types": [
"string"
]
}import os
import requests
res = requests.post(
"https://api.codespar.dev/oauth/register",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"redirect_uris": [
"https://example.com/hook"
],
"client_name": "Example",
"token_endpoint_auth_method": "https://example.com/hook",
"grant_types": [
"string"
],
"response_types": [
"string"
]
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/oauth/register", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"redirect_uris": [
"https://example.com/hook"
],
"client_name": "Example",
"token_endpoint_auth_method": "https://example.com/hook",
"grant_types": [
"string"
],
"response_types": [
"string"
]
}),
});
const data = await res.json();const result = await cs.api.post("/oauth/register", {
body: {
redirect_uris: [
"https://example.com/hook"
],
client_name: "Example",
token_endpoint_auth_method: "https://example.com/hook",
grant_types: [
"string"
],
response_types: [
"string"
]
}
});{
"client_id": "client_0000000000000000",
"redirect_uris": [
"https://example.com/hook"
],
"client_name": "Example",
"token_endpoint_auth_method": "none",
"grant_types": [
"string"
],
"response_types": [
"string"
]
}POST /oauth/token
https://api.codespar.dev/oauth/tokenToken endpoint — authorization_code and refresh_token grants
application/x-www-form-urlencoded, unauthenticated (public client, token_endpoint_auth_method: none); the authorization code IS the authority. grant_type=authorization_code requires code, code_verifier, redirect_uri and client_id; grant_type=refresh_token requires refresh_token. Anything else is 400 unsupported_grant_type.
Redemption is atomic and single-use even under a concurrent retry: a second redemption of the same code sees no row and gets 400 invalid_grant. The refresh grant ROTATES — the presented refresh token is revoked and a new one returned, so a client must persist the new value. A refresh chain lives at most 90 days from the consent that began it: past that the refresh grant answers 400 invalid_grant and the user has to authorize again. expires_in is the access token's lifetime in seconds Responses carry Cache-Control: no-store (RFC 6749 §5.1).
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_grant (code invalid, expired, already used, bound to a different client or redirect_uri, PKCE verification failed, carrying no recorded scope grant, or a refresh chain older than 90 days), invalid_request, or unsupported_grant_type. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
access_token | string | yes | — |
expires_in | integer | yes | — |
refresh_token | string | yes | — |
scope | string | yes | — |
token_type | "Bearer" | yes | — |
curl -X POST https://api.codespar.dev/oauth/token \
-H "Authorization: Bearer $CODESPAR_API_KEY"POST /oauth/token HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.post(
"https://api.codespar.dev/oauth/token",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/oauth/token", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.post("/oauth/token", {
body: {
grant_type: "authorization_code",
code: "string",
code_verifier: "string",
redirect_uri: "https://example.com/hook",
client_id: "client_0000000000000000",
refresh_token: "string"
}
});{
"access_token": "string",
"token_type": "Bearer",
"expires_in": 0,
"refresh_token": "string",
"scope": "string"
}Well-known documents
3 operations under /.well-known (GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Discovery
1 operation under /v1/discovery/manifest (GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.