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

# Autenticación OAuth

> Authorization code con PKCE, tokens de acceso, refresh rotativo y revocación.

Las apps de Commercy usan OAuth 2.0 con **authorization code + PKCE (S256)** y un **cliente confidencial**: tu servidor guarda el `client_secret` y nunca lo expone a un navegador.

| Qué | Valor |
| - | - |
| Endpoint de autorización | `GET https://api.commercy.com.ar/oauth/apps/authorize` |
| Endpoint de token | `POST https://api.commercy.com.ar/oauth/apps/token` |
| Endpoint de revocación | `POST https://api.commercy.com.ar/oauth/apps/revoke` |
| Access token | JWT, dura **1 hora** |
| Refresh token | Opaco, dura **90 días**, de **un solo uso** (rotativo) |
| Code de autorización | Dura **60 segundos**, de un solo uso |

<Warning>
  Estos endpoints no tienen CORS a propósito: hay que llamarlos desde tu servidor. Un access token o un client secret nunca deben vivir en un navegador.
</Warning>

## Paso 1: mandá al comerciante a autorizar

Generá un `code_verifier` aleatorio (43 a 128 caracteres de `A-Z a-z 0-9 - . _ ~`), calculá su `code_challenge` (SHA-256 en base64url, sin padding) y guardá el verifier y el `state` en la sesión del comerciante.

Parámetros de `GET /oauth/apps/authorize`:

| Parámetro | Obligatorio | Detalle |
| - | - | - |
| `client_id` | Sí | El `app_...` de tu app. |
| `redirect_uri` | Sí | Debe coincidir **exactamente** con una de las registradas en tu app. |
| `response_type` | Sí | Siempre `code`. |
| `state` | Sí | Valor aleatorio tuyo; lo verificás al volver (protección CSRF). |
| `code_challenge` | Sí | 43 caracteres base64url del SHA-256 del verifier. |
| `code_challenge_method` | Sí | Siempre `S256`. |
| `scope` | No | Scopes separados por espacios. Tienen que ser un subconjunto de los que pidió tu app. Si no lo mandás, se piden todos los de tu app. |

Commercy muestra la pantalla de consentimiento (la solicitud vence a los 10 minutos). Quien autoriza tiene que ser el dueño de la tienda (el usuario con rol Admin de la cuenta principal), no un usuario de su equipo. Al terminar:

* **Si acepta**, redirige a tu `redirect_uri` con `?code=...&state=...`.
* **Si rechaza**, redirige con `?error=access_denied&state=...`.
* **Si algún parámetro es inválido** pero el `client_id` y la `redirect_uri` son correctos, redirige a tu `redirect_uri` con `?error=...&error_description=...&state=...` (por ejemplo `invalid_request`, `invalid_scope` o `unsupported_response_type`).
* **Si el `client_id` o la `redirect_uri` son inválidos**, nunca redirigimos a la URL recibida: el comerciante ve una pantalla de error en Commercy.

Verificá siempre que el `state` de la vuelta sea el que mandaste.

## Paso 2: canjeá el code por tokens

`POST /oauth/apps/token`. Autenticá tu app con `client_secret_basic` (header `Authorization: Basic base64(client_id:client_secret)`) o `client_secret_post` (`client_id` y `client_secret` en el cuerpo). El cuerpo puede ser `application/x-www-form-urlencoded` o JSON.

| Campo | Detalle |
| - | - |
| `grant_type` | `authorization_code` |
| `code` | El code recibido. |
| `code_verifier` | El verifier original. |
| `redirect_uri` | La misma que usaste en `/authorize`. |

Respuesta `200`:

```json theme={null}
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "9f2c8a...",
  "scope": "read_shops read_products read_orders",
  "installation_id": "6710b3c2e4a1f20012ab34cd",
  "store": { "id": "6710b3c2e4a1f20012ab34ce", "name": "Mi Tienda" }
}
```

El canje es lo que **crea la instalación**. `installation_id` identifica la instalación de tu app en esa tienda y `store.id` es el comercio. Guardalos junto con los tokens, cifrados.

Si el comerciante vuelve a autorizar tu app mientras ya está instalada, la instalación conserva y amplía sus scopes (nunca se reducen en silencio) y los refresh tokens anteriores quedan invalidados.

## Paso 3: usá y renová el access token

Mandá el access token en cada request a la API: `Authorization: Bearer <access_token>`. Cuando venza (o recibas `401 invalid_token`), renovalo con el refresh token:

| Campo | Detalle |
| - | - |
| `grant_type` | `refresh_token` |
| `refresh_token` | El refresh token vigente. |

La respuesta tiene el mismo formato que el canje y trae un **refresh token nuevo**. Guardalo y descartá el anterior.

### Refresh rotativo y reuso

