Conectar con MatchaiPróximamente
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.
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 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
- Crea una app de Matchai para obtener un ID de cliente y un secreto de cliente, y para registrar tus URL de redirección.
- Decide qué alcances 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
| 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
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.
Redirige al profesional a Matchai. Envía su navegador a la URL de autorización con estos parámetros:
Campo
Tipo
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=S256El profesional responde. Si no inició sesión en Matchai, primero inicia sesión. Luego ve la página de autorización y presiona Permitir, o rechaza.
Matchai lo envía de vuelta. Regresa a tu redirect_uri con un code y tu state:
https://app.your-website-company.example/matchai/callback?code=…&state=3b1f0c9e7a2d4e68Si 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.
Intercambia el código por tokens. Desde tu servidor, haz un POST al endpoint de tokens con un cuerpo codificado como formulario:
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"{
"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.
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.
curl https://api.matchai.co/v1/me \
-H "Authorization: Bearer $MATCHAI_ACCESS_TOKEN"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:
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.
La página de autorización
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 |
| 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 |
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.
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.
Ejemplo: un creador de sitios web conecta a una corredora
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.
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.
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.
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.
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.
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.
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.
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
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.
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ó:
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
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:
- El cliente llama a una herramienta de usuario en
https://matchai.co/mcpsin token. El servidor responde401con un encabezadoWWW-Authenticate: Bearer resource_metadata=…que apunta a los metadatos del recurso protegido. - El cliente lee los metadatos, encuentra el servidor de autorización y lee
https://api.matchai.co/.well-known/oauth-authorization-server. - Se registra en
https://api.matchai.co/oauth/registery obtiene un ID de cliente. - 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 cubre las herramientas públicas, que siguen funcionando sin inicio de sesión.
Terminar una conexión
- 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) 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
- Comprueba
stateen cada devolución de llamada, y rechaza las devoluciones de llamada que no iniciaste. - Usa PKCE con
S256en 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.
Crea una app de MatchaiPróximamente
Registra la app que los profesionales conectan con Conectar con Matchai: nombre, logo, sitio web, URL de redirección, ID y secreto, modo de prueba y quién paga.
Servidor MCP
Conecta un asistente de IA al servidor MCP de Matchai y busca anuncios. Endpoints, transporte, límites y configuración para Claude, ChatGPT y otros clientes.