> ## Índice de la documentación
> Consulta el índice completo de la documentación en: https://developer.rhombus.com/llms.txt
> Úsalo para descubrir todas las páginas disponibles antes de seguir explorando.

# Iniciar sesión con Rhombus

> Agrega Iniciar sesión con Rhombus a tu aplicación con OAuth 2.0 y PKCE: registra un cliente, solicita scopes, gestiona códigos de autorización, access tokens y refresh tokens, y llama a la API con un token Bearer.

> **Note**: Esta página se tradujo automáticamente. Si encuentras errores o tienes sugerencias, [escríbenos](mailto:support@rhombus.com).

## Descripción general

Rhombus soporta OAuth 2.0 con PKCE para que puedas crear aplicaciones que inicien sesión con las credenciales existentes de Rhombus. Cuando un usuario hace clic en **Iniciar sesión con Rhombus** en tu aplicación, es redirigido a la Consola de Rhombus para autenticarse, y luego es devuelto a tu URI de redirección con un código de autorización de corta duración. Intercambias ese código por un access token y llamas a la API de Rhombus en nombre del usuario.

Este es el mismo flujo utilizado por el [Rhombus CLI](https://developer.rhombus.com/es/rhombus-cli) oficial: `rhombus login` es una implementación de referencia funcional en Go que puedes leer de principio a fin.

Usa esta guía para construir:

- **Herramientas CLI** que se autentican mediante inicio de sesión por navegador (como el propio Rhombus CLI)
- **Aplicaciones web** que permiten a los usuarios de Rhombus iniciar sesión en tu servicio
- **Paneles de administración** e integraciones internas para clientes que gestionan muchas organizaciones de Rhombus
- **Aplicaciones de escritorio** usando una redirección por loopback local

> **Note**: **Lo que esta guía no es.** Esta guía es para desarrolladores externos que crean aplicaciones que inician sesión a _usuarios de Rhombus_. Si eres un cliente de Rhombus que intenta configurar **SAML SSO** para tus empleados (Okta, Azure AD, Google Workspace) o **SCIM** para el aprovisionamiento de usuarios, consulta [Aprovisionamiento SAML SSO y SCIM](https://developer.rhombus.com/es/implementations/saml-sso-provisioning) en su lugar: esa es una superficie distinta del flujo OAuth descrito aquí.

## Cómo funciona el flujo

La superficie OAuth de Rhombus abarca tres hosts. Esta es una fuente común de confusión: tu aplicación se comunica con los tres en distintas etapas del flujo.

| Rol                                                          | Host de EE. UU.               | Host de la UE                    |
| ------------------------------------------------------------ | ----------------------------- | -------------------------------- |
| Donde el usuario inicia sesión y aprueba tu aplicación       | `console.rhombussystems.com`  | `console.eu.rhombussystems.com`  |
| Endpoints de tokens, refresh, revocación y registro dinámico | `auth-web.rhombussystems.com` | `auth-web.eu.rhombussystems.com` |
| Llamadas a la API realizadas con el access token resultante  | `api2.rhombussystems.com`     | `api2.eu.rhombussystems.com`     |

> **Note**: El diagrama y los ejemplos de código de esta guía usan los hosts de EE. UU. Para las [organizaciones de la UE](https://developer.rhombus.com/es/api-regions), sustituye los hosts `.eu.` en todo el flujo. Las aplicaciones OAuth están vinculadas a la región: la llamada de registro, la URL de autorización y el intercambio de tokens deben apuntar a la misma región que la organización del usuario que inicia sesión — un desajuste falla con "This application is not available for your organization".

```mermaid
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 Bearer access token
    API->>App: Response
```

## Antes de comenzar

> **Info**: Antes de comenzar, asegúrate de tener:
>
> - Una **cuenta de Rhombus**, más una clave de API (generada en la [Consola de Rhombus](https://console.rhombussystems.com/settings/integrations/api-tokens/) en **Configuración → Integraciones y recursos para desarrolladores → Tokens de API**) si registras tu aplicación con la API; el [registro dinámico de clientes](https://developer.rhombus.com/es/oauth-authentication#registro-dinámico-de-clientes) no necesita clave
> - Una **URI de redirección** que tu aplicación controle: una URL HTTPS pública en producción, o `http://localhost:<port>/callback` para aplicaciones CLI y de escritorio
> - Familiaridad básica con el **flujo de código de autorización OAuth 2.0** y **PKCE** ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636))

> **Note**: La persona que autoriza tu aplicación necesita un rol que pueda gestionar claves de API (o acceso completo). Los usuarios sin ese permiso pueden iniciar sesión en la consola, pero no pueden aprobar la aplicación.

## Paso 1: Registra tu aplicación

Antes de poder iniciar un flujo OAuth, Rhombus necesita conocer tu aplicación. El registro te entrega un `clientId` y, para un cliente confidencial, un `clientSecret`. Un cliente público (una CLI o una app de escritorio que no puede guardar un secreto) se registra con `tokenEndpointAuthMethod: "none"` y no recibe ningún secreto.

Hay dos formas de obtener un `clientId`:

- **[Registrarse con la API](https://developer.rhombus.com/es/oauth-authentication#registrarse-con-la-api)**: Llama a `submitApplication` con una clave de API de tu organización. Más adelante puedes listar, actualizar y eliminar la aplicación con los endpoints de OAuth.
- **[Registro dinámico de clientes](https://developer.rhombus.com/es/oauth-authentication#registro-dinámico-de-clientes)**: La aplicación se registra sola en `/oauth/register`, sin clave de API. Pensado para herramientas de línea de comandos, aplicaciones de escritorio y clientes MCP que se registran en su primera ejecución.

### Registrarse con la API

[`POST /api/oauth/submitApplication`](https://developer.rhombus.com/api-reference/oauth-webservice/submit-oauth-application) devuelve un `clientId` y `clientSecret` nuevos. **Almacena el `clientSecret` de forma segura**: no se puede recuperar más tarde.

```python Python
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
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);
await saveToSecretsManager("rhombus-oauth-client-secret", clientSecret); // shown only once; never log it
```

```go Go
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
}
```

Ejemplo de respuesta:

```json
{
  "clientId": "PJjjlcKAQPCzIcaeprzEVg",
  "clientSecret": "YOUR_CLIENT_SECRET"
}
```

> **Tip**: Hay endpoints adicionales disponibles para gestionar aplicaciones registradas: `getAllApplicationsForOrg`, `getApplicationByClientId`, `updateApplication` y `deleteApplication`. Consulta la [Referencia de la API](https://developer.rhombus.com/api-reference/oauth-webservice/get-all-applications-for-organization) bajo la etiqueta **OAuth**. Eliminar una aplicación revoca todas las autorizaciones que los usuarios le dieron.

### Registro dinámico de clientes

Una aplicación también puede registrarse sola con el registro dinámico de clientes de OAuth 2.0 ([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)): envía un cuerpo JSON a `https://auth-web.rhombussystems.com/oauth/register`, sin clave de API. Registra un cliente público (`"token_endpoint_auth_method": "none"`) cuando la aplicación no pueda guardar un secreto; en ese caso depende solo de PKCE.

```bash cURL
curl -X POST "https://auth-web.rhombussystems.com/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Acme CLI",
    "redirect_uris": ["http://localhost:8765/callback"],
    "token_endpoint_auth_method": "none"
  }'
```

La respuesta incluye el nuevo `client_id`, y un `client_secret` salvo que hayas registrado un cliente público. Guarda el `client_id` y reutilízalo en los siguientes inicios de sesión en lugar de registrarte de nuevo.

El registro anónimo está limitado a 10 registros por hora desde una misma dirección IP de cliente y a 100 por hora en total. Si se supera cualquiera de los dos límites, el endpoint devuelve `429 Too Many Requests` con un encabezado `Retry-After` y un cuerpo de error de OAuth cuyo `error` es `temporarily_unavailable`; espera los segundos que indica `Retry-After` antes de volver a intentarlo.

## Paso 2: Construye la URL de autorización

Rhombus usa PKCE (Proof Key for Code Exchange) para protegerse contra la interceptación de códigos de autorización. Para cada intento de inicio de sesión, genera:

- Un **code verifier**: una cadena aleatoria URL-safe de 43 a 128 caracteres
- Un **code challenge**: el hash SHA-256 del verifier, codificado en base64url (sin padding)
- Un parámetro **state**: un valor aleatorio impredecible utilizado para prevenir CSRF

Conserva el `codeVerifier` y el `state` junto con la sesión del usuario (o, para herramientas CLI, en memoria del proceso) hasta que llegue el callback. Necesitarás ambos.

Indica en `scope` el acceso que necesita tu aplicación, separado por espacios (consulta [Scopes](https://developer.rhombus.com/es/oauth-authentication#scopes)). Los ejemplos solicitan los tres scopes porque más adelante esta guía emite una clave de API; una aplicación que solo lee solicita `scope=read`.

```python Python
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",
    "scope":                 "read write physical",
    "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
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",
  scope: "read write physical",
  state,
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
});
const authorizeUrl = `https://console.rhombussystems.com/oauth/authorize?${params}`;
console.log(authorizeUrl);
```

```go Go
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"},
        "scope":                 {"read write physical"},
        "state":                 {state},
        "code_challenge":        {codeChallenge},
        "code_challenge_method": {"S256"},
    }
    fmt.Println("https://console.rhombussystems.com/oauth/authorize?" + params.Encode())
}
```

> **Warning**: El endpoint de autorización usa los nombres de parámetros estándar de OAuth 2.0: `client_id`, `redirect_uri`, `response_type=code`, `scope`, `state`, `code_challenge` y `code_challenge_method=S256`. Solo se admite `S256` como método de challenge. (Por compatibilidad, la consola también acepta las formas abreviadas `redirect` y `challenge`, pero prefiere los nombres estándar mostrados arriba.)

Redirige el navegador del usuario a la URL que construiste. El usuario iniciará sesión en Rhombus y aprobará tu aplicación.

## Paso 3: Maneja el callback de redirección

Después de que el usuario se autentica, Rhombus redirige a tu `redirectUri` con parámetros de consulta:

En caso de éxito, el callback incluye:

| Parámetro | Descripción                                                                                                                                                                                          |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`    | Código de autorización para intercambiar por un access token. De corta duración.                                                                                                                     |
| `state`   | El valor de `state` que enviaste — **debes verificar que coincida**.                                                                                                                                 |
| `iss`     | _(cuando está configurado)_ El identificador del emisor ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207)). Si está presente, valida que coincida con el servidor de autorización esperado. |

En caso de fallo, el callback incluye `error` (p. ej., `access_denied`) y `error_description` (detalle legible para humanos) en lugar de `code`.

> **Warning**: Verifica siempre que el parámetro `state` coincida con lo que enviaste. Una falta de coincidencia indica un posible ataque CSRF: aborta el flujo.

```python Flask
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
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
// 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
}
```

## Paso 4: Intercambia el código por un access token

Llama a `POST https://auth-web.rhombussystems.com/oauth/token` con el código de autorización y tu verifier PKCE. Esta es una solicitud de token OAuth 2.0 estándar: envía los parámetros **codificados en formulario** (`application/x-www-form-urlencoded`), no como JSON, y autentica tu cliente con su `clientId`/`clientSecret`, ya sea mediante HTTP Basic auth (`client_secret_basic`) o en el cuerpo de la solicitud (`client_secret_post`, mostrado abajo). Un cliente público (registrado con `token_endpoint_auth_method` `none`) no tiene secreto y envía solo `client_id`. Este es un host distinto del de la API principal: el endpoint `/oauth/token` reside en `auth-web.rhombussystems.com`.

```python Python
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"]
granted_scope = tokens["scope"]
```

```javascript JavaScript
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,
  scope: grantedScope,
} = await tokenResponse.json();
```

```go Go
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"`
    Scope        string `json:"scope"`
}
json.NewDecoder(resp.Body).Decode(&tokens)
```

Ejemplo de respuesta:

```json
{
  "access_token": "AAAAAAAAAAAAAAAAAAAAAA",
  "refresh_token": "BBBBBBBBBBBBBBBBBBBBBB",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read write physical"
}
```

`scope` lista los scopes que tiene el token, separados por espacios.

## Paso 5: Llama a la API de Rhombus

Envía el access token en el encabezado estándar en cada llamada a la API de Rhombus:

- `Authorization: Bearer <accessToken>`

Un token Bearer sin encabezado `x-auth-scheme` se interpreta como un access token OAuth, y `Authorization: Bearer` no transporta ningún otro tipo de credencial. El mismo encabezado funciona en la API REST (`api2`), en los hosts de medios y en el handshake del [WebSocket de eventos](https://developer.rhombus.com/es/websocket/authentication). También se acepta el par de encabezados equivalente `x-auth-scheme: api-oauth-token` y `x-auth-access-token: <accessToken>`.

```python Python
import requests

headers = {
    "Authorization": f"Bearer {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
const response = await fetch(
  "https://api2.rhombussystems.com/api/user/getUsersInOrg",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${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
req, _ := http.NewRequest("POST",
    "https://api2.rhombussystems.com/api/user/getUsersInOrg",
    strings.NewReader("{}"))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")

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

> **Check**: Si esta llamada devuelve una lista de usuarios, tu flujo OAuth funciona de extremo a extremo. El usuario se autenticó, tienes un access token y estás llamando a la API en su nombre.

## Scopes

Un scope limita lo que puede hacer un access token. Solicita los scopes en el parámetro `scope` de la URL de autorización, separados por espacios (por ejemplo, `scope=read%20write`). Los valores que Rhombus no reconoce se ignoran, y una solicitud sin `scope` recibe los tres. La pantalla de consentimiento lista los scopes que pide tu aplicación:

| Scope      | Pantalla de consentimiento                                                                 | Qué cubre                                                                                                                                                                                                                                                                          |
| ---------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read`     | **Read**: ver cámaras, grabaciones y configuración                                         | Operaciones cuyo nombre empieza por `get`, `find`, `search`, `list`, `export`, `count`, `query`, `lookup`, `fetch`, `download`, `view`, `describe` o `preview`; medios y video en vivo; el WebSocket de eventos                                                                    |
| `write`    | **Write**: cambiar configuración y datos                                                   | Todas las demás operaciones                                                                                                                                                                                                                                                        |
| `physical` | **Physical**: desbloquear puertas y ascensores, confinamiento, alarmas, altavoces y robots | Desbloqueos de puertas y ascensores (incluidos los de integraciones), confinamiento (lockdown), anulaciones de estado, relés, first-in, armar y desarmar, escalar a alarma, reproducción en altavoces, el socket en vivo del robot y el envío de audio de intercomunicación (talk) |

La página de cada operación en la [referencia de la API](https://developer.rhombus.com/api-reference/overview) muestra el scope que necesita.

- **Los scopes nunca añaden acceso.** Un token solo puede hacer lo que permiten a la vez el rol del usuario y sus scopes (consulta [Scopes de OAuth](https://developer.rhombus.com/es/authentication/permissions#scopes-de-oauth)).
- **El WebSocket de eventos necesita `read`.** Un token sin ese scope se rechaza en el handshake.
- **El audio de intercomunicación se comprueba en cada mensaje.** Enviar audio sin `physical` cierra el socket con el código 1008.
- **Crear, rotar y renovar claves de API requiere los tres scopes** (consulta [Emite una API key de larga duración](https://developer.rhombus.com/es/oauth-authentication#emite-una-api-key-de-larga-duración)).
- **Las respuestas de tokens incluyen `scope`**, los scopes que tiene el token. El documento de descubrimiento del servidor de autorización (`https://auth-web.rhombussystems.com/.well-known/oauth-authorization-server`) y el documento de recurso protegido de la API (`https://api2.rhombussystems.com/.well-known/oauth-protected-resource`) listan los scopes admitidos en `scopes_supported`.

Una llamada que necesita un scope que el token no tiene devuelve `403` con `errorCode` `PERMISSION_REQUIRED` y un encabezado que indica el scope necesario:

```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="physical"
```

Para obtener el scope que falta, vuelve a llevar al usuario por el flujo de autorización con ese scope añadido a `scope`.

## Tiempo de vida del access token

El campo `expires_in` en la respuesta de tokens te indica cuánto tiempo (en segundos) es válido el access token (típicamente una hora). Cuando expira, la API de Rhombus devuelve `401` con `errorCode` `CREDENTIAL_EXPIRED`; [refréscalo](https://developer.rhombus.com/es/oauth-authentication#refresh-tokens).

Un cliente que guarda el refresh token mantiene el acceso mientras siga refrescando; la CLI de Rhombus funciona así. Un servicio en segundo plano, un daemon o un trabajo programado que no deba depender del inicio de sesión de un usuario puede, en cambio, **emitir una clave de API** con el access token (consulta [Emite una API key de larga duración](https://developer.rhombus.com/es/oauth-authentication#emite-una-api-key-de-larga-duración)).

## Refresh tokens

Intercambia el refresh token por un nuevo access token en el mismo endpoint `/oauth/token`, codificado como formulario:

```bash cURL
curl -X POST "https://auth-web.rhombussystems.com/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN" \
  -d client_id="$CLIENT_ID" \
  -d client_secret="$CLIENT_SECRET"   # solo clientes confidenciales
```

- **Los clientes públicos** (registrados con `token_endpoint_auth_method` `none`, protegidos solo por PKCE) envían solo `client_id`. **Los clientes confidenciales** envían también `client_secret`, en el cuerpo o con HTTP Basic auth.
- **Cada refresh rota el refresh token.** La respuesta incluye un nuevo `refresh_token`; guárdalo, porque el que enviaste deja de funcionar.
- **Nunca reutilices un refresh token reemplazado.** Dentro de los 30 segundos posteriores a su reemplazo se rechaza. Pasados 30 segundos, presentarlo revoca toda la autorización, incluidas las claves de API emitidas con ella, y el usuario tiene que volver a iniciar sesión. Si varios procesos comparten una misma sesión, deja que solo uno de ellos refresque.
- **Un refresh token sin usar durante 90 días expira.**
- **Un refresh fallido devuelve `400`** con el error de OAuth `invalid_grant`: el refresh token se reemplazó, expiró o se revocó. Vuelve a llevar al usuario por el flujo de inicio de sesión.

Los access tokens y los refresh tokens son cadenas opacas, no JWT.

## Revocar una autorización

Para terminar una autorización, envía el refresh token a `https://auth-web.rhombussystems.com/oauth/token/revoke`, codificado como formulario, con `token`, `client_id` y, en un cliente confidencial, `client_secret`. Revocar una autorización:

- Detiene sus tokens en un plazo de 60 segundos en todos los hosts. Las conexiones abiertas al WebSocket de eventos reciben un frame STOMP `ERROR` y se cierran con el código 1008; los sockets de medios abiertos (video en vivo, intercomunicación, robot, PTZ) se cierran con el código 1008.
- Elimina las claves de API emitidas con esa autorización.
- Termina los [tokens federados](https://developer.rhombus.com/es/authentication/federated-tokens) emitidos con esa autorización.

Eliminar la aplicación revoca todas sus autorizaciones. Consulta también [Permisos y rotación de claves](https://developer.rhombus.com/es/authentication/permissions#tokens-de-oauth).

## Emite una API key de larga duración

Una vez que un usuario ha iniciado sesión con OAuth, puedes intercambiar el access token de corta duración por una clave de API de larga duración, para un servicio que siga llamando a la API sin la sesión del usuario.

Llama a `POST /api/integrations/org/submitApiTokenApplication` con `Authorization: Bearer <accessToken>`. El token necesita los tres scopes (`read`, `write` y `physical`). El cuerpo indica el rol de la nueva clave en `permissionGroupUuid`: el rol del propio usuario, o uno que tenga permitido asignar. Lista los roles con [`getPermissionGroups`](https://developer.rhombus.com/api-reference/permission-webservice/get-permission-groups) y los roles asignables del usuario con [`getPermissionsForCurrentUser`](https://developer.rhombus.com/api-reference/permission-webservice/get-permissions-for-current-user). Para que la clave caduque, añade `expiresAtMs` (milisegundos desde epoch). Algunos rechazos, como un rol que el usuario no puede asignar, devuelven HTTP 200 con `"error": true` y un `errorMsg`, así que revisa el cuerpo además del código de estado.

```python Python
import requests

response = requests.post(
    "https://api2.rhombussystems.com/api/integrations/org/submitApiTokenApplication",
    headers={
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
    },
    json={
        "displayName": "Acme Console — background worker",
        "permissionGroupUuid": role_uuid,
        "authType": "API_TOKEN",
    },
)

data = response.json()
if data.get("error"):
    raise RuntimeError(data.get("errorMsg"))
api_key = data["apiKey"]  # store this securely — treat it like a password
```

```javascript JavaScript
const response = await fetch(
  "https://api2.rhombussystems.com/api/integrations/org/submitApiTokenApplication",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      displayName: "Acme Console — background worker",
      permissionGroupUuid: roleUuid,
      authType: "API_TOKEN",
    }),
  }
);

const data = await response.json();
if (data.error) throw new Error(data.errorMsg);
const { apiKey } = data; // store apiKey securely — treat it like a password
```

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

req, _ := http.NewRequest("POST",
    "https://api2.rhombussystems.com/api/integrations/org/submitApiTokenApplication",
    bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
// ... execute and decode; store apiKey securely.
```

A partir de ese punto, usa la clave de API con los encabezados estándar `x-auth-scheme: api-token` + `x-auth-apikey`: no se requieren más llamadas OAuth. Para una clave de certificado (mTLS), envía `"authType": "CERT"` con un `csr` cuya firma sea válida; consulta [Claves de API y certificados](https://developer.rhombus.com/es/authentication/api-keys#claves-de-certificado-mtls). El mismo access token puede rotar una clave (`POST /api/integrations/org/rotateApiToken`) o renovar el certificado de una clave de certificado (`POST /api/integrations/org/renewApiTokenCert`), con los mismos tres scopes; consulta [Gestionar claves con la API](https://developer.rhombus.com/es/authentication/api-keys#gestionar-claves-con-la-api).

> **Warning**: **Trata las claves de API emitidas como contraseñas.** Actúan con el rol que indicaste en `permissionGroupUuid` y funcionan hasta que las eliminas o hasta que pasa su `expiresAtMs`. Revocar la autorización OAuth con la que se emitieron las elimina. Almacénalas cifradas en reposo, nunca en el control de versiones, y rota o elimina las claves no utilizadas.

## Trabajar con cuentas partner

Los usuarios de una organización partner (MSP o revendedor) pueden completar el mismo flujo de "Iniciar sesión con Rhombus", y su access token va en el mismo encabezado `Authorization: Bearer <accessToken>`. (`x-auth-scheme: partner-api-oauth-token` con `x-auth-access-token` se acepta como alias.)

Para actuar dentro de una organización cliente gestionada, agrega `x-auth-org: <clientOrgUuid>`, exactamente igual que con una clave de API de partner. Es solo un encabezado, y un valor vacío cuenta como ausente. Un token de partner que llama a un endpoint a nivel de cliente sin `x-auth-org` actúa en la organización partner, y la respuesta incluye los encabezados `Deprecation` y `Sunset`: consulta [Llamadas a la API Partner](https://developer.rhombus.com/es/partner-api-calls#llamadas-sin-x-auth-org).

El callback no indica si el usuario es partner. Si llamas a `POST /api/integrations/org/submitApiTokenApplication` con el token de un usuario partner, la respuesta es `403 Forbidden`. Vuelve a intentarlo con el endpoint de partner que se muestra abajo. La [CLI de Rhombus](https://developer.rhombus.com/es/rhombus-cli) detecta las cuentas partner de la misma forma al iniciar sesión, y `rhombus login --partner` omite esa comprobación.

### Emite una clave de API de partner

Llama a `POST /api/partner/submitApiTokenApplication` con el access token, sin `x-auth-org`. El cuerpo es el mismo que el del endpoint de organización. El token necesita los tres scopes, y el rol de partner del usuario debe incluir la administración de la API.

```bash cURL
curl -X POST "https://api2.rhombussystems.com/api/partner/submitApiTokenApplication" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"displayName": "Acme MSP integration", "permissionGroupUuid": "PARTNER_ROLE_UUID", "authType": "API_TOKEN"}'
```

La respuesta contiene `apiKey`. Úsala con `x-auth-scheme: api-token` y `x-auth-apikey`. Para actuar dentro de una organización cliente gestionada, agrega `x-auth-org: <clientOrgUuid>`. Consulta [Llamadas a la API Partner](https://developer.rhombus.com/es/partner-api-calls), que también explica cómo rotar y renovar claves de partner.

## Implementación de referencia

El [Rhombus CLI](https://github.com/RhombusSystems/rhombus-cli) es una referencia de producción para todo lo de esta guía. `rhombus login` inicia sesión con OAuth y mantiene vigentes los tokens del perfil en lugar de crear una clave de API. [`cmd/login.go`](https://github.com/RhombusSystems/rhombus-cli/blob/main/cmd/login.go) recorre todo el flujo de extremo a extremo: registro dinámico como cliente público, generación de PKCE, servidor de callback local, construcción de la URL de autorización, intercambio de tokens y almacenamiento de los tokens; la CLI los refresca antes de que expiren.

Si algo en tu implementación no funciona, compara tu comportamiento contra el CLI: es el ejemplo canónico.

## Solución de problemas

**state no coincide en el callback**

Tu callback recibió un valor de `state` distinto al que enviaste. Verifica que estás conservando el `state` que generaste en el Paso 2 junto con la sesión del usuario (o en memoria para herramientas CLI) y comparándolo en el callback. Una falta de coincidencia persistente puede indicar un intento de CSRF: aborta el flujo en lugar de reintentar silenciosamente.

**El intercambio de tokens devuelve HTTP 400 o una respuesta con \`error\`**

Causas comunes:

- **`redirect_uri` no coincide**: el `redirect_uri` en el cuerpo del intercambio de tokens debe coincidir exactamente (incluyendo esquema, host, puerto y ruta) con el `redirect_uri` que enviaste en el Paso 2 y la URI registrada con tu aplicación OAuth.
- **`code` expirado o reutilizado**: los códigos de autorización expiran a los 5 minutos y solo funcionan una vez. Intercámbialos en el callback.
- **`code_verifier` no genera el hash de `code_challenge`**: verifica que estás usando SHA-256 y codificación base64url **sin** padding `=` tanto en la generación del challenge como en la transmisión del verifier.
- **Tipo de contenido o autenticación de cliente incorrectos**: el endpoint de tokens espera `application/x-www-form-urlencoded` (no JSON). Un cliente confidencial envía su `clientId`/`clientSecret` mediante HTTP Basic auth o en el cuerpo del formulario (`client_secret_post`); un cliente público envía solo `client_id`. La solicitud de token no usa un encabezado `x-auth-scheme`.

**El refresh devuelve 400 invalid_grant**

El refresh token fue reemplazado por un refresh anterior, pasó 90 días sin usarse o su autorización se revocó: mediante el endpoint de revocación, al eliminar la aplicación o porque se presentó un refresh token reemplazado más de 30 segundos después de su reemplazo. Guarda siempre el `refresh_token` más reciente de cada respuesta. Cuando un refresh falla, vuelve a llevar al usuario por el flujo de inicio de sesión.

**Las llamadas a la API fallan con 401 o 403 y un access token**

Envía el token como `Authorization: Bearer <token>` (o con `x-auth-scheme: api-oauth-token` y `x-auth-access-token`), nunca en `x-auth-apikey`. Un **401** significa que el token falta, está mal formado, es desconocido, expiró o fue revocado; `errorCode` indica cuál (`UNAUTHENTICATED`, `INVALID_CREDENTIAL`, `CREDENTIAL_EXPIRED`, `CREDENTIAL_REVOKED`). Refresca un token expirado. Un **403** con `PERMISSION_REQUIRED` significa que el token es válido, pero el rol del usuario o los scopes del token no permiten la llamada; cuando falta un scope, el encabezado `WWW-Authenticate` lo indica (consulta [Scopes](https://developer.rhombus.com/es/oauth-authentication#scopes)). Consulta [Cómo interpretar los errores de autenticación](https://developer.rhombus.com/es/authentication#cómo-interpretar-los-errores-de-autenticación).

**La redirección termina en la página de inicio de sesión de la consola otra vez sin código**

El `client_id` que estás enviando puede no ser reconocido. Verifica que estás usando el `clientId` devuelto por `submitApplication`, no el UUID de aplicación de una respuesta diferente. Si rotaste aplicaciones, el `clientId` antiguo ya no es válido.

## Próximos pasos

- **[Rhombus CLI](https://developer.rhombus.com/es/rhombus-cli)**: Lee cómo el CLI oficial usa este flujo de extremo a extremo
- **[Referencia de la API](https://developer.rhombus.com/api-reference/overview)**: Navega todos los endpoints disponibles una vez que tengas un access token
- **[Límites de tasa](https://developer.rhombus.com/es/rate-limits)**: Comprende los límites de solicitudes antes de lanzar
- **[Soporte de Rhombus](mailto:support@rhombus.com)**: Obtén ayuda con tu integración.