Cada refresh token se puede usar **una sola vez**. Si alguien presenta un refresh token que ya se usó, lo tratamos como una posible filtración: revocamos toda la familia de tokens y la instalación pasa a estar revocada, así que **el comerciante tiene que reinstalar la app**.

Excepción: si el token usado se vuelve a presentar dentro de los **10 segundos**, respondemos `invalid_grant` pero **no** revocamos. Eso cubre el caso típico de dos workers tuyos refrescando a la vez. Aun así, serializá los refresh: un solo proceso renueva y los demás esperan.

## Revocar tokens

`POST /oauth/apps/revoke` con la misma autenticación de cliente y el campo `token` (un refresh token o un access token). Sigue el RFC 7009: responde `200 {}` aunque el token no exista.

Revocar invalida los tokens de la instalación (la API empieza a responder `401 invalid_token`), pero no la desinstala: para volver a operar el comerciante tiene que autorizar de nuevo.

## Desinstalación

El comerciante puede desinstalar tu app desde su panel de Commercy. Tu app recibe el webhook obligatorio `app.uninstalled`, y a las 48 horas `store.redact` si no se reinstaló (ver [Webhooks](/webhooks)).

## Cuando necesitás más scopes

Si ampliás los scopes de tu app, el cambio pasa por revisión (ver [Revisión y publicación](/revision)). Una vez aprobado, los comerciantes ya instalados siguen con los scopes anteriores hasta que vuelvan a pasar por `/authorize`, donde ven y aceptan los permisos nuevos.

## Errores del endpoint de token

Estos endpoints responden con el formato de OAuth (RFC 6749), **no** con el envelope de la API:

```json theme={null}
{ "error": "invalid_grant", "error_description": "install_limit_reached" }
```

| `error` | Status | Cuándo |
| - | - | - |
| `invalid_request` | 400 | Falta un parámetro, o mandaste credenciales distintas por Basic y por cuerpo. |
| `invalid_client` | 401 | `client_id` o `client_secret` incorrectos, app suspendida o developer suspendido. Si usaste Basic, la respuesta trae `WWW-Authenticate: Basic`. |
| `invalid_grant` | 400 | Code vencido, ya usado o de otra app; `redirect_uri` o `code_verifier` que no coinciden; refresh token vencido, usado o revocado. Con `error_description: install_limit_reached`, el comercio ya alcanzó el máximo de apps instaladas de su plan. |
| `unsupported_grant_type` | 400 | `grant_type` distinto de `authorization_code` y `refresh_token`. |
| `server_error` | 500 | Error nuestro: reintentá. |

Límites: 300 requests por minuto por `client_id` en `/token` y en `/revoke`, y 10 intentos fallidos de autenticación de cliente cada 10 minutos por combinación `client_id` + IP. Al excederlos recibís `429` con el header `Retry-After` y un cuerpo `{ "error": "Too many requests", "retryAfter": 30 }`.

## Ejemplo completo en Node

```js theme={null}
const crypto = require("crypto");

const API = "https://api.commercy.com.ar";
const CLIENT_ID = process.env.COMMERCY_CLIENT_ID;
const CLIENT_SECRET = process.env.COMMERCY_CLIENT_SECRET;
const REDIRECT_URI = "https://miapp.example.com/callback";

const b64url = (buf) => buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

// 1) Armá la URL de autorización y guardá verifier + state en la sesión del comerciante.
function startInstall() {
  const verifier = b64url(crypto.randomBytes(32)); // 43 caracteres
  const challenge = b64url(crypto.createHash("sha256").update(verifier).digest());
  const state = b64url(crypto.randomBytes(16));
  const url = new URL(`${API}/oauth/apps/authorize`);
  url.search = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    response_type: "code",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  }).toString();
  return { url: url.toString(), verifier, state };
}

async function tokenRequest(params) {
  const res = await fetch(`${API}/oauth/apps/token`, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: "Basic " + Buffer.from(`${encodeURIComponent(CLIENT_ID)}:${encodeURIComponent(CLIENT_SECRET)}`).toString("base64"),
    },
    body: new URLSearchParams(params),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`OAuth ${res.status}: ${body.error} ${body.error_description ?? ""}`);
  return body;
}

// 2) En /callback: verificá el state y canjeá el code.
function exchangeCode({ code, verifier }) {
  return tokenRequest({
    grant_type: "authorization_code",
    code,
    code_verifier: verifier,
    redirect_uri: REDIRECT_URI,
  });
}

// 3) Renová antes de que venza. Guardá SIEMPRE el refresh_token nuevo.
function refresh(refreshToken) {
  return tokenRequest({ grant_type: "refresh_token", refresh_token: refreshToken });
}

module.exports = { startInstall, exchangeCode, refresh };
```


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