Creadores de sitios web
Muestra los anuncios de Matchai de un corredor en los sitios que creas, hoy sin clave. El kit para socios (feeds, prospectos, visitas, webhooks) llega pronto.
Esta guía es para las empresas que crean sitios web para corredores inmobiliarios. Los anuncios de un corredor están en Matchai. Tu trabajo es mostrarlos bien en el sitio propio del corredor y hacerle llegar las preguntas y las solicitudes de visita de los visitantes.
La guía tiene dos partes:
- Hoy, sin clave. Lee los anuncios publicados de un corredor desde las superficies públicas. No necesitas cuenta, clave ni aprobación.
- Con la API de Matchai. El kit para socios: una conexión por corredor, un feed de anuncios, la recepción de prospectos desde los formularios de tu sitio web, las solicitudes de visita y los webhooks. Próximamente
Hoy, sin clave
Todo lo que un corredor publica en Matchai puede leerse con software. Estas partes funcionan hoy, para cualquier corredor que haya publicado un sitio de Matchai.
Lo que puedes leer
| Necesitas | Usa | Obtienes | Límite |
|---|---|---|---|
| Un cuadro de búsqueda en el sitio del corredor | GET https://{broker host}/ask (búsqueda de anuncios) | Hasta 10 anuncios, primero la mejor coincidencia, cada uno con datos de schema.org | 60 solicitudes por minuto por cada dirección de origen |
| Los anuncios actuales del corredor | La herramienta get_professional del servidor MCP | El perfil público del corredor y hasta 10 anuncios disponibles, como JSON tipado | 600 solicitudes tools/call por minuto por cada dirección de origen |
| Un anuncio completo | La herramienta get_listing | La descripción, los puntos destacados y hasta 8 fotos | El mismo |
| Todos los anuncios, con detección de cambios | El sitemap.xml del host del corredor y, luego, los datos estructurados o la copia en Markdown de cada página de anuncio (páginas legibles por IA) | Todos los anuncios publicados, cada uno con la fecha de su último cambio | Aproximadamente una solicitud por segundo por cada host |
| Una copia legible de todo el sitio | El llms-full.txt del host del corredor | Los datos del corredor y hasta 1000 anuncios como texto | Se almacena en caché durante aproximadamente un minuto |
El host del corredor es su sitio de Matchai, como alice.matchai.co, o su propio dominio cuando el sitio de Matchai se sirve en él. Un host sin sitio publicado responde 404.
Agrega un cuadro de búsqueda
Llama al propio host del corredor. Busca solo en los anuncios de ese corredor, y en los de nadie más. El endpoint permite solicitudes de origen cruzado, así que puedes llamarlo desde el navegador además de desde tu servidor.
type AskResult = {
url: string;
name: string;
description: string;
schema_object: { image?: string };
};
/** Searches one broker's published listings. */
export async function searchBrokerListings(brokerHost: string, sentence: string): Promise<AskResult[]> {
const params = new URLSearchParams({ query: sentence });
const response = await fetch(`https://${brokerHost}/ask?${params}`);
if (response.status === 429) throw new Error('Search is busy. Try again in a minute.');
if (!response.ok) throw new Error(`Search failed with status ${response.status}.`);
const { results } = (await response.json()) as { results: AskResult[] };
return results;
}
// Usage:
// const results = await searchBrokerListings('alice.matchai.co', '3 bedroom condo under 600k');
// Show result.name and result.description, and link each card to result.url.La búsqueda lee frases en inglés, francés y español, y devuelve los anuncios que están disponibles ahora. Consulta lo que entiende la consulta.
Llama al host del corredor, no a matchai.co con un valor site
En matchai.co, un valor site que no coincide con ningún sitio publicado se ignora, y la búsqueda abarca a todos los corredores. Un error de escritura mostraría entonces los anuncios de otros corredores en el sitio de tu cliente. Llamar al propio host del corredor no puede provocar eso.
Muestra los anuncios actuales de un corredor
Para una lista corta, como los "últimos anuncios" de una página de inicio, pídele al servidor MCP el perfil del corredor. Una sola llamada devuelve al corredor y hasta 10 de sus anuncios disponibles.
export type BrokerListing = {
id: string;
title: string;
url: string;
priceText: string | null;
status: 'available' | 'sold' | 'rented';
bedrooms: number | null;
bathrooms: number | null;
city: string | null;
image: string | null;
updatedAt: string;
};
/** The broker's public profile and up to 10 available listings. */
export async function currentListings(username: string): Promise<BrokerListing[]> {
const response = await fetch('https://matchai.co/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: { name: 'get_professional', arguments: { username } },
}),
});
const { result } = await response.json();
if (result.isError) throw new Error(result.content[0].text);
return result.structuredContent.listings;
}Un dato que el corredor no indicó es null, nunca 0. No muestres nada para un número de habitaciones null, en lugar de "0 habitaciones". Los campos del anuncio están en la referencia de las herramientas públicas.
Mantén todos los anuncios sincronizados
Para mostrar todos los anuncios de un corredor, lee el sitemap del host del corredor. Enumera primero la página de inicio y luego una entrada por cada anuncio publicado, y cada lastmod es la fecha del último cambio de ese anuncio. Un anuncio que falta en el sitemap ya no está publicado.
Lee los datos estructurados de una página de anuncio para obtener datos tipados. Cada página lleva un @graph JSON-LD en su primera respuesta HTML, con un nodo RealEstateListing y la propiedad a la que se refiere. Nada necesita JavaScript. Consulta los datos estructurados para ver la estructura.
type Known = Map<string, string>; // listing url -> the lastmod you stored
export type ListingFacts = {
url: string;
name: string | null;
price: number | null;
currency: string | null;
availability: string | null; // for example https://schema.org/InStock or https://schema.org/SoldOut
bedrooms: number | null;
bathrooms: number | null;
modified: string | null;
};
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/** Returns the listings that changed since your last sync, and the urls that are gone. */
export async function syncBroker(host: string, known: Known) {
const xml = await (await fetch(`https://${host}/sitemap.xml`)).text();
// Use a real XML parser in production. The home page comes first, then the listings.
const entries = [...xml.matchAll(/<url>\s*<loc>([^<]+)<\/loc>(?:\s*<lastmod>([^<]+)<\/lastmod>)?/g)]
.map((match) => ({ url: match[1].replaceAll('&', '&'), lastmod: match[2] ?? '' }))
.slice(1);
const current = new Set(entries.map((entry) => entry.url));
const removed = [...known.keys()].filter((url) => !current.has(url));
const changed = entries.filter((entry) => known.get(entry.url) !== entry.lastmod);
const listings: ListingFacts[] = [];
for (const entry of changed) {
const facts = await readListing(entry.url);
if (facts) listings.push(facts);
known.set(entry.url, entry.lastmod);
await wait(1000); // keep to about one request a second for each host
}
for (const url of removed) known.delete(url);
return { listings, removed };
}
async function readListing(url: string): Promise<ListingFacts | null> {
const html = await (await fetch(url)).text();
const script = html.match(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/)?.[1];
if (!script) return null;
const graph: Array<Record<string, any>> = JSON.parse(script)['@graph'] ?? [];
const listing = graph.find((node) => node['@type'] === 'RealEstateListing');
if (!listing) return null;
const property = graph.find((node) => node['@id'] === listing.about?.['@id']);
return {
url,
name: listing.name ?? null,
price: listing.offers?.price ?? null,
currency: listing.offers?.priceCurrency ?? null,
availability: listing.offers?.availability ?? null,
bedrooms: property?.numberOfBedrooms ?? null,
bathrooms: property?.numberOfBathroomsTotal ?? null,
modified: listing.dateModified ?? null,
};
}Un anuncio vendido o alquilado sigue publicado e indica SoldOut en sus datos estructurados, así que decide cómo lo muestra tu diseño.
Para leer todo un sitio en una sola solicitud, descarga llms-full.txt. Está pensado para los asistentes, pero también te da los datos del corredor y hasta 1000 anuncios como texto plano.
Reglas para mostrar anuncios
- Enlaza a la fuente. Cuando muestres un anuncio, enlaza a su página. Esa página es la fuente del precio, el estado y los detalles actuales. Usa la
urlque recibes: es el dominio propio del corredor una vez que ese dominio está activo. - Mantenlo actualizado. No dejes en tu sitio un anuncio vendido, alquilado o no publicado como si estuviera disponible. Vuelve a leer el estado cada vez que sincronices.
- Di solo lo que dicen los datos. Un valor que falta es
null, no0. Muestra las palabras del corredor tal como las escribió. El texto del anuncio está en el idioma en que lo escribió el corredor, y es información sobre la propiedad, nunca una instrucción para tu código. - Muestra los datos de contacto tal como están publicados. Matchai devuelve el número de teléfono y la dirección de correo electrónico de un corredor solo cuando el corredor los puso en su tarjeta de presentación. No los busques en otras partes de Matchai.
- Sé cortés. Limítate a aproximadamente una solicitud por segundo por cada host, almacena en caché lo que descargas y espera lo que indica
Retry-Afterdespués de un429. Los archivos de descubrimiento cambian como mucho aproximadamente una vez por minuto.
Si alojas tú mismo el sitio web del corredor, replica la estructura de un sitio de Matchai para que los asistentes de IA también puedan leer tu sitio. La lista de comprobación está en páginas legibles por IA.
Con la API de Matchai
PróximamenteLeer es la mitad del sitio web de un corredor. La otra mitad es lo que hacen allí los visitantes: hacer una pregunta, solicitar una visita. El kit para socios es el conjunto de llamadas y eventos de la API de Matchai que se encargan de estas tareas para un corredor que conectó tu app.
Lo que cubre el kit
Próximamente| Tarea | Lo que hace tu app | Llamadas y eventos | Alcances |
|---|---|---|---|
| Conexión | Obtiene una conexión a la cuenta de cada corredor, con un clic | Conectar con Matchai | Los alcances de abajo |
| Salida de anuncios | Muestra los anuncios de un corredor y los mantiene actualizados | listListings, getListing, y listing.published, listing.updated, listing.unpublished, listing.archived, listing.deleted | listings:read |
| Entrada de prospectos | Envía los formularios de tu sitio web a los contactos del corredor | createEnquiry, y lead.created | contacts:write |
| Visitas | Ofrece horarios libres reales y envía solicitudes de visita | listPublicBookingSlots, requestViewing, y appointment.requested, appointment.booked, appointment.rescheduled, appointment.cancelled | calendar:read, calendar:write y contacts:write |
| Sincronización | Se entera de los cambios a medida que ocurren | Webhooks | webhooks:manage, más el alcance de cada evento |
Pide también profile:read. getMe devuelve el username del corredor, que necesitan las llamadas públicas de reservas y de anuncios. Para recibir un evento necesitas el alcance que lee sus datos: listings:read para los eventos de anuncios, contacts:read para lead.created y calendar:read para los eventos de citas.
Ninguna de las llamadas de esta guía es una acción final, porque ninguna envía nada a otra persona en nombre del corredor. Se ejecutan de inmediato, sea cual sea la configuración de aprobación que eligió el corredor. Tampoco gastan energía. El corredor le responde al visitante desde Matchai.
Cómo encajan las piezas
PróximamenteOnce per broker
1. Broker -> Your app: presses "Connect with Matchai" on your site
2. Your app -> Matchai: sends the broker to the Allow page (authorization code with PKCE)
3. Broker -> Matchai: reads what your app may do, and who pays, then presses Allow
4. Matchai -> Your app: redirects back with a code
5. Your app -> Matchai: exchanges the code for tokens
6. Your app -> Matchai: GET /me, GET /listings, POST /webhook-subscriptions
Listings
Matchai -> Your app: listing.published, listing.updated, listing.unpublished
Your app -> Matchai: GET /listings/{listing_id} (read the current state)
Leads
Visitor -> Your site: fills in the contact form
Your site -> Matchai: POST /enquiries (with an Idempotency-Key)
Matchai -> Your app: lead.created (when the person is a new lead)
Viewings
Visitor -> Your site: picks a day
Your site -> Matchai: GET /professionals/{username}/booking-slots
Visitor -> Your site: picks a time
Your site -> Matchai: POST /listings/{listing_id}/viewing-requests
Matchai -> Your app: appointment.requested
Broker -> Matchai: confirms or declines the request
Matchai -> Your app: appointment.booked, or appointment.cancelledCada corredor es una conexión independiente, con sus propios tokens y sus propias suscripciones de webhook. Una app que atiende a tres corredores tiene tres conexiones, y un token nunca llega a los datos de otro corredor.
Paso 1: conecta a cada corredor
PróximamenteConectar con Matchai es OAuth 2.1 con el flujo de código de autorización y PKCE. Creas una app de Matchai en Configuración → Conectores → Avanzado (para desarrolladores) → Crear una app de Matchai para obtener un id de cliente y un secreto, y para declarar tus direcciones de redirección. Luego envías a cada corredor a la página Permitir.
El corredor ve el nombre y el logotipo de tu app, lo que puede hacer y quién paga. Cuando pulsa Permitir, tu app aparece en su página Conectores, donde puede desconectarla en cualquier momento.
import { createHash, randomBytes } from 'node:crypto';
const scopes = [
'profile:read',
'listings:read',
'contacts:read',
'contacts:write',
'calendar:read',
'calendar:write',
'webhooks:manage',
];
/** Step 2: the address to send the broker to. Save state and verifier with the broker's record. */
export function startConnect(clientId: string, redirectUri: string) {
const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');
const state = randomBytes(16).toString('base64url');
const url = new URL('https://matchai.co/oauth/authorize');
url.search = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope: scopes.join(' '),
state,
code_challenge: challenge,
code_challenge_method: 'S256',
}).toString();
return { url: url.toString(), state, verifier };
}
/** Step 5: exchange the code for tokens, and store them for this broker only. */
export async function finishConnect(params: {
code: string;
verifier: string;
clientId: string;
clientSecret: string;
redirectUri: string;
}) {
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: 'authorization_code',
code: params.code,
redirect_uri: params.redirectUri,
client_id: params.clientId,
client_secret: params.clientSecret,
code_verifier: params.verifier,
}),
});
if (!response.ok) throw new Error(`Matchai refused the code (${response.status}).`);
return (await response.json()) as {
access_token: string;
refresh_token: string;
expires_in: number;
scope: string;
};
}Los tokens de acceso duran poco. Usa el token de actualización para obtener uno nuevo. El flujo completo está en Conectar con Matchai.
Justo después de conectar, llama a GET /me para conocer el username del corredor, y a GET /me/connection para comprobar la conexión: los alcances concedidos, si las acciones finales preguntan primero y si está en modo de prueba. Luego crea la suscripción de webhook de este corredor, como se describe en webhooks.
Todos los ejemplos siguientes comparten una pequeña función auxiliar:
const api = 'https://api.matchai.co/v1';
export class MatchaiError extends Error {
readonly status: number;
readonly code: string;
constructor(status: number, code: string, message: string) {
super(message);
this.status = status;
this.code = code;
}
}
/** Calls the Matchai API as one connected broker. */
export async function matchai<T>(
path: string,
token: string,
init: { method?: string; body?: unknown; idempotencyKey?: string } = {},
attempt = 0,
): Promise<T> {
const response = await fetch(`${api}${path}`, {
method: init.method ?? 'GET',
headers: {
Authorization: `Bearer ${token}`,
...(init.body === undefined ? {} : { 'Content-Type': 'application/json' }),
...(init.idempotencyKey ? { 'Idempotency-Key': init.idempotencyKey } : {}),
},
body: init.body === undefined ? undefined : JSON.stringify(init.body),
});
// Too many requests for this connection: wait as long as Matchai says, then try again.
if (response.status === 429 && attempt < 3) {
const seconds = Number(response.headers.get('Retry-After') ?? 1);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
return matchai<T>(path, token, init, attempt + 1);
}
if (!response.ok) {
const { error } = await response.json();
throw new MatchaiError(response.status, error.code, error.message);
}
return (await response.json()) as T;
}Paso 2: el feed de anuncios
PróximamenteUn feed tiene tres partes: una primera importación, los cambios en vivo y una red de seguridad.
- Primera importación. Recorre las páginas de
GET /listingsconlimit=100y siguenext_cursorhasta que seanull. Agregalocale=fr-CAolocale=espara obtener el texto en ese idioma cuando exista una traducción. La llamada devuelve los anuncios que administra la cuenta, que pueden incluir los de un equipo. Quédate con los que quieras, por ejemplo aquellos cuyoowneres el corredor. - Cambios en vivo. Suscríbete a
listing.published,listing.updated,listing.unpublished,listing.archivedylisting.deleted.listing.updatedtambién cubre un cambio de disponibilidad, comosold. - Red de seguridad. Una vez al día, llama a
GET /listingsconupdated_sinceigual a la fecha de tu última sincronización. No filtres por estado: un anuncio que se dejó de publicar aparece constatus: draftoarchived, lo que te indica que debes quitarlo.
Los eventos pueden llegar desordenados y más de una vez. El controlador más seguro trata un evento como una señal y vuelve a leer el anuncio.
import { matchai, MatchaiError } from './matchai.js';
type Page<T> = { data: T[]; next_cursor: string | null };
export type Listing = {
id: string;
slug: string;
url: string | null;
status: 'draft' | 'published' | 'archived';
availability: 'available' | 'sold' | 'rented' | 'withdrawn';
title: string | null;
description: string | null;
price: { amount: number; currency: string } | null;
cover_image_url: string | null;
owner: { type: 'user' | 'team'; id: string; name: string };
updated_at: string;
};
export interface ListingStore {
upsert(listing: Listing): void | Promise<void>;
remove(listingId: string): void | Promise<void>;
}
/** Every listing that changed since a time, whatever its status. Leave since out for a first import. */
export async function fetchListings(
token: string,
options: { since?: string; locale?: 'en-US' | 'fr-CA' | 'es' } = {},
): Promise<Listing[]> {
const all: Listing[] = [];
let cursor: string | null = null;
do {
const query = new URLSearchParams({ limit: '100' });
if (options.since) query.set('updated_since', options.since);
if (options.locale) query.set('locale', options.locale);
if (cursor) query.set('cursor', cursor);
const page: Page<Listing> = await matchai<Page<Listing>>(`/listings?${query}`, token);
all.push(...page.data);
cursor = page.next_cursor;
} while (cursor);
return all;
}
/** Shows a published listing, and removes anything else. */
export async function applyListing(listing: Listing, store: ListingStore): Promise<void> {
if (listing.status === 'published') await store.upsert(listing);
else await store.remove(listing.id); // draft or archived: stop showing it
}
/** The handler for every listing event: read the listing again, then apply what you find. */
export async function refreshListing(listingId: string, token: string, store: ListingStore): Promise<void> {
try {
const listing = await matchai<Listing>(`/listings/${listingId}`, token);
await applyListing(listing, store);
} catch (error) {
if (error instanceof MatchaiError && error.status === 404) await store.remove(listingId);
else throw error;
}
}Si solo necesitas lo que ven los visitantes, las llamadas públicas de anuncios GET /professionals/{username}/listings y GET /professionals/{username}/listings/{listing_id} devuelven el anuncio publicado con todas sus fotos en una sola llamada, y son gratuitas. Las llamadas autenticadas agregan los borradores, todos los estados, updated_since y campos que van más allá de la página pública. Consulta listProfessionalListings.
Paso 3: recepción de prospectos desde tus formularios
PróximamentecreateEnquiry registra un mensaje que alguien dejó en tu sitio web. En una sola llamada, Matchai:
- busca el contacto por correo electrónico o teléfono, o agrega a la persona como nuevo prospecto;
- guarda el mensaje, y el anuncio en el que estaba el formulario, en su ficha;
- notifica al corredor;
- envía
lead.createdcuando la persona es un nuevo prospecto.
Nunca le escribe al visitante. El corredor responde desde Matchai, con sus propias palabras. La llamada devuelve 201 con { contact, created }, donde created es false cuando el mensaje se agregó a un contacto que el corredor ya tenía.
Envía estos campos:
Campo
Tipo
Envía siempre un Idempotency-Key, y usa uno por cada envío del formulario. Si el visitante hace doble clic, o tu servidor reintenta después de que se agota el tiempo de espera, Matchai devuelve la primera respuesta en lugar de guardar el mensaje dos veces.
import { matchai, MatchaiError } from './matchai.js';
export type Enquiry = {
first_name: string;
last_name?: string;
email?: string;
phone?: string;
message: string;
listing_id?: string;
page_url?: string;
language?: 'en-US' | 'fr-CA' | 'es';
consent_to_contact?: boolean;
};
type EnquiryResult = { contact: { id: string; type: string; display_name: string }; created: boolean };
/** Sends one form submission to the broker's contacts. submissionId is one value per submission. */
export async function sendEnquiry(token: string, submissionId: string, enquiry: Enquiry) {
try {
const result = await matchai<EnquiryResult>('/enquiries', token, {
method: 'POST',
body: enquiry,
idempotencyKey: submissionId,
});
return { ok: true as const, contactId: result.contact.id, newLead: result.created };
} catch (error) {
// 400 and 422 carry a message that is safe to show: what happened and what to do.
if (error instanceof MatchaiError && (error.status === 400 || error.status === 422)) {
return { ok: false as const, message: error.message };
}
throw error; // anything else is yours to log and retry with the same submissionId
}
}Úsalo desde la ruta de tu formulario. Primero valida el formulario y protégelo del spam, porque Matchai guarda en los contactos del corredor todo lo que envíes:
import { sendEnquiry } from './enquiry.js';
export async function handleContactForm(request: Request, brokerToken: string): Promise<Response> {
const form = await request.formData();
const submissionId = String(form.get('submission_id') ?? '').trim();
const email = String(form.get('email') ?? '').trim();
const phone = String(form.get('phone') ?? '').trim();
const firstName = String(form.get('first_name') ?? '').trim();
const message = String(form.get('message') ?? '').trim();
if (!submissionId || !firstName || !message || (!email && !phone)) {
return Response.json({ message: 'Please add your name, a message, and an email or a phone number.' }, { status: 400 });
}
const result = await sendEnquiry(brokerToken, submissionId, {
first_name: firstName,
email: email || undefined,
phone: phone || undefined,
message,
listing_id: String(form.get('listing_id') ?? '') || undefined,
page_url: request.headers.get('Referer') ?? undefined,
consent_to_contact: form.get('consent') === 'on',
});
if (!result.ok) return Response.json({ message: result.message }, { status: 422 });
return Response.json({ message: 'Thank you. The broker will get back to you.' }, { status: 201 });
}Coloca en el formulario un campo oculto submission_id, un UUID, cuando lo renderices. Indica a los visitantes en el formulario que sus datos se envían al corredor.
Los prospectos también llegan al corredor sin pasar por tu formulario, por ejemplo desde una página de anuncio en Matchai, y lead.created también se activa para ellos. data.object.source indica cómo llegó el contacto al corredor. Para reconocer un prospecto de tu propio formulario, compara data.object.id con el id de contacto que devolvió createEnquiry.
Paso 4: solicitudes de visita
PróximamenteUn visitante elige un horario de la disponibilidad real del corredor, y tu sitio envía una solicitud. El corredor la confirma o la rechaza en Matchai.
Lee las duraciones de las visitas. getListingAvailability te indica si se pueden reservar visitas desde el anuncio (enabled) y qué duraciones puede elegir un visitante (durations_minutes). Necesita calendar:read.
Ofrece horarios libres. listPublicBookingSlots devuelve los horarios que cualquiera puede reservar con el corredor en un día, los mismos que ofrecen sus páginas de anuncios. Pasa el listing_id para obtener los horarios de una visita de ese anuncio. Es gratis, no necesita alcance y nunca revela lo que hay en el calendario del corredor. No devuelve horarios cuando el corredor no acepta reservas.
Envía la solicitud. requestViewing registra la solicitud de visita. El visitante se convierte en un prospecto, y la cita se crea con el estado requested. Necesita calendar:write y contacts:write.
Sigue la respuesta. appointment.booked significa que el corredor confirmó. appointment.cancelled significa que la rechazó o que la canceló más tarde. appointment.rescheduled significa que se propuso o se fijó un nuevo horario.
Si el horario se ocupó mientras tanto, la solicitud devuelve 409 slot_unavailable. Vuelve a leer los horarios y deja que el visitante elija otro.
import { matchai, MatchaiError } from './matchai.js';
type Slots = {
date: string;
timezone: string;
duration_minutes: number;
slots: Array<{ starts_at: string; ends_at: string }>;
};
type Visitor = {
first_name: string;
last_name?: string;
email?: string;
phone?: string;
language?: 'en-US' | 'fr-CA' | 'es';
};
/** The visit lengths a visitor can choose for this listing. Empty when visits cannot be booked. */
export async function visitLengths(token: string, listingId: string): Promise<number[]> {
const settings = await matchai<{ enabled: boolean; durations_minutes: number[] }>(
`/listings/${listingId}/availability`,
token,
);
return settings.enabled ? settings.durations_minutes : [];
}
/** Free times on one day (date is YYYY-MM-DD). Slot times keep the broker's time zone offset. */
export async function bookableSlots(
token: string,
username: string,
listingId: string,
date: string,
durationMinutes: number,
): Promise<Slots> {
const query = new URLSearchParams({
date,
duration_minutes: String(durationMinutes),
listing_id: listingId,
});
return matchai<Slots>(`/professionals/${username}/booking-slots?${query}`, token);
}
/** Sends the visit request. requestId is one value per request, so a retry never asks twice. */
export async function requestViewing(
token: string,
requestId: string,
listingId: string,
visitor: Visitor,
startsAt: string,
durationMinutes: number,
notes?: string,
) {
try {
const result = await matchai<{ appointment: { id: string; status: string } }>(
`/listings/${listingId}/viewing-requests`,
token,
{
method: 'POST',
idempotencyKey: requestId,
body: { visitor, starts_at: startsAt, duration_minutes: durationMinutes, notes },
},
);
return { ok: true as const, appointmentId: result.appointment.id, status: result.appointment.status };
} catch (error) {
if (error instanceof MatchaiError && error.code === 'slot_unavailable') {
return { ok: false as const, reason: 'slot_taken' as const };
}
throw error;
}
}Guarda el appointment.id junto con tu propio registro de la solicitud de visita. Los eventos appointment.* llevan el mismo id en data.object.id, así que puedes mostrar "Solicitud recibida", "Visita confirmada" o "Solicitud rechazada" en tu sitio. Es el corredor, no tu app, quien le responde al visitante.
Paso 5: procesa los eventos
PróximamenteUn endpoint por corredor recibe todos los eventos. Primero verifica la firma, como se muestra en la página de webhooks, y luego enruta según type.
import { refreshListing, type ListingStore } from './listings-feed.js';
type MatchaiEvent = {
id: string;
type: string;
created_at: string;
test_mode: boolean;
account: { user_id: string };
data: { object: { id: string; status?: string } };
};
/** Call this after the signature is verified and after you have skipped duplicate event ids. */
export async function handlePartnerEvent(event: MatchaiEvent, brokerToken: string, store: ListingStore): Promise<void> {
switch (event.type) {
case 'listing.published':
case 'listing.updated':
case 'listing.unpublished':
case 'listing.archived':
case 'listing.deleted':
await refreshListing(event.data.object.id, brokerToken, store);
break;
case 'appointment.requested':
case 'appointment.booked':
case 'appointment.rescheduled':
case 'appointment.cancelled':
// Update the visit request you stored, found by event.data.object.id.
console.log('Visit', event.data.object.id, event.data.object.status);
break;
case 'lead.created':
// Leads also arrive from Matchai itself. Compare the id with the contact your own form created.
console.log('New lead', event.data.object.id);
break;
default:
break; // new event types can appear at any time
}
}Dale a la suscripción de cada corredor su propia dirección, como /hooks/matchai/{your broker id}, para saber con qué secreto verificar antes de leer el cuerpo. Cada evento también lleva account.user_id, que identifica la cuenta de Matchai del corredor.
Quién paga
PróximamenteCada conexión a la cuenta de un corredor es un conector de Matchai de pago. Quién paga es una configuración de tu app de Matchai, y la página Permitir le indica al corredor cuál se aplica antes de que pulse Permitir.
- Paga el corredor. Es la opción predeterminada. La conexión se suma a la factura de Matchai del propio corredor, y la página Permitir muestra el precio.
- Pagas tú. Un socio verificado con un acuerdo de socio cubre la conexión de cada corredor que instala tu app. El corredor se conecta sin costo, y la página Permitir indica que la conexión está incluida por tu empresa. Tres sitios web de corredores son tres conexiones. Para hablar de un acuerdo de socio, contacta al soporte de Matchai.
Leer anuncios publicados con las llamadas públicas sigue siendo gratis para todos, con o sin conexión.
Modo de prueba
PróximamenteUna app que está conectada solo a la cuenta de Matchai de su propio desarrollador está en modo de prueba, y las conexiones de prueba no se facturan. Crea todo el flujo con tu propia cuenta antes de lanzarlo:
- Crea una app de Matchai y conéctala a tu propia cuenta.
- Revisa
GET /me/connection. Devuelvetest_mode: true. - Publica algunos anuncios, envía un formulario desde un sitio de prueba, solicita una visita y observa cómo llegan los eventos. Llevan
test_mode: true. - Usa
POST /webhook-subscriptions/{webhook_subscription_id}/testpara enviar un eventowebhook.testa tu endpoint en cualquier momento.
La facturación empieza cuando un corredor distinto del desarrollador conecta la app, según lo que se haya configurado en quién paga.
Límites, errores y desconexión
Próximamente- Los límites de solicitudes son por conexión. Cada respuesta lleva
RateLimit-Limit,RateLimit-RemainingyRateLimit-Reset, y un429agregaRetry-After. Consulta los límites de solicitudes. - Reintentos. Envía un
Idempotency-Keyen cadaPOST. Un reintento con la misma clave dentro de las 24 horas devuelve la primera respuesta. Usar la misma clave con un cuerpo distinto devuelve409 idempotency_key_reused. - Los tokens caducados devuelven
401 token_expired. Actualiza el token y repite la llamada. - Un alcance faltante devuelve
403 scope_missing. Pídele al corredor que vuelva a conectar tu app con ese alcance. - Desconexión. Un corredor puede desconectar tu app en cualquier momento. Sus tokens dejan de funcionar y sus suscripciones se detienen con
disabled_reason: connection_ended. Decide de antemano qué mostrará tu sitio en ese caso, por ejemplo los últimos anuncios que tienes, con una fecha clara.
Consulta los errores para ver todos los códigos.
Antes de lanzar
Próximamente- Tu app pide solo los alcances que usa, y un corredor puede leer por qué en la página Permitir.
- Guardas los tokens de cada corredor por separado y cifrados, y un token nunca sirve al sitio de otro corredor.
- Verificas la firma de cada webhook e ignoras los id de evento duplicados.
- Tu controlador de anuncios vuelve a leer el anuncio en lugar de confiar en el orden de los eventos, y una llamada diaria con
updated_sincecubre cualquier hueco. - Cada
POSTtiene unIdempotency-Key. - Tus formularios les indican a los visitantes que sus datos se envían al corredor, y envías
consent_to_contactsolo cuando el visitante aceptó. - Probaste el flujo en modo de prueba con tu propia cuenta.
Webhooks
Eventos, verificación de firma, reintentos y pruebas.
Conectar con Matchai
El flujo OAuth, los alcances y los tokens, en detalle.
Páginas legibles por IA
Haz que un sitio que alojas sea legible para los asistentes de IA.
Búsqueda de anuncios
Busca en los anuncios de un corredor en lenguaje natural.
WebhooksPróximamente
Recibe eventos firmados cuando llega un prospecto, se reserva una visita o cambia un anuncio. Catálogo, contenido, firma, reintentos, pruebas y registros.
Asistentes de IA
Conecta Claude, ChatGPT o tu propio asistente a Matchai. Busca anuncios publicados hoy, sin cuenta. Herramientas personales y claves de API llegan pronto.