> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.rhombus.community/llms.txt
> Use this file to discover all available pages before exploring further.

# Sign in with Rhombus

> Add Sign in with Rhombus to your application using OAuth 2.0 with PKCE — handle authorization codes, access tokens, refresh tokens, and SSO identity claims.

## Overview

Rhombus supports OAuth 2.0 with PKCE so you can build applications that sign users in with their existing Rhombus credentials. When a user clicks **Sign in with Rhombus** in your app, they're redirected to the Rhombus Console to authenticate, then bounced back to your redirect URI with a short-lived authorization code. You exchange that code for an access token and call the Rhombus API on the user's behalf.

This is the same flow used by the official [Rhombus CLI](/rhombus-cli) — `rhombus login` is a working reference implementation in Go that you can read end to end.

Use this guide to build:

* **CLI tools** that authenticate via browser login (like the Rhombus CLI itself)
* **Web applications** that let Rhombus users sign in to your service
* **Admin dashboards** and internal integrations for customers that manage many Rhombus orgs
* **Desktop applications** using a local-loopback redirect

<Note>
  **What this guide isn't.** This is for third-party developers building apps that sign in *Rhombus users*. If you're a Rhombus customer trying to configure **SAML SSO** for your employees (Okta, Azure AD, Google Workspace) or **SCIM** for user provisioning, see [SAML SSO & SCIM Provisioning](/implementations/saml-sso-provisioning) instead — that's a separate surface from the OAuth flow described here.
</Note>

## How the flow works

The Rhombus OAuth surface spans three hosts. This is a common source of confusion — your app talks to all three at different stages of the flow.

| Host                          | Role                                           |
| ----------------------------- | ---------------------------------------------- |
| `console.rhombussystems.com`  | Where the user signs in and approves your app  |
| `auth-web.rhombussystems.com` | Token exchange endpoint                        |
| `api2.rhombussystems.com`     | API calls made with the resulting access token |

```mermaid theme={null}
sequenceDiagram
    participant User
    participant App as Your App
    participant Console as console.rhombussystems.com
    participant Auth as auth-web.rhombussystems.com
    participant API as api2.rhombussystems.com

    User->>App: Click "Sign in with Rhombus"
    App->>App: Generate PKCE verifier + state
    App->>Console: Redirect to /oauth/authorize?client_id=...
    Console->>User: Prompt to sign in and authorize
    User->>Console: Approve
    Console->>App: Redirect to redirect_uri?code=...&state=...
    App->>App: Verify state matches
    App->>Auth: POST /oauth/token (code + verifier)
    Auth->>App: access_token, refresh_token, expires_in
    App->>API: API request with x-auth-access-token
    API->>App: Response
```

## Before you begin

<Info>
  Before you start, make sure you have:

  * A **Rhombus account** with an API key (generated in the [Rhombus Console](https://console.rhombussystems.com/) under **Settings → API Management**)
  * A **redirect URI** your app controls — a public HTTPS URL in production, or `http://localhost:<port>/callback` for CLI and desktop apps
  * Basic familiarity with **OAuth 2.0 Authorization Code flow** and **PKCE** ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636))
</Info>

## Step 1: Register your application

Before you can start an OAuth flow, Rhombus needs to know about your application. Registration gives you a `clientId` and `clientSecret` pair.

There are two paths to a `clientId`, depending on where you are in your build:

