Skip to main content

OAuth

4 operations under /oauth (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

5 min read
View MarkdownEdit on GitHub

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

GEThttps://api.codespar.dev/oauth/authorize
No credential

Consent 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

NameTypeRequiredDescription
client_idstringyes—
code_challengestringyes—
code_challenge_method"S256"yes—
redirect_uristringyes—
response_type"code"yes—
scopestringnoSpace-separated. Requesting more than the pasted key holds is refused.
statestringno—

Responses

StatusBodyDescription
200—The consent page.
400—Rendered as HTML, never redirected.
Example request
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_KEY
import 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

POSThttps://api.codespar.dev/oauth/authorize
No credential

Consent 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

StatusBodyDescription
302—Location: <redirect_uri>?code=…&state=… (RFC 6749 §4.1.2).
400—Rendered as HTML, never redirected.
Example request
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_KEY
import 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

POSThttps://api.codespar.dev/oauth/register
No credential

Register 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

FieldTypeRequiredDescription
client_namestringnoTruncated to 256 characters. Rendered on the consent page, escaped and marked unverified.
grant_typesarray of stringno—
redirect_urisarray of stringyesAbsolute https URIs; http only for loopback hosts.
response_typesarray of stringno—
token_endpoint_auth_methodstringno—

Responses

StatusBodyDescription
201objectCreated
400objectinvalid_redirect_uri when a URI is missing, relative, or http on a non-loopback host.
429objectrate_limit_exceeded: this address spent its registration budget. Retry-After and retry_after_seconds say when the next one is accepted.
503objectclient_registration_cap_reached: the server holds as many registered clients as it accepts. Nothing was registered; retry later.

Response 201

FieldTypeRequiredDescription
client_idstringyes—
client_namestringno—
grant_typesarray of stringyes—
redirect_urisarray of stringyes—
response_typesarray of stringyes—
token_endpoint_auth_method"none"yes—
Example request
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"
    ]
  }
});
Example response 201
application/json
{
  "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

POSThttps://api.codespar.dev/oauth/token
No credential

Token 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

StatusBodyDescription
200objectOK
400objectinvalid_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

FieldTypeRequiredDescription
access_tokenstringyes—
expires_inintegeryes—
refresh_tokenstringyes—
scopestringyes—
token_type"Bearer"yes—
Example request
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_KEY
import 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"
  }
});
Example response 200
application/json
{
  "access_token": "string",
  "token_type": "Bearer",
  "expires_in": 0,
  "refresh_token": "string",
  "scope": "string"
}
OAuth | CodeSpar