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

# Conectar un negocio

> El flujo de autorización paso a paso: la URL de conexión con PKCE y state, el regreso a tu web, el canje del código y la primera llamada a la API.

Así conecta un propietario tu app a su negocio. Tu web lo manda a Craqui, el propietario elige el negocio y los
permisos, Craqui lo devuelve a tu web con un código, y tu servidor cambia ese código por tokens.

```
Tu web ──▶ https://www.craqui.com/oauth/authorize?…        el propietario elige negocio y permisos
       ◀── https://app.example.com/craqui/callback?code=…&state=…&iss=https://api.craqui.com
Tu servidor ──POST https://api.craqui.com/oauth/token──▶  { access_token, refresh_token, business_id, … }
Tu servidor ──GET  https://api.craqui.com/v1/business──▶  Authorization: Bearer crq_oat_…
```

Necesitas el identificador y el secreto de tu app, y que la `redirect_uri` que vas a usar esté entre sus
direcciones de retorno. Lee [Crea una app](https://docs.craqui.com/apps/register.md).

## Los datos del servidor de autorización

| | |
| - | - |
| Issuer | `https://api.craqui.com` |
| Pantalla de conexión (`authorization_endpoint`) | `https://www.craqui.com/oauth/authorize` |
| Tokens (`token_endpoint`) | `https://api.craqui.com/oauth/token` |
| Revocación (`revocation_endpoint`) | `https://api.craqui.com/oauth/revoke` |
| Metadata (RFC 8414) | `https://api.craqui.com/.well-known/oauth-authorization-server` |
| Metadata del recurso (RFC 9728) | `https://api.craqui.com/.well-known/oauth-protected-resource/v1` |
| Grant types | `authorization_code` y `refresh_token` |
| PKCE | Obligatorio, solo `S256` |
| Autenticación de tu app | `client_secret_basic` o `client_secret_post` |

Si tu librería de OAuth descubre el servidor sola, dale el issuer `https://api.craqui.com` y el tipo de metadata
de OAuth 2.0 (`oauth-authorization-server`), no el de OpenID Connect: Craqui no emite `id_token`.

## 1. Prepara el pedido en tu servidor

Cuando el propietario toca «Conectar con Craqui» en tu web, tu servidor genera tres valores nuevos para ese
intento y los guarda en la sesión de esa persona (del lado del servidor, nunca en una cookie legible ni en el
`localStorage`):

* **`code_verifier`**: un valor aleatorio de 43 a 128 caracteres (`A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`).
* **`code_challenge`**: el SHA-256 del `code_verifier`, en base64url y sin `=`.
* **`state`**: un valor aleatorio que ata la respuesta a esta sesión. Craqui no lo exige, pero sin él tu web
  queda expuesta a que alguien le haga conectar a tu usuario el negocio de otra persona (CSRF). Mándalo
  siempre.

Con `openssl`, para probar desde la terminal:

```bash theme={null}
CODE_VERIFIER=$(openssl rand -hex 32)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 16)
```

## 2. Manda al propietario a la pantalla de conexión

Redirige el navegador a `https://www.craqui.com/oauth/authorize` con estos parámetros:

| Parámetro | Valor |
| - | - |
| `response_type` | `code` |
| `client_id` | El identificador de tu app (`app_…`) |
| `redirect_uri` | Una de tus direcciones de retorno, **exactamente** como la registraste |
| `scope` | Los permisos que pides, separados por espacios. `business:read` va siempre, aunque no lo pidas |
| `state` | El `state` del paso 1 |
| `code_challenge` | El `code_challenge` del paso 1 |
| `code_challenge_method` | `S256` |
| `resource` | Opcional. Si lo mandas, tiene que ser `https://api.craqui.com/v1` |

```
https://www.craqui.com/oauth/authorize
  ?response_type=code
  &client_id=app_01jsy4wejjfcesyg3c5t6nm6sq
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcraqui%2Fcallback
  &scope=business%3Aread%20clients%3Aread%20payments%3Aread
  &state=9f2c1e7a4b6d8f03a5c7e9b1d3f5a7c9
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
```

(Los saltos de línea son para leerla: la URL va en una sola línea, con cada valor codificado.)

`scope` solo puede nombrar permisos que tu app declaró al registrarse. Uno que no declaró, o uno que ninguna app
puede tener, vuelve a tu web con `error=invalid_scope`. Lee [Permisos](https://docs.craqui.com/apps/permissions.md).

## 3. El propietario conecta

En la pantalla de Craqui, el propietario:

1. Inicia sesión, si no la tenía.
2. Ve el nombre de tu app, el dominio de tu sitio y, si la aprobamos, la marca **Verificada por Craqui**.
3. Elige el negocio. Solo aparecen los negocios donde es propietario; si tiene uno solo, ya viene elegido.
4. Revisa los permisos y puede quitar cualquiera, menos el de los datos básicos del negocio.
5. Toca **Conectar** o **Cancelar**.

El pedido vale 10 minutos. Si el propietario tarda más, la pantalla le pide volver a tu web y empezar de nuevo.

Si a la URL le falta algo o algo no coincide (un `client_id` desconocido, una `redirect_uri` que no está
registrada, sin PKCE), Craqui **no vuelve a tu web**: le muestra al propietario que la conexión no está bien
configurada. Una dirección que no se pudo verificar no recibe nada.

## 4. Recibe la respuesta en tu dirección de retorno

Si el propietario conectó tu app, Craqui redirige a tu `redirect_uri` con:

```
https://app.example.com/craqui/callback?code=6uN2yQ0rT8vX4bZ1cF5hJ7kL9mP3sW6dG8aE2iO0qRt&state=9f2c1e7a4b6d8f03a5c7e9b1d3f5a7c9&iss=https%3A%2F%2Fapi.craqui.com
```

Antes de usar el código, tu servidor comprueba dos cosas, y si alguna falla descarta la respuesta:

* **`state`** es exactamente el que guardaste en la sesión de esa persona. Después, bórralo: sirve una vez.
* **`iss`** es exactamente `https://api.craqui.com`. Si tu app se conecta a más de un servidor de
  autorización, así sabes que el código es de Craqui (RFC 9207).

Si el propietario canceló, o el pedido no se pudo atender, la respuesta trae un error en vez del código, con el
mismo `state` e `iss`:

```
https://app.example.com/craqui/callback?error=access_denied&error_description=The+user+denied+the+authorization+request&state=9f2c1e7a4b6d8f03a5c7e9b1d3f5a7c9&iss=https%3A%2F%2Fapi.craqui.com
```

| `error` | Qué pasó |
| - | - |
| `access_denied` | El propietario tocó **Cancelar**, o no tiene ningún negocio donde pueda conectar tu app |
| `invalid_scope` | `scope` nombra un permiso que tu app no puede pedir |
| `invalid_target` | `resource` no es `https://api.craqui.com/v1` |

Estos errores vuelven a tu web solo si quien conecta puede conectar tu app. Con una app en desarrollo, alguien
que no es propietario del negocio que la registró no vuelve a tu web: la pantalla le dice que no puede conectarla.

## 5. Cambia el código por tokens

El código vence a los **120 segundos** y sirve **una sola vez**. Desde tu servidor:

```bash theme={null}
curl -s https://api.craqui.com/oauth/token \
  -u "$CRAQUI_CLIENT_ID:$CRAQUI_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d code_verifier="$CODE_VERIFIER" \
  --data-urlencode redirect_uri="https://app.example.com/craqui/callback"
```

* La app se autentica con su identificador y su secreto en `Authorization: Basic` (`-u` en `curl`), o con
  `client_id` y `client_secret` en el cuerpo. Uno de los dos, nunca los dos.
* El cuerpo va como `application/x-www-form-urlencoded`.
* `redirect_uri` es la misma del paso 2, letra por letra.

La respuesta:

```json theme={null}
{
  "access_token": "crq_oat_TU_ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "crq_ort_TU_REFRESH_TOKEN",
  "scope": "business:read clients:read payments:read",
  "business_id": "biz_01jtm0r4w8a3b6c9d2e5f7g1hk"
}
```

* **`business_id`** es el negocio que eligió el propietario. Guarda los tokens bajo ese id: es como sabes, sin
  otra llamada, a qué negocio pertenece cada par.
* **`scope`** son los permisos que de verdad tiene la conexión. Pueden ser menos de los que pediste: el
  propietario pudo quitar alguno, y un negocio no da permisos que no aplican a él. Lee
  [Permisos](https://docs.craqui.com/apps/permissions.md).
* Guarda el refresh token cifrado, en tu base de datos. El access token vence en una hora; lo que haces
  después está en [Tokens](https://docs.craqui.com/apps/tokens.md).

Si algo no coincide (el `code_verifier`, la `redirect_uri`, el código ya se usó o venció, o es de otra app), la
respuesta es `400` con `error: "invalid_grant"`, sin decir cuál de los chequeos falló. Un `code_verifier` que no
tiene el formato de PKCE (43 a 128 caracteres de `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`) responde antes,
con `invalid_request`. Si el mismo código se
presenta dos veces, Craqui además **desconecta** la conexión que salió de ese código: alguien más pudo haberlo
visto.

### Si el negocio ya estaba conectado

Si un propietario vuelve a conectar tu app a un negocio donde ya estaba conectada, la conexión nueva reemplaza a
la anterior: los tokens viejos de ese negocio dejan de funcionar y valen los que acabas de recibir, con los
permisos que eligió ahora. Reemplaza lo que tenías guardado para ese `business_id`.

## 6. Llama a la API

El access token va en el header `Authorization`, igual que una clave:

```bash theme={null}
curl -s https://api.craqui.com/v1/business \
  -H "Authorization: Bearer $CRAQUI_ACCESS_TOKEN"
```

```json theme={null}
{
  "id": "biz_01jtm0r4w8a3b6c9d2e5f7g1hk",
  "object": "business",
  "livemode": true,
  "name": "Estudio Marea",
  "vertical": "wellness",
  "time_zone": "America/Montevideo",
  "currency": "UYU",
  "country": "UY",
  "public_url": "https://estudiomarea.craqui.com",
  "api_version": "2026-12-01",
  "warnings": []
}
```

El campo `vertical` dice el tipo de negocio que conectó el propietario: léelo para saber qué rutas puede usar tu
app en ese negocio.

Desde acá, todo lo de la API vale igual: las mismas rutas, la [paginación](https://docs.craqui.com/pagination.md), la
[idempotencia](https://docs.craqui.com/idempotency.md), las [versiones](https://docs.craqui.com/versioning.md) y los [errores](https://docs.craqui.com/errors.md). El token solo ve el negocio
de la conexión: ningún parámetro del pedido lo cambia. Y va **solo** en el header: un token en la URL o en un
cuerpo de formulario se rechaza con [`invalid_request`](https://docs.craqui.com/errors/invalid_request).

La versión de la API de una app queda fijada al crearla, como la de una clave; el header `Craqui-Version` la
cambia en un pedido.

## El mismo flujo en TypeScript

Con [`oauth4webapi`](https://github.com/panva/oauth4webapi) (`npm install oauth4webapi@3`), en tu servidor. La
librería genera el PKCE y el `state`, y comprueba `state` e `iss` en la respuesta. `session` es la sesión de
servidor de quien está conectando, y `saveConnection`, tu propio guardado:

```ts theme={null}
import * as oauth from 'oauth4webapi';

const as: oauth.AuthorizationServer = {
  issuer: 'https://api.craqui.com',
  authorization_endpoint: 'https://www.craqui.com/oauth/authorize',
  token_endpoint: 'https://api.craqui.com/oauth/token',
  revocation_endpoint: 'https://api.craqui.com/oauth/revoke',
  authorization_response_iss_parameter_supported: true,
};
const client: oauth.Client = { client_id: process.env.CRAQUI_CLIENT_ID! };
const clientAuth = oauth.ClientSecretBasic(process.env.CRAQUI_CLIENT_SECRET!);
const redirectUri = 'https://app.example.com/craqui/callback';

// Paso 1 y 2: «Conectar con Craqui». Devuelve la URL a la que rediriges el navegador.
export async function startConnect(session: { craqui?: { verifier: string; state: string } }): Promise<string> {
  const verifier = oauth.generateRandomCodeVerifier();
  const state = oauth.generateRandomState();
  session.craqui = { verifier, state };

  const url = new URL(as.authorization_endpoint!);
  url.searchParams.set('response_type', 'code');
  url.searchParams.set('client_id', client.client_id);
  url.searchParams.set('redirect_uri', redirectUri);
  url.searchParams.set('scope', 'business:read clients:read payments:read');
  url.searchParams.set('state', state);
  url.searchParams.set('code_challenge', await oauth.calculatePKCECodeChallenge(verifier));
  url.searchParams.set('code_challenge_method', 'S256');
  return url.href;
}

// Pasos 4 y 5: tu dirección de retorno. `callbackUrl` es la URL completa del pedido que llegó.
export async function finishConnect(session: { craqui?: { verifier: string; state: string } }, callbackUrl: URL) {
  const pending = session.craqui;
  delete session.craqui; // un intento, una vez
  if (!pending) throw new Error('No connection in progress for this session');

  // Comprueba state e iss, y lanza un error si la respuesta trae `error` (por ejemplo, access_denied).
  const params = oauth.validateAuthResponse(as, client, callbackUrl, pending.state);

  const response = await oauth.authorizationCodeGrantRequest(as, client, clientAuth, params, redirectUri, pending.verifier);
  const tokens = await oauth.processAuthorizationCodeResponse(as, client, response);

  await saveConnection({
    businessId: tokens.business_id as string,
    accessToken: tokens.access_token,
    accessExpiresAt: Date.now() + (tokens.expires_in ?? 3600) * 1000,
    refreshToken: tokens.refresh_token!,
    scopes: (tokens.scope ?? '').split(' '),
  });
}

declare function saveConnection(c: {
  businessId: string;
  accessToken: string;
  accessExpiresAt: number;
  refreshToken: string;
  scopes: string[];
}): Promise<void>;
```

> [!NOTE]
>
> Este código corre en tu servidor. El secreto, el `code_verifier` y los tokens nunca pasan por el navegador:
> lo único que ve el navegador es la URL de la pantalla de conexión y el regreso a tu web con el código.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.