<Columns cols={2}>
  <Card title="Prototyping & development" icon="flask">
    Call the `submitApplication` API directly with your existing API key. Fastest path to a working flow on `localhost`. Self-serve, immediate.
  </Card>

  <Card title="Production & distribution" icon="shield-check" href="https://rhombus.community">
    Apps that will be shipped to customers or accept logins from users outside your own organization must be reviewed by Rhombus. Contact your Rhombus representative or post in the [Developer Community](https://rhombus.community) to start review.
  </Card>
</Columns>

<Warning>
  Production OAuth applications require Rhombus review. You can self-register a `clientId` with the API call below for local development and testing, but do not distribute apps to end users with a self-registered `clientId` — Rhombus may rate-limit or revoke unreviewed production use. Start a review as soon as your prototype works.
</Warning>

### Register with the API

`POST /api/oauth/submitApplication` returns a fresh `clientId` and `clientSecret`. **Store the `clientSecret` securely** — it is not retrievable later.

<CodeGroup>
  ```python Python theme={null}
  import requests

  headers = {
      "x-auth-scheme": "api-token",
      "x-auth-apikey": "YOUR_API_KEY",
      "Content-Type": "application/json",
  }

  response = requests.post(
      "https://api2.rhombussystems.com/api/oauth/submitApplication",
      headers=headers,
      json={
          "name": "Acme Console",
          "description": "Acme internal dashboard for Rhombus operators",
          "contactEmail": "engineering@acme.example",
          "redirectUri": "https://acme.example/oauth/callback",
      },
  )

  data = response.json()
  print("clientId:    ", data["clientId"])
  print("clientSecret:", data["clientSecret"])  # store securely — not retrievable later
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://api2.rhombussystems.com/api/oauth/submitApplication",
    {
      method: "POST",
      headers: {
        "x-auth-scheme": "api-token",
        "x-auth-apikey": "YOUR_API_KEY",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: "Acme Console",
        description: "Acme internal dashboard for Rhombus operators",
        contactEmail: "engineering@acme.example",
        redirectUri: "https://acme.example/oauth/callback",
      }),
    }
  );

  const { clientId, clientSecret } = await response.json();
  console.log("clientId:    ", clientId);
  console.log("clientSecret:", clientSecret); // store securely — not retrievable later
  ```

  ```go Go theme={null}
  package main

  import (
      "bytes"
      "encoding/json"
      "fmt"
      "net/http"
  )

  func main() {
      body, _ := json.Marshal(map[string]string{
          "name":         "Acme Console",
          "description":  "Acme internal dashboard for Rhombus operators",
          "contactEmail": "engineering@acme.example",
          "redirectUri":  "https://acme.example/oauth/callback",
      })

      req, _ := http.NewRequest("POST",
          "https://api2.rhombussystems.com/api/oauth/submitApplication",
          bytes.NewReader(body))
      req.Header.Set("x-auth-scheme", "api-token")
      req.Header.Set("x-auth-apikey", "YOUR_API_KEY")
      req.Header.Set("Content-Type", "application/json")

      resp, _ := http.DefaultClient.Do(req)
      defer resp.Body.Close()

      var result struct {
          ClientID     string `json:"clientId"`
          ClientSecret string `json:"clientSecret"`
      }
      json.NewDecoder(resp.Body).Decode(&result)
      fmt.Println("clientId:    ", result.ClientID)
      fmt.Println("clientSecret:", result.ClientSecret) // store securely
  }
  ```
</CodeGroup>

Example response:

```json theme={null}
{
  "clientId": "PJjjlcKAQPCzIcaeprzEVg",
  "clientSecret": "kixFP1l8c55dDt0WdeX8BNwUlnFknGTr9qdn3AYKpsM"
}
```

<Tip>
  Additional endpoints are available to manage registered applications: `getAllApplicationsForOrg`, `getApplicationByClientId`, `updateApplication`, and `deleteApplication`. See the [API Reference](/api-reference/overview) under the **OAuth** tag.
</Tip>

## Step 2: Build the authorization URL

Rhombus uses PKCE (Proof Key for Code Exchange) to protect against authorization code interception. For every login attempt, generate:

* A **code verifier** — a 43–128 character URL-safe random string
* A **code challenge** — the SHA-256 hash of the verifier, base64url-encoded (without padding)
* A **state** parameter — an unguessable random value used to prevent CSRF

Persist the `codeVerifier` and `state` alongside the user's session (or, for CLI tools, in process memory) until the callback lands. You'll need both.

<CodeGroup>
  ```python Python theme={null}
  import base64
  import hashlib
  import secrets
  from urllib.parse import urlencode

  def new_pkce_pair():
      verifier = secrets.token_urlsafe(64)  # 86 chars after base64
      digest = hashlib.sha256(verifier.encode("ascii")).digest()
      challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
      return verifier, challenge

  client_id    = "PJjjlcKAQPCzIcaeprzEVg"
  redirect_uri = "https://acme.example/oauth/callback"

  code_verifier, code_challenge = new_pkce_pair()
  state = secrets.token_urlsafe(32)

  # Save code_verifier + state in the user's session before redirecting.

  params = {
      "client_id":             client_id,
      "redirect_uri":          redirect_uri,
      "response_type":         "code",
      "state":                 state,
      "code_challenge":        code_challenge,
      "code_challenge_method": "S256",
  }
  authorize_url = f"https://console.rhombussystems.com/oauth/authorize?{urlencode(params)}"
  print(authorize_url)
  ```

  ```javascript JavaScript theme={null}
  import crypto from "node:crypto";

  function newPkcePair() {
    const verifier = crypto.randomBytes(64).toString("base64url");
    const challenge = crypto
      .createHash("sha256")
      .update(verifier)
      .digest("base64url");
    return { verifier, challenge };
  }

  const clientId    = "PJjjlcKAQPCzIcaeprzEVg";
  const redirectUri = "https://acme.example/oauth/callback";

  const { verifier: codeVerifier, challenge: codeChallenge } = newPkcePair();
  const state = crypto.randomBytes(32).toString("base64url");

  // Save codeVerifier + state in the user's session before redirecting.

  const params = new URLSearchParams({
    client_id: clientId,
    redirect_uri: redirectUri,
    response_type: "code",
    state,
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
  });
  const authorizeUrl = `https://console.rhombussystems.com/oauth/authorize?${params}`;
  console.log(authorizeUrl);
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/rand"
      "crypto/sha256"
      "encoding/base64"
      "fmt"
      "net/url"
  )

  func newPkcePair() (verifier, challenge string) {
      b := make([]byte, 64)
      rand.Read(b)
      verifier = base64.RawURLEncoding.EncodeToString(b)
      sum := sha256.Sum256([]byte(verifier))
      challenge = base64.RawURLEncoding.EncodeToString(sum[:])
      return
  }

  func randomState() string {
      b := make([]byte, 32)
      rand.Read(b)
      return base64.RawURLEncoding.EncodeToString(b)
  }

  func main() {
      clientID    := "PJjjlcKAQPCzIcaeprzEVg"
      redirectURI := "https://acme.example/oauth/callback"

      codeVerifier, codeChallenge := newPkcePair()
      state := randomState()

      // Save codeVerifier + state in the user's session before redirecting.

      params := url.Values{
          "client_id":             {clientID},
          "redirect_uri":          {redirectURI},
          "response_type":         {"code"},
          "state":                 {state},
          "code_challenge":        {codeChallenge},
          "code_challenge_method": {"S256"},
      }
      fmt.Println("https://console.rhombussystems.com/oauth/authorize?" + params.Encode())
  }
  ```
</CodeGroup>

<Warning>
  The authorization endpoint uses standard OAuth 2.0 parameter names — `client_id`, `redirect_uri`, `response_type=code`, `state`, `code_challenge`, and `code_challenge_method=S256`. Only `S256` is supported for the challenge method. (For backward compatibility the console also accepts the short forms `redirect` and `challenge`, but prefer the standard names shown above.)
</Warning>

Redirect the user's browser to the URL you built. They'll sign in to Rhombus and approve your application.

## Step 3: Handle the redirect callback

After the user authenticates, Rhombus redirects to your `redirectUri` with query parameters:

On success, the callback carries:

| Parameter | Description                                                                                                                                                               |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`    | Authorization code to exchange for an access token. Short-lived.                                                                                                          |
| `state`   | The `state` value you sent — **you must verify it matches**.                                                                                                              |
| `iss`     | *(when configured)* The issuer identifier ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207)). If present, validate it matches the expected authorization server. |

