OAuth para apps
Tu app se conecta a la cuenta de Cord de un negocio con un clic, sin que nadie copie una llave.
Cuándo usarlo
Si construyes una app que muchos negocios van a conectar con su Cord —un conector, un complemento, una plataforma de automatización—, usa OAuth 2.0 en lugar de pedirles una llave de API. La persona autoriza en una pantalla de Cord, elige el espacio de trabajo y puede revocar el acceso cuando quiera. Es el mismo mecanismo que usan las apps de Cord en Zapier y Make, y el complemento de Cord para Gmail.
Para una integración de tu propio negocio, una llave de API es más simple.
Registrar tu app
El registro de clientes OAuth todavía no es de autoservicio. Escríbenos a contacto@cordhq.app con el nombre de tu app, su dominio y las direcciones de regreso (redirect_uri) exactas. Te damos un client_id y un client_secret. Guarda el secreto en tu servidor: Cord solo conserva su huella y no puede volver a mostrarlo.
Las direcciones de regreso deben ser https (o http://localhost para desarrollo) y se comparan exactas, sin comodines.
1. Pide la autorización
Manda a la persona a:
https://cordhq.app/oauth/authorize
?response_type=code
&client_id=cord_oc_...
&redirect_uri=https://tuapp.com/cord/callback
&scope=write
&state=VALOR_ALEATORIO
&code_challenge=... (opcional, PKCE)
&code_challenge_method=S256 (opcional, PKCE)
| Parámetro | Detalle |
|---|---|
scope |
read para solo lectura; write para lectura y escritura (incluye read). Sin scope, Cord pide read. Puedes separar valores con espacio, coma o +; cualquier otro valor responde invalid_scope. |
state |
Un valor aleatorio que tu app verifica al regreso. Evita que un tercero te inyecte una autorización. |
code_challenge |
PKCE: BASE64URL(SHA256(code_verifier)), 43 caracteres sin relleno. Recomendado siempre; solo se acepta code_challenge_method=S256. |
Si la persona no tiene sesión, Cord le pide iniciarla y regresa a la misma pantalla. Solo quien tiene el permiso de Ajustes en un espacio de trabajo puede autorizar apps en él. La pantalla muestra qué podrá hacer tu app y qué no.
Al autorizar, Cord regresa a tu redirect_uri con ?code=...&state=.... Si la persona cancela, regresa con ?error=access_denied&state=....
2. Canjea el código
El código dura 5 minutos y sirve una sola vez. Canjéalo desde tu servidor:
curl -X POST "https://cordhq.app/api/oauth/token" \
-d grant_type=authorization_code \
-d code=cord_ac_... \
-d redirect_uri=https://tuapp.com/cord/callback \
-d client_id=cord_oc_... \
-d client_secret=cord_cs_... \
-d code_verifier=...
También puedes mandar las credenciales del cliente con autenticación HTTP Basic.
{ "access_token": "cord_at_...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "cord_rt_...", "scope": "write" }
3. Llama a la API
El access_token funciona como una llave en toda la API v1 y en el servidor MCP:
curl "https://cordhq.app/api/v1/me" -H "Authorization: Bearer cord_at_..."
4. Renueva el acceso
El access token dura 1 hora. Renuévalo con el refresh token, que dura 180 días:
curl -X POST "https://cordhq.app/api/oauth/token" \
-d grant_type=refresh_token \
-d refresh_token=cord_rt_... \
-d client_id=cord_oc_... \
-d client_secret=cord_cs_...
Cada renovación entrega un refresh token nuevo y el anterior deja de servir. Guarda siempre el nuevo. Si un refresh token ya usado se presenta de nuevo pasados 30 segundos, Cord lo trata como posible robo y revoca la conexión completa; la persona tendrá que volver a autorizar.
5. Revoca
curl -X POST "https://cordhq.app/api/oauth/revoke" \
-d token=cord_rt_... -d client_id=cord_oc_... -d client_secret=cord_cs_...
Responde 200 aunque el token no exista, para no revelar qué tokens son válidos. La persona también puede revocar tu app en la pestaña Llaves del Cord Workbench, donde aparece como Conexión autorizada.
PKCE
PKCE evita que un código interceptado se pueda canjear. Genera un code_verifier aleatorio de 43 a 128 caracteres (A-Z, a-z, 0-9, -, ., _, ~), guárdalo en la sesión de tu servidor y manda su huella al autorizar:
import { createHash, randomBytes } from 'node:crypto';
const codeVerifier = randomBytes(48).toString('base64url'); // 64 caracteres
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url'); // 43 caracteres
// En /oauth/authorize: code_challenge=<codeChallenge>&code_challenge_method=S256
// En /api/oauth/token: code_verifier=<codeVerifier>
Si mandaste code_challenge al autorizar, el canje exige el code_verifier que le corresponde. Si no lo mandaste, el canje no debe llevar code_verifier: uno inesperado también responde invalid_grant.
Errores de la autorización
Hay dos clases de error en /oauth/authorize, y se comportan distinto a propósito.
Cord no regresa a tu app cuando no puede confiar en la dirección de regreso. La persona ve una página de error de Cord y nada se redirige:
| Situación | Lo que pasa |
|---|---|
client_id que no existe |
Página de error de Cord (unknown_client). |
redirect_uri que no coincide exacta con una registrada |
Página de error de Cord. Revisa mayúsculas, barra final y puerto. |
Cord regresa a tu redirect_uri con error y tu state cuando el cliente y la dirección son válidos pero la petición no:
error |
Cuándo |
|---|---|
unsupported_response_type |
response_type no es code. |
invalid_scope |
scope trae un valor distinto de read o write. |
invalid_request |
code_challenge mal formado, o code_challenge_method distinto de S256. |
access_denied |
La persona canceló, o eligió un espacio de trabajo en el que no puede autorizar apps. |
https://tuapp.com/cord/callback?error=invalid_scope&state=VALOR_ALEATORIO
Estas redirecciones no llevan error_description. Verifica siempre que state sea el que mandaste antes de mostrar el error.
Errores del canje y la renovación
POST /api/oauth/token acepta el cuerpo como application/x-www-form-urlencoded o como JSON, y responde siempre JSON con error y error_description:
{ "error": "invalid_grant", "error_description": "El código o el token no es válido, ya se usó o venció." }
| Código | error |
error_description |
Cuándo |
|---|---|---|---|
401 |
invalid_client |
Las credenciales de la aplicación no son válidas. | client_id o client_secret incorrectos, por Basic o en el cuerpo. |
400 |
invalid_request |
El cuerpo de la petición no se pudo leer. | El cuerpo no es un formulario ni JSON válido. |
400 |
invalid_request |
Faltan code o redirect_uri. | Canje sin code o sin redirect_uri. |
400 |
invalid_request |
Falta refresh_token. | Renovación sin refresh_token. |
400 |
invalid_request |
Este espacio ya tiene demasiadas conexiones de esta aplicación… | El espacio de trabajo ya tiene 25 conexiones vivas de tu app. La persona debe revocar las que no use. |
400 |
unsupported_grant_type |
Solo se admite authorization_code y refresh_token. | Cualquier otro grant_type. |
400 |
invalid_grant |
El código o el token no es válido, ya se usó o venció. | Código vencido o usado, redirect_uri distinta a la de la autorización, code_verifier que no cuadra, o refresh token vencido, usado o de otro cliente. |
429 |
— | — | Más de 300 peticiones por minuto desde la misma IP. Respeta Retry-After. |
error_description está en español y puede cambiar: decide por error.
POST /api/oauth/revoke responde 401 con error: "invalid_client" si las credenciales del cliente no son válidas, y 200 en cualquier otro caso.
Cuota
Las llamadas con un token de OAuth cuentan contra la cuota de API del plan del negocio, igual que una llave. La única excepción es el complemento de Cord para Gmail, que es una superficie de la propia Cord: sus llamadas no consumen la cuota mensual, aunque sí los límites por minuto.