---
title: "Conectar con Matchai"
description: "Conecta la cuenta de Matchai de un profesional a tu app con OAuth 2.1 y PKCE. Endpoints, la página de autorización, tokens y un ejemplo completo en TypeScript."
last_updated: "2026-10-21"
lang: "es"
status: "coming-soon"
canonical: "https://matchai.co/docs/es/developers/authentication/connect-with-matchai"
---

# Conectar con Matchai

> Conecta la cuenta de Matchai de un profesional a tu app con OAuth 2.1 y PKCE. Endpoints, la página de autorización, tokens y un ejemplo completo en TypeScript.

**Próximamente. Esto se documenta antes del lanzamiento para que puedas planificar tu integración; todavía no está disponible.**

Source: https://matchai.co/docs/es/developers/authentication/connect-with-matchai

**Conectar con Matchai** es la forma en que una app obtiene acceso a la cuenta de un profesional, con su permiso. Es OAuth 2.1 con el flujo de código de autorización y PKCE. El profesional ve qué puede hacer tu app y quién paga, presiona **Permitir**, y tu app aparece en su página Conectores, donde puede desconectarla en cualquier momento.

Úsalo para todo lo que vayan a usar otros profesionales: un creador de sitios web, un CRM, la app de un socio, un asistente de IA. Para tu propia cuenta, una [clave de API](https://matchai.co/docs/es/developers/authentication/api-keys) es más sencilla.

Cada conexión es **una cuenta de Matchai conectada a una app**, con sus propios tokens. Un creador de sitios web con tres corredores como clientes tiene tres conexiones.

## Antes de empezar [#before-you-start]

- [Crea una app de Matchai](https://matchai.co/docs/es/developers/authentication/create-a-matchai-app) para obtener un ID de cliente y un secreto de cliente, y para registrar tus URL de redirección.
- Decide qué [alcances](https://matchai.co/docs/es/developers/authentication#scopes) necesita tu app. Pide la lista más corta que funcione.
- Ten un servidor que pueda mantener privado el secreto de cliente. El intercambio de tokens ocurre ahí, nunca en el navegador.

## Endpoints [#endpoints]

| Propósito | URL |
|---|---|
| Autorización (la página donde se presiona **Permitir**) | `https://matchai.co/oauth/authorize` |
| Token y renovación | `https://api.matchai.co/oauth/token` |
| Registro dinámico de clientes | `https://api.matchai.co/oauth/register` |
| Metadatos del servidor de autorización | `https://api.matchai.co/.well-known/oauth-authorization-server` |
| Metadatos del recurso protegido | `https://api.matchai.co/.well-known/oauth-protected-resource` para la API REST, y `https://matchai.co/.well-known/oauth-protected-resource` para el servidor MCP |

Los tres últimos son para los clientes MCP y para las bibliotecas que descubren los endpoints por su cuenta. Si escribes el flujo a mano, solo necesitas los dos primeros.

PKCE con `S256` es obligatorio. Los códigos de autorización son de un solo uso y de corta duración, y los tokens de acceso son de corta duración: usa el token de actualización para obtener otros nuevos.

## El flujo [#the-flow]

<Steps>
<Step>

**Prepara PKCE y un valor de state.** Genera un `code_verifier` aleatorio y guárdalo. Envía su hash SHA-256, codificado en base64url, como `code_challenge`. Genera también un `state` aleatorio. Guarda ambos donde solo este navegador pueda devolverlos, como una cookie `HttpOnly`.

</Step>
<Step>

**Redirige al profesional a Matchai.** Envía su navegador a la URL de autorización con estos parámetros:

<TypeTable
  type={{
    response_type: { type: '"code"', required: true, description: 'Siempre code.' },
    client_id: { type: 'string', required: true, description: 'El ID de cliente de tu app de Matchai.' },
    redirect_uri: { type: 'string', required: true, description: 'Una de las URL de redirección que registraste, escrita exactamente igual.' },
    scope: { type: 'string', required: true, description: 'Los alcances que pides, separados por espacios, por ejemplo "profile:read listings:read contacts:write".' },
    state: { type: 'string', required: true, description: 'Un valor imposible de adivinar. Matchai lo devuelve sin cambios, y debes comprobar que coincide.' },
    code_challenge: { type: 'string', required: true, description: 'El hash SHA-256 de tu code_verifier, codificado en base64url.' },
    code_challenge_method: { type: '"S256"', required: true, description: 'Siempre S256.' },
  }}
/>

```text title="Authorization URL"
https://matchai.co/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fapp.your-website-company.example%2Fmatchai%2Fcallback
  &scope=profile%3Aread%20listings%3Aread%20contacts%3Awrite
  &state=3b1f0c9e7a2d4e68
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
```

</Step>
<Step>

**El profesional responde.** Si no inició sesión en Matchai, primero inicia sesión. Luego ve la [página de autorización](#the-allow-page) y presiona **Permitir**, o rechaza.

</Step>
<Step>

**Matchai lo envía de vuelta.** Regresa a tu `redirect_uri` con un `code` y tu `state`:

```text title="Callback"
https://app.your-website-company.example/matchai/callback?code=…&state=3b1f0c9e7a2d4e68
```

Si rechaza, en lugar de un `code` hay un `error=access_denied` y tu `state`. Comprueba que `state` coincide con el valor que guardaste antes de hacer cualquier otra cosa.

</Step>
<Step>

**Intercambia el código por tokens.** Desde tu servidor, haz un `POST` al endpoint de tokens con un cuerpo codificado como formulario:

```bash title="Terminal"
curl https://api.matchai.co/oauth/token \
  -d grant_type=authorization_code \
  -d code="$MATCHAI_AUTH_CODE" \
  -d redirect_uri="https://app.your-website-company.example/matchai/callback" \
  -d client_id="$MATCHAI_CLIENT_ID" \
  -d client_secret="$MATCHAI_CLIENT_SECRET" \
  -d code_verifier="$MATCHAI_CODE_VERIFIER"
```

```json title="Response"
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…",
  "scope": "profile:read listings:read contacts:write"
}
```

`expires_in` es la duración del token de acceso en segundos. Léelo en cada respuesta; no escribas una duración fija en el código. Guarda ambos tokens asociados a este único profesional, cifrados, y nunca los escribas en los registros.

</Step>
<Step>

**Llama a la API.** Envía el token de acceso como token bearer. Empieza con `GET /v1/me`: te dice qué cuenta acaba de conectarse. `GET /v1/me/connection` te dice si las acciones finales preguntan primero.

```bash title="Terminal"
curl https://api.matchai.co/v1/me \
  -H "Authorization: Bearer $MATCHAI_ACCESS_TOKEN"
```

</Step>
<Step>

**Renueva el token cuando venza.** Un token de acceso vencido responde `401` con el código `token_expired`. Intercambia el token de actualización por un token de acceso nuevo y repite la solicitud una vez:

```bash title="Terminal"
curl https://api.matchai.co/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token="$MATCHAI_REFRESH_TOKEN" \
  -d client_id="$MATCHAI_CLIENT_ID" \
  -d client_secret="$MATCHAI_CLIENT_SECRET"
```

Guarda siempre el token de actualización más reciente que te dé una respuesta. Si la renovación falla, la conexión terminó: pídele al profesional que vuelva a conectarse.

</Step>
</Steps>

## La página de autorización [#the-allow-page]

La página de autorización es el momento en que el profesional decide. Está diseñada para que nunca tenga que adivinar:

| Lo que muestra | De dónde viene |
|---|---|
| El **nombre, el logo y el sitio web** de tu app, con una marca cuando Matchai verificó al editor | Tu [app de Matchai](https://matchai.co/docs/es/developers/authentication/create-a-matchai-app) |
| **Lo que tu app puede hacer**, cada alcance en palabras sencillas, como "Ver tus anuncios" o "Agregar y editar tus contactos" | El `scope` que pediste |
| Si **las acciones finales preguntan primero** | Tu conexión empieza en **Preguntarme**. Enviar, publicar, pagar y eliminar esperan su aprobación. |
| **Quién paga** | "Incluido con" tu producto cuando tu app es pagada por el socio, una línea que dice que la conexión se agrega a su factura de Matchai cuando es pagada por el usuario, y nada en absoluto en [modo de prueba](https://matchai.co/docs/es/developers/authentication/create-a-matchai-app#test-mode) |

Al presionar **Permitir** se crea la conexión. A partir de ese momento aparece en su página Conectores con los mismos datos, y puede cambiar cómo se manejan las acciones finales o presionar **Desconectar**.

<Callout title="Por qué los alcances se leen como frases">
El profesional está decidiendo si deja que el software de un desconocido actúe en su nombre. Una lista como `contacts:write messages:send` le pide que traduzca; una frase le permite decidir. Pide los alcances que puedas explicar en una línea cada uno.
</Callout>

## Ejemplo: un creador de sitios web conecta a una corredora [#example-a-website-builder-connects-a-broker]

Supongamos que diriges una empresa que crea sitios web para corredores. Tu producto se llama "Tu empresa de sitios web" en Matchai, y Camille Tremblay, una corredora, es una de tus clientas.

<Steps>
<Step>

Camille abre la configuración de su sitio web en tu producto y presiona **Conectar Matchai**. Tu servidor ejecuta los pasos 1 y 2 de arriba y la redirige.

</Step>
<Step>

Ya inició sesión en Matchai. La página de autorización nombra a Tu empresa de sitios web, enumera lo que puede hacer (ver sus anuncios, agregar contactos, enterarse de los nuevos prospectos) y dice quién paga. Presiona **Permitir**.

</Step>
<Step>

Vuelve a tu producto. Tu servidor intercambia el código, llama a `GET /v1/me` y guarda sus tokens en su ficha de cliente. Le tomó unos pocos clics y no compartió ninguna contraseña.

</Step>
<Step>

Tu producto lee sus anuncios con `GET /v1/listings` y los muestra en su nuevo sitio web. Se suscribe a `listing.published`, `listing.updated` y `lead.created` con `POST /v1/webhook-subscriptions`, así que el sitio se mantiene al día sin consultas periódicas.

</Step>
<Step>

Un visitante completa el formulario de contacto de su sitio. Tu servidor lo envía con `POST /v1/enquiries`. Un prospecto y su conversación aparecen en los contactos de Matchai de Camille, y `lead.created` llega a tu endpoint.

</Step>
<Step>

Meses después, Camille desconecta tu producto desde su página Conectores. Tu siguiente llamada responde `401`, sus suscripciones de webhook se desactivan y tu producto muestra "Volver a conectar Matchai". No se eliminó nada de su cuenta de Matchai.

</Step>
</Steps>

Tu siguiente cliente, un segundo corredor, repite el mismo flujo y obtiene un conjunto de tokens separado. Ninguna de las dos conexiones puede ver los datos del otro corredor.

## Un ejemplo completo [#a-complete-example]

Este servidor hace los pasos 1 a 5 para un cliente que inició sesión. Usa Express. `currentCustomerId` y `saveConnection` representan tu propio inicio de sesión y tu propio almacenamiento.

```ts title="server.ts"
import { createHash, randomBytes } from 'node:crypto';
import cookieParser from 'cookie-parser';
import express from 'express';

// Your own code: who is signed in to your product, and where tokens are kept.
declare function currentCustomerId(request: express.Request): string;
declare function saveConnection(connection: {
  customerId: string;
  matchaiUserId: string;
  accessToken: string;
  refreshToken: string;
  expiresAt: Date;
}): Promise<void>;

const clientId = process.env.MATCHAI_CLIENT_ID!;
const clientSecret = process.env.MATCHAI_CLIENT_SECRET!;
const redirectUri = 'https://app.your-website-company.example/matchai/callback';
const scopes = ['profile:read', 'listings:read', 'contacts:write', 'webhooks:manage'];

const base64url = (bytes: Buffer) => bytes.toString('base64url');

const app = express();
app.use(cookieParser());

// Steps 1 and 2: send the professional to Matchai.
app.get('/matchai/connect', (request, response) => {
  currentCustomerId(request); // Only a signed-in customer can start a connection.

  const state = base64url(randomBytes(24));
  const verifier = base64url(randomBytes(48));
  const challenge = base64url(createHash('sha256').update(verifier).digest());

  // Keep both values in a cookie that only this browser sends back.
  response.cookie('matchai_oauth', JSON.stringify({ state, verifier }), {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 10 * 60 * 1000,
  });

  const authorize = new URL('https://matchai.co/oauth/authorize');
  authorize.search = new URLSearchParams({
    response_type: 'code',
    client_id: clientId,
    redirect_uri: redirectUri,
    scope: scopes.join(' '),
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  }).toString();

  response.redirect(authorize.toString());
});

// Steps 4 and 5: Matchai sends the professional back here.
app.get('/matchai/callback', async (request, response) => {
  const customerId = currentCustomerId(request);
  const saved = JSON.parse(request.cookies.matchai_oauth ?? 'null') as
    | { state: string; verifier: string }
    | null;
  response.clearCookie('matchai_oauth');

  if (request.query.error === 'access_denied') {
    return response.redirect('/settings/website?matchai=declined');
  }

  const { code, state } = request.query;
  if (!saved || typeof code !== 'string' || state !== saved.state) {
    return response.status(400).send('This connection request is not valid. Please start again.');
  }

  const tokenResponse = await fetch('https://api.matchai.co/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: redirectUri,
      client_id: clientId,
      client_secret: clientSecret,
      code_verifier: saved.verifier,
    }),
  });
  if (!tokenResponse.ok) {
    return response.status(502).send('Matchai did not accept the connection. Please try again.');
  }
  const tokens = await tokenResponse.json();

  // Learn which Matchai account just connected.
  const me = await fetch('https://api.matchai.co/v1/me', {
    headers: { Authorization: `Bearer ${tokens.access_token}` },
  }).then((result) => result.json());

  await saveConnection({
    customerId,
    matchaiUserId: me.user_id,
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
  });

  response.redirect('/settings/website?matchai=connected');
});

app.listen(3000);
```

Luego, cada llamada a la API pasa por una sola función que renueva un token vencido y detecta una conexión que terminó:

```ts title="matchai-client.ts"
type Tokens = { accessToken: string; refreshToken: string; expiresAt: Date };

// Your own code: tokens are stored per customer.
declare function loadTokens(customerId: string): Promise<Tokens>;
declare function saveTokens(customerId: string, tokens: Tokens): Promise<void>;
declare function markDisconnected(customerId: string): Promise<void>;

async function refresh(refreshToken: string): Promise<Tokens | null> {
  const response = await fetch('https://api.matchai.co/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.MATCHAI_CLIENT_ID!,
      client_secret: process.env.MATCHAI_CLIENT_SECRET!,
    }),
  });
  if (!response.ok) return null;

  const tokens = await response.json();
  return {
    accessToken: tokens.access_token,
    // Keep the newest refresh token you were given.
    refreshToken: tokens.refresh_token ?? refreshToken,
    expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
  };
}

export async function matchaiFetch(
  customerId: string,
  path: string,
  init: RequestInit & { headers?: Record<string, string> } = {},
): Promise<Response> {
  let tokens = await loadTokens(customerId);

  const call = () =>
    fetch(`https://api.matchai.co/v1${path}`, {
      ...init,
      headers: { ...init.headers, Authorization: `Bearer ${tokens.accessToken}` },
    });

  const response = await call();
  if (response.status !== 401) return response;

  // The access token expired, or the professional disconnected. Refresh once.
  const refreshed = await refresh(tokens.refreshToken);
  if (!refreshed) {
    await markDisconnected(customerId); // Show "Reconnect Matchai" in your product.
    return response;
  }
  await saveTokens(customerId, refreshed);
  tokens = refreshed;
  return call();
}
```

Renueva los tokens de un cliente a la vez. Si dos solicitudes los renuevan al mismo tiempo, solo la primera puede usar el token de actualización que tiene.

## Clientes MCP y registro dinámico de clientes [#mcp-clients-and-dynamic-client-registration]

Los asistentes de IA que hablan MCP, como Claude y ChatGPT, no te piden que registres una app. Encuentran el inicio de sesión por su cuenta y se registran solos con el **registro dinámico de clientes**:

1. El cliente llama a una herramienta de usuario en `https://matchai.co/mcp` sin token. El servidor responde `401` con un encabezado `WWW-Authenticate: Bearer resource_metadata=…` que apunta a los metadatos del recurso protegido.
2. El cliente lee los metadatos, encuentra el servidor de autorización y lee `https://api.matchai.co/.well-known/oauth-authorization-server`.
3. Se registra en `https://api.matchai.co/oauth/register` y obtiene un ID de cliente.
4. Ejecuta el mismo flujo de arriba. El profesional ve la misma página de autorización, que nombra al asistente.

Para agregar Matchai a un asistente, solo escribes la dirección del servidor. La [guía del servidor MCP](https://matchai.co/docs/es/developers/mcp-server) cubre las herramientas públicas, que siguen funcionando sin inicio de sesión.

## Terminar una conexión [#ending-a-connection]

- **El profesional se desconecta.** Presiona **Desconectar** en la página Conectores. Todos los tokens de la conexión dejan de funcionar, sus suscripciones de webhook se desactivan (`disabled_reason: connection_ended`) y no se elimina nada de su cuenta.
- **Tu app se desconecta.** Llama a `DELETE /v1/me/connection` ([`disconnect`](https://matchai.co/docs/developers/api-reference/me/disconnect)) para obtener el mismo efecto, por ejemplo cuando un cliente deja tu producto.

En ambos casos, elimina los tokens guardados y trata las respuestas `401` posteriores como "hay que volver a conectarse".

## Lista de verificación de seguridad [#security-checklist]

- Comprueba `state` en cada devolución de llamada, y rechaza las devoluciones de llamada que no iniciaste.
- Usa PKCE con `S256` en cada flujo. Matchai rechaza un flujo sin él.
- Guarda el secreto de cliente y cada token en tu servidor, cifrados en reposo y fuera de tus registros.
- Registra URL de redirección exactas, y nunca construyas una a partir de lo que escribe el usuario.
- Pide los menos alcances posibles. Necesitar más después significa volver a pedírselo al profesional.
- Ten un conjunto de tokens por profesional, y nunca uses el token de un cliente para la solicitud de otro.

<Cards>
  <Card title="Crea una app de Matchai" href="https://matchai.co/docs/es/developers/authentication/create-a-matchai-app">
    Obtén un ID y un secreto de cliente, y configura el modo de prueba.
  </Card>
  <Card title="Autenticación" href="https://matchai.co/docs/es/developers/authentication">
    Alcances, acciones finales y límites de energía.
  </Card>
  <Card title="Errores" href="https://matchai.co/docs/es/developers/errors">
    Qué significa un 401 o un 403 y qué hacer a continuación.
  </Card>
  <Card title="Webhooks" href="https://matchai.co/docs/es/developers/webhooks">
    Suscríbete a eventos para cada cuenta conectada.
  </Card>
</Cards>

## Sitemap

- [Todas las páginas de la documentación de Matchai](https://matchai.co/docs/sitemap.md)
- [llms.txt](https://matchai.co/docs/es/llms.txt)