On failure, the callback carries `error` (e.g., `access_denied`) and `error_description` (human-readable detail) instead of `code`.

<Warning>
  Always verify the `state` parameter matches what you sent. A mismatch indicates a possible CSRF attack — abort the flow.
</Warning>

<CodeGroup>
  ```python Flask theme={null}
  from flask import Flask, request, session, abort

  app = Flask(__name__)

  @app.route("/oauth/callback")
  def callback():
      if request.args.get("error"):
          return f"Login failed: {request.args['error_description']}", 400

      code  = request.args.get("code")
      state = request.args.get("state")

      if state != session.pop("oauth_state", None):
          abort(400, "state mismatch")

      code_verifier = session.pop("oauth_verifier", None)
      # Proceed to Step 4 — exchange `code` for an access token.
      ...
  ```

  ```javascript Express theme={null}
  import express from "express";
  const app = express();

  app.get("/oauth/callback", (req, res) => {
    const { code, state, error, error_description } = req.query;

    if (error) return res.status(400).send(`Login failed: ${error_description}`);
    if (state !== req.session.oauthState) return res.status(400).send("state mismatch");

    const codeVerifier = req.session.oauthVerifier;
    // Proceed to Step 4 — exchange `code` for an access token.
  });
  ```

  ```go Go theme={null}
  // HTTP handler for your redirect URI.
  func handleCallback(w http.ResponseWriter, r *http.Request) {
      q := r.URL.Query()

      if errMsg := q.Get("error"); errMsg != "" {
          http.Error(w, "login failed: "+q.Get("error_description"), 400)
          return
      }

      code := q.Get("code")
      state := q.Get("state")

      if state != session.State(r) {
          http.Error(w, "state mismatch", 400)
          return
      }

      codeVerifier := session.Verifier(r)
      // Proceed to Step 4 — exchange `code` for an access token.
      _, _ = code, codeVerifier
  }
  ```
