---
title: "Exchange auth code for session token"
method: POST
path: "/oauth/exchange"
tags: ["OAuth"]
---

# Exchange auth code for session token

`POST /oauth/exchange`

## Request

- Base URL: `https://api.uptimemonitoring.com`
- URL: `https://api.uptimemonitoring.com/oauth/exchange`
- Auth: none required

Exchanges a one-time auth code (from the OAuth callback redirect) for a
session token. Auth codes expire after 60 seconds and can only be used once.

## Request body

- object
  - `code` string, required: One-time auth code from the OAuth callback redirect.

## Response `200`

Session token and account info.

- object
  - `token` string: Session token for Bearer authentication.
  - `account` Account
    - `id` integer
    - `github_id` integer: GitHub numeric ID of the account's owner. Since issue #311, sourced from `users.github_id` via the account's owner membership (`account_members`), not a column on `accounts` itself — display-only, never an authorization input. `0` for an owner whose identity is email-only (issue #315) rather than GitHub — never `null`.
    - `github_username` string: GitHub username of the account's owner, sourced the same way as github_id. `""` (empty string) for an email-only owner — never `null`.
    - `email` string, nullable: Any verified email identity held by the account's owner — either the address behind an email-identity sign-in (issue #315, sign-in part A) or one GitHub reported as verified and primary, adopted onto the owner's `users` row on a sign-in that found none on file yet (issue #390) — sourced from `users.email` via the account's owner membership, the same way github_id/github_username are. `null` when the owner holds no verified email of either kind. Display-only, never an authorization input; never present on any public, unauthenticated surface (`GET /feed/{token}`, `GET /feed/{token}/rss`). Omitted entirely (issue #323, team accounts B1) unless the authenticated caller's OWN role is `owner` (issue #325: an account can have more than one owner, so this is no longer "caller is THE owner") — a viewer or member on the same account never sees it, regardless of whether the account's (earliest) owner has adopted an email of either kind.
    - `notification_email` string, nullable
    - `role` 'viewer' | 'member' | 'owner': The AUTHENTICATED CALLER's own authority on this account (issue #323, team accounts B1) — not a property of the account itself. Always present. The SPA uses this to hide owner/member-only controls.
    - `is_admin` boolean: True when the authenticated account is in the admins table — i.e. the same answer auth.RequireAdmin returns for any admin-gated route. Always present on GET /api/v1/account and on the PATCH /api/v1/account success response.
    - `last_cap_hit_at` string, date-time, nullable: Timestamp of the latest successfully recorded POST /api/v1/monitors (or MCP create_monitor) rejection for hitting the per-account hard cap (100 monitors) — not a guaranteed latest-refusal timestamp. Updates are coalesced to at most once per account per minute, and a failure to record a hit is swallowed (the REST 409 or MCP tool-error result the client sees is unaffected either way), so a refusal can occur without advancing this field, and null does not prove the account has never been refused. Advisory only — the app uses it to time an upgrade offer; never cleared once set.
    - `silent_monitors` integer: Count of ENABLED monitors on this account with no ACTIVE attached recipient (issue #212). "Active" excludes a disabled recipient and a still-unconfirmed (`pending`) email recipient (issue #321) — a monitor whose only attached recipient is pending or disabled is counted as silent, the same as one with no attached recipient at all. Disabling a monitor removes it from this count — a disabled, unreachable monitor is not flagged as silent since it is not being checked. Present on both `GET` and `PATCH /api/v1/account`.
    - `created_at` string, date-time
    - `updated_at` string, date-time

## Other responses

- `400`: Invalid request.
- `401`: Invalid or expired auth code.
- `413`: Request body exceeds the 64 KB hard limit. Per docs/SPEC.md §Hard Limits the dispatcher rejects any payload larger than 65,536 bytes before it is decoded.
- `415`: The request `Content-Type` is not `application/json`. Write operations (POST, PUT, PATCH) require that header; a missing or non-JSON media type is rejected before the body is read. The `charset` parameter is accepted and ignored, so `application/json; charset=utf-8` is valid.

---

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