</CodeGroup>

## Step 4: Exchange the code for an access token

Call `POST https://auth-web.rhombussystems.com/oauth/token` with the authorization code and your PKCE verifier. This is a standard OAuth 2.0 token request: send the parameters **form-encoded** (`application/x-www-form-urlencoded`), not as JSON, and authenticate your client with its `clientId`/`clientSecret` — either via HTTP Basic auth (`client_secret_basic`) or in the request body (`client_secret_post`, shown below). This is a different host from the main API — the `/oauth/token` endpoint lives on `auth-web.rhombussystems.com`.

<CodeGroup>
  ```python Python theme={null}
  import requests

  token_response = requests.post(
      "https://auth-web.rhombussystems.com/oauth/token",
      headers={"Content-Type": "application/x-www-form-urlencoded"},
      data={
          "grant_type":    "authorization_code",
          "code":          code,
          "redirect_uri":  redirect_uri,
          "code_verifier": code_verifier,
          "client_id":     client_id,
          "client_secret": client_secret,
      },
  )
  token_response.raise_for_status()
  tokens = token_response.json()

  access_token  = tokens["access_token"]
  refresh_token = tokens["refresh_token"]
  expires_in_s  = tokens["expires_in"]
  ```

  ```javascript JavaScript theme={null}
  const tokenResponse = await fetch("https://auth-web.rhombussystems.com/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: redirectUri,
      code_verifier: codeVerifier,
      client_id: clientId,
      client_secret: clientSecret,
    }),
  });

  if (!tokenResponse.ok) {
    throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
  }

  const {
    access_token: accessToken,
    refresh_token: refreshToken,
    expires_in: expiresIn,
  } = await tokenResponse.json();
  ```

  ```go Go theme={null}
  form := url.Values{
      "grant_type":    {"authorization_code"},
      "code":          {code},
      "redirect_uri":  {redirectURI},
      "code_verifier": {codeVerifier},
      "client_id":     {clientID},
      "client_secret": {clientSecret},
  }

  req, _ := http.NewRequest("POST",
      "https://auth-web.rhombussystems.com/oauth/token",
      strings.NewReader(form.Encode()))
  req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

  resp, err := http.DefaultClient.Do(req)
  if err != nil || resp.StatusCode != 200 {
      return fmt.Errorf("token exchange failed")
  }
  defer resp.Body.Close()

  var tokens struct {
      AccessToken  string `json:"access_token"`
      RefreshToken string `json:"refresh_token"`
      TokenType    string `json:"token_type"`
      ExpiresIn    int    `json:"expires_in"`
  }
  json.NewDecoder(resp.Body).Decode(&tokens)
  ```
</CodeGroup>

Example response:

```json theme={null}
{
  "access_token": "eyJhbGciOi...",
  "refresh_token": "def50200...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

## Step 5: Call the Rhombus API

Use the access token with two headers on every Rhombus API call:

* `x-auth-scheme: api-oauth-token`
* `x-auth-access-token: <accessToken>`

This is different from the standard API key scheme (`api-token` + `x-auth-apikey`) — OAuth access tokens use their own scheme identifier so Rhombus can apply OAuth-specific authorization.

<CodeGroup>
  ```python Python theme={null}
  import requests

  headers = {
      "x-auth-scheme": "api-oauth-token",
      "x-auth-access-token": access_token,
      "Content-Type": "application/json",
  }

  response = requests.post(
      "https://api2.rhombussystems.com/api/user/getUsersInOrg",
      headers=headers,
      json={},
  )

  users = response.json().get("users", [])
  for user in users:
      print(user.get("email"), "—", user.get("name"))
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://api2.rhombussystems.com/api/user/getUsersInOrg",
    {
      method: "POST",
      headers: {
        "x-auth-scheme": "api-oauth-token",
        "x-auth-access-token": accessToken,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({}),
    }
  );

  const { users = [] } = await response.json();
  for (const user of users) {
    console.log(`${user.email} — ${user.name}`);
  }
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("POST",
      "https://api2.rhombussystems.com/api/user/getUsersInOrg",
      strings.NewReader("{}"))
  req.Header.Set("x-auth-scheme", "api-oauth-token")
  req.Header.Set("x-auth-access-token", accessToken)
  req.Header.Set("Content-Type", "application/json")

  resp, _ := http.DefaultClient.Do(req)
  defer resp.Body.Close()
  // ... decode response
  ```
</CodeGroup>

<Check>
  If this call returns a list of users, your OAuth flow is working end to end. The user has authenticated, you have an access token, and you're calling the API on their behalf.
</Check>

## Access token lifetime

The `expires_in` field on the token response tells you how long (in seconds) the access token is valid (typically one hour). When it expires, the Rhombus API will return an authentication error.

For long-lived access — background services, daemons, scheduled jobs, or any client that cannot re-prompt the user — **mint a durable API key** using the OAuth access token (see next section) rather than trying to maintain a refreshed OAuth session. This is the pattern the Rhombus CLI uses.

<Note>
  A `refresh_token` is returned alongside the access token. You can exchange it for a new access token by calling the same `/oauth/token` endpoint with `grant_type=refresh_token` and `refresh_token=<token>` (plus your client authentication). For long-lived, non-interactive access — background services, daemons, scheduled jobs — prefer minting a durable API key (see below) rather than maintaining a refreshed OAuth session. This is the pattern the Rhombus CLI uses.
</Note>

## Mint a long-lived API key

Once a user has signed in with OAuth, you can trade the short-lived access token for a permanent API key. This is what `rhombus login` does so the CLI can continue making API calls after the browser session ends.

Call `POST /api/integrations/org/submitApiTokenApplication` with `x-auth-scheme: api-oauth-token` and `x-auth-access-token: <accessToken>`:

<CodeGroup>
  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api2.rhombussystems.com/api/integrations/org/submitApiTokenApplication",
      headers={
          "x-auth-scheme": "api-oauth-token",
          "x-auth-access-token": access_token,
          "Content-Type": "application/json",
      },
      json={
          "displayName": "Acme Console — background worker",
          "authType": "API_TOKEN",
      },
  )

  data = response.json()
  api_key = data["apiKey"]  # store this securely — treat it like a password
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://api2.rhombussystems.com/api/integrations/org/submitApiTokenApplication",
    {
      method: "POST",
      headers: {
        "x-auth-scheme": "api-oauth-token",
        "x-auth-access-token": accessToken,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        displayName: "Acme Console — background worker",
        authType: "API_TOKEN",
      }),
    }
  );

  const { apiKey } = await response.json();
  // Store apiKey securely — treat it like a password.
  ```

  ```go Go theme={null}
  body, _ := json.Marshal(map[string]string{
      "displayName": "Acme Console — background worker",
      "authType":    "API_TOKEN",
  })

  req, _ := http.NewRequest("POST",
      "https://api2.rhombussystems.com/api/integrations/org/submitApiTokenApplication",
      bytes.NewReader(body))
  req.Header.Set("x-auth-scheme", "api-oauth-token")
  req.Header.Set("x-auth-access-token", accessToken)
  req.Header.Set("Content-Type", "application/json")
  // ... execute and decode; store apiKey securely.
  ```
</CodeGroup>

From that point on, use the API key with the standard `x-auth-scheme: api-token` + `x-auth-apikey` headers — no further OAuth calls required. The CLI also supports a certificate-based (mTLS) variant of this flow for higher-security deployments; see [`cmd/login.go`](https://github.com/RhombusSystems/rhombus-cli/blob/main/cmd/login.go) for the full implementation.

<Warning>
  **Treat minted API keys like passwords.** They are long-lived and confer the same permissions as the user who created them. Store them encrypted at rest, never in source control, and rotate or delete unused keys.
</Warning>

## Working with partner accounts

"Sign in with Rhombus" issues **user-scoped** OAuth tokens — they act within the organization the user belongs to. The OAuth flow does not distinguish partner accounts, and the callback does not tell you whether the user is a partner.

For **partner/MSP scenarios** — where you need to act across multiple client organizations — use the **Partner API** instead of OAuth. It relies on a partner-scoped API key:

* Send `x-auth-scheme: partner-api-token` with your partner API key
* Target a specific client organization by adding its UUID in the `x-auth-org` header on each call

See [Partner API Calls](/partner-api-calls) for the full pattern.

## Reference implementation

The [Rhombus CLI](https://github.com/RhombusSystems/rhombus-cli) is a production reference for everything in this guide. [`cmd/login.go`](https://github.com/RhombusSystems/rhombus-cli/blob/main/cmd/login.go) walks the full flow end to end: PKCE generation, local callback server, authorization URL construction, token exchange, mTLS API-key minting, and credential persistence.

If something in your implementation isn't working, diff your behavior against the CLI — it's the canonical example.

## Troubleshooting

<AccordionGroup>
  <Accordion title="state mismatch on callback">
    Your callback received a `state` value different from what you sent. Verify you're persisting the `state` you generated in Step 2 alongside the user's session (or in memory for CLI tools) and comparing it on the callback. A persistent mismatch can indicate a CSRF attempt — abort the flow rather than retrying silently.
  </Accordion>

  <Accordion title="Token exchange returns HTTP 400 or an `error` response">
    Common causes:

    * **`redirect_uri` mismatch** — the `redirect_uri` in the token exchange body must match exactly (including scheme, host, port, and path) the `redirect_uri` you sent in Step 2 and the URI registered with your OAuth application.
    * **Expired `code`** — authorization codes are short-lived (seconds, not minutes). Exchange them immediately on callback.
    * **`code_verifier` doesn't hash to `code_challenge`** — verify you're using SHA-256 and base64url encoding **without** `=` padding on both the challenge generation and the verifier transmission.
    * **Wrong content type or client auth** — the token endpoint expects `application/x-www-form-urlencoded` (not JSON), and your `clientId`/`clientSecret` must be sent via HTTP Basic auth or in the form body (`client_secret_post`). The token request does not use an `x-auth-scheme` header.
  </Accordion>

  <Accordion title="API calls return 401 with a valid access token">
    Check the headers. OAuth access tokens use `x-auth-scheme: api-oauth-token` and `x-auth-access-token: <token>`. Using `x-auth-apikey` (the API key header) with an OAuth access token will fail — those are different schemes with different header names.
  </Accordion>

  <Accordion title="Redirect lands on the console login page again with no code">
    The `client_id` you're sending may be unrecognized. Double-check you're using the `clientId` returned from `submitApplication`, not the application UUID from a different response. If you rotated applications, the old `clientId` is no longer valid.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Rhombus CLI" icon="terminal" href="/rhombus-cli">
    Read how the official CLI uses this flow end to end
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/overview">
    Browse all endpoints available once you have an access token
  </Card>

  <Card title="Rate Limits" icon="gauge" href="/rate-limits">
    Understand request limits before you ship
  </Card>

  <Card title="Developer Community" icon="users" href="https://rhombus.community">
    Request production OAuth review and ask questions
  </Card>
</Columns>
