Créateurs de sites Web
Affichez les inscriptions Matchai d'un courtier sur vos sites, dès maintenant et sans clé. Trousse partenaire à venir : flux, prospects, visites et webhooks.
Ce guide s'adresse aux entreprises qui créent des sites Web pour des courtiers immobiliers. Les inscriptions d'un courtier se trouvent dans Matchai. Votre rôle est de bien les présenter sur le site du courtier, et de renvoyer au courtier les questions et les demandes de visite des visiteurs.
Le guide comporte deux parties :
- Dès aujourd'hui, sans clé. Lisez les inscriptions publiées d'un courtier à partir des interfaces publiques. Vous n'avez besoin ni d'un compte, ni d'une clé, ni d'une approbation.
- Avec l'API Matchai. La trousse partenaire : une connexion par courtier, un flux d'inscriptions, la réception des prospects à partir des formulaires de votre site Web, les demandes de visite et les webhooks. Bientôt disponible
Dès aujourd'hui, sans clé
Tout ce qu'un courtier publie sur Matchai peut être lu par un logiciel. Ces éléments fonctionnent dès aujourd'hui, pour tout courtier qui a publié un site Matchai.
Ce que vous pouvez lire
| Vous avez besoin de | Utilisez | Vous obtenez | Limite |
|---|---|---|---|
| Une boîte de recherche sur le site du courtier | GET https://{broker host}/ask (recherche d'inscriptions) | Jusqu'à 10 inscriptions, la meilleure correspondance d'abord, chacune avec des données schema.org | 60 requêtes par minute pour chaque adresse appelante |
| Les inscriptions actuelles du courtier | L'outil get_professional du serveur MCP | Le profil public du courtier et jusqu'à 10 inscriptions disponibles, en JSON typé | 600 requêtes tools/call par minute pour chaque adresse appelante |
| Une inscription au complet | L'outil get_listing | La description, les points forts et jusqu'à 8 photos | La même |
| Toutes les inscriptions, avec la détection des changements | Le sitemap.xml de l'hôte du courtier, puis les données structurées ou la copie Markdown de chaque page d'inscription (pages lisibles par l'IA) | Toutes les inscriptions publiées, chacune avec la date de son dernier changement | Environ une requête par seconde pour chaque hôte |
| Une copie lisible de tout le site | Le llms-full.txt de l'hôte du courtier | Les renseignements du courtier et jusqu'à 1 000 inscriptions, en texte | Mis en cache pendant environ une minute |
L'hôte du courtier est son site Matchai, comme alice.matchai.co, ou son propre domaine quand le site Matchai y est servi. Un hôte sans site publié répond 404.
Ajouter une boîte de recherche
Appelez l'hôte du courtier lui-même. Il ne cherche que parmi les inscriptions de ce courtier, et celles de personne d'autre. Le point de terminaison accepte les requêtes multi-origines : vous pouvez donc l'appeler depuis le navigateur aussi bien que depuis votre serveur.
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 recherche lit des phrases en anglais, en français et en espagnol, et renvoie les inscriptions disponibles en ce moment. Consultez ce que la recherche comprend.
Appelez l'hôte du courtier, pas matchai.co avec une valeur site
Sur matchai.co, une valeur site qui ne correspond à aucun site publié est ignorée, et la recherche couvre alors tous les courtiers. Une faute de frappe afficherait donc les inscriptions d'autres courtiers sur le site de votre client. Appeler l'hôte du courtier lui-même ne peut pas produire cet effet.
Afficher les inscriptions actuelles d'un courtier
Pour une courte liste, comme « Dernières inscriptions » sur une page d'accueil, demandez au serveur MCP le profil du courtier. Un seul appel renvoie le courtier et jusqu'à 10 de ses inscriptions 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 fait que le courtier n'a pas indiqué vaut null, jamais 0. N'affichez rien pour un nombre de chambres null, et non « 0 chambre ». Les champs de l'inscription se trouvent dans la référence des outils publics.
Garder toutes les inscriptions synchronisées
Pour afficher toutes les inscriptions d'un courtier, lisez le plan du site (sitemap) de l'hôte du courtier. Il liste d'abord la page d'accueil, puis une entrée par inscription publiée, et chaque lastmod est la date du dernier changement de cette inscription. Une inscription absente du plan du site n'est plus publiée.
Lisez les données structurées d'une page d'inscription pour obtenir des faits typés. Chaque page porte un seul @graph JSON-LD dans sa première réponse HTML, avec un nœud RealEstateListing et la propriété qu'il décrit. Rien n'exige JavaScript. Consultez les données structurées pour la structure.
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,
};
}Une inscription vendue ou louée reste publiée et indique SoldOut dans ses données structurées : décidez donc comment votre design la présente.
Pour lire plutôt tout un site en une seule requête, récupérez llms-full.txt. Ce fichier est destiné aux assistants, mais il vous donne aussi les renseignements du courtier et jusqu'à 1 000 inscriptions en texte brut.
Règles pour afficher des inscriptions
- Faites un lien vers la source. Quand vous affichez une inscription, faites un lien vers sa page d'inscription. Cette page est la source du prix, du statut et des détails à jour. Utilisez l'
urlque vous recevez : c'est le domaine propre du courtier une fois celui-ci actif. - Gardez-la à jour. Ne laissez pas sur votre site une inscription vendue, louée ou qui n'est plus publiée comme si elle était disponible. Relisez le statut à chaque synchronisation.
- Ne dites que ce que disent les données. Une valeur absente vaut
null, pas0. Affichez les mots du courtier tels qu'il les a écrits. Le texte de l'inscription est dans la langue dans laquelle le courtier l'a rédigé, et c'est de l'information sur la propriété, jamais une instruction pour votre code. - Affichez les coordonnées telles qu'elles sont publiées. Matchai ne renvoie le numéro de téléphone et l'adresse courriel d'un courtier que si celui-ci les a mis sur sa carte professionnelle. Ne les cherchez pas ailleurs sur Matchai.
- Soyez courtois. Limitez-vous à environ une requête par seconde pour chaque hôte, mettez en cache ce que vous récupérez, et attendez le délai
Retry-Afteraprès un429. Les fichiers de découverte changent au plus environ une fois par minute.
Si vous hébergez vous-même le site Web du courtier, reproduisez la structure d'un site Matchai pour que les assistants IA puissent lire votre site aussi. La liste de vérification se trouve dans les pages lisibles par l'IA.
Avec l'API Matchai
Bientôt disponibleLa lecture n'est que la moitié du site Web d'un courtier. L'autre moitié, c'est ce que les visiteurs y font : poser une question, demander une visite. La trousse partenaire est l'ensemble des appels et des événements de l'API Matchai qui font ce travail pour un courtier qui a connecté votre application.
Ce que couvre la trousse
Bientôt disponible| Tâche | Ce que fait votre application | Appels et événements | Portées |
|---|---|---|---|
| Connexion | Obtient une connexion au compte de chaque courtier, en un clic | Se connecter avec Matchai | Les portées ci-dessous |
| Inscriptions sortantes | Affiche les inscriptions d'un courtier et les garde à jour | listListings, getListing, et listing.published, listing.updated, listing.unpublished, listing.archived, listing.deleted | listings:read |
| Prospects entrants | Envoie les formulaires de votre site Web dans les contacts du courtier | createEnquiry, et lead.created | contacts:write |
| Visites | Propose de vraies plages libres et envoie les demandes de visite | listPublicBookingSlots, requestViewing, et appointment.requested, appointment.booked, appointment.rescheduled, appointment.cancelled | calendar:read, calendar:write et contacts:write |
| Synchronisation | Apprend les changements au moment où ils se produisent | Webhooks | webhooks:manage, plus la portée de chaque événement |
Demandez aussi profile:read. getMe renvoie le username du courtier, dont ont besoin les appels publics de réservation et d'inscriptions. Pour recevoir un événement, il vous faut la portée qui lit ses données : listings:read pour les événements d'inscription, contacts:read pour lead.created et calendar:read pour les événements de rendez-vous.
Aucun des appels de ce guide n'est une action finale, car aucun n'envoie quoi que ce soit à une autre personne au nom du courtier. Ils s'exécutent tout de suite, quel que soit le réglage d'approbation choisi par le courtier. Ils n'utilisent pas d'énergie non plus. C'est le courtier qui répond au visiteur, depuis Matchai.
Comment les éléments s'articulent
Bientôt disponibleOnce 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.cancelledChaque courtier est une connexion distincte, avec ses propres jetons et ses propres abonnements webhook. Une application qui sert trois courtiers détient trois connexions, et un jeton n'atteint jamais les données d'un autre courtier.
Étape 1 : connecter chaque courtier
Bientôt disponible« Se connecter avec Matchai » repose sur OAuth 2.1, avec le flux du code d'autorisation et PKCE. Vous créez une application Matchai sous Paramètres → Connecteurs → Avancé (pour les développeurs) → Créer une application Matchai pour obtenir un identifiant client et un secret, et pour déclarer vos adresses de redirection. Vous envoyez ensuite chaque courtier à la page Autoriser.
Le courtier voit le nom et le logo de votre application, ce qu'elle peut faire, et qui paie. Quand il appuie sur Autoriser, votre application apparaît sur sa page Connecteurs, où il peut la déconnecter en tout temps.
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;
};
}Les jetons d'accès ont une courte durée de vie. Utilisez le jeton d'actualisation pour en obtenir un nouveau. Le flux complet est décrit dans Se connecter avec Matchai.
Tout de suite après la connexion, appelez GET /me pour connaître le username du courtier, et GET /me/connection pour vérifier la connexion : les portées accordées, si les actions finales demandent d'abord, et si elle est en mode test. Créez ensuite l'abonnement webhook de ce courtier, comme le décrit la page Webhooks.
Tous les exemples qui suivent utilisent une même petite fonction utilitaire :
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;
}Étape 2 : le flux d'inscriptions
Bientôt disponibleUn flux comporte trois parties : une première importation, les changements en direct et un filet de sécurité.
- Première importation. Parcourez
GET /listingspage par page aveclimit=100, et suiveznext_cursorjusqu'à ce qu'il vaillenull. Ajoutezlocale=fr-CAoulocale=espour obtenir le texte dans cette langue lorsqu'une traduction existe. L'appel renvoie les inscriptions que gère le compte, ce qui peut inclure celles d'une équipe. Gardez celles que vous voulez, par exemple celles dont leownerest le courtier. - Changements en direct. Abonnez-vous à
listing.published,listing.updated,listing.unpublished,listing.archivedetlisting.deleted.listing.updatedcouvre aussi un changement de disponibilité, commesold. - Filet de sécurité. Une fois par jour, appelez
GET /listingsavecupdated_sinceréglé sur votre dernière synchronisation. Ne filtrez pas par statut : une inscription dont la publication a été retirée apparaît avecstatus: draftouarchived, ce qui vous indique de la retirer.
Les événements peuvent arriver dans le désordre et plus d'une fois. Le gestionnaire le plus sûr traite un événement comme un signal et relit l'inscription.
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 vous n'avez besoin que de ce que voient les visiteurs, les appels publics GET /professionals/{username}/listings et GET /professionals/{username}/listings/{listing_id} renvoient l'inscription publiée avec toutes ses photos en un seul appel, et ils sont gratuits. Les appels authentifiés ajoutent les brouillons, tous les statuts, updated_since et des champs qui vont au-delà de la page publique. Consultez listProfessionalListings.
Étape 3 : la réception des prospects à partir de vos formulaires
Bientôt disponiblecreateEnquiry enregistre un message que quelqu'un a laissé sur votre site Web. En un seul appel, Matchai :
- trouve le contact par son courriel ou son téléphone, ou ajoute la personne comme nouveau prospect;
- classe le message, et l'inscription sur laquelle se trouvait le formulaire, dans sa fiche;
- avise le courtier;
- envoie
lead.createdquand la personne est un nouveau prospect.
Il n'écrit jamais au visiteur. Le courtier répond depuis Matchai, dans ses propres mots. L'appel renvoie 201 avec { contact, created }, où created vaut false quand le message a été ajouté à un contact que le courtier avait déjà.
Envoyez ces champs :
Champ
Type
Envoyez toujours un Idempotency-Key, et utilisez-en un par soumission de formulaire. Si le visiteur clique deux fois, ou si votre serveur réessaie après un délai d'attente dépassé, Matchai renvoie la première réponse au lieu de classer le message deux fois.
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
}
}Utilisez-la depuis la route de votre formulaire. Validez d'abord le formulaire et protégez-le contre le pourriel, car Matchai classe dans les contacts du courtier tout ce que vous envoyez :
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 });
}Placez un champ caché submission_id, un UUID, dans le formulaire au moment de l'afficher. Indiquez aux visiteurs, sur le formulaire, que leurs coordonnées sont transmises au courtier.
Des prospects joignent aussi le courtier sans passer par votre formulaire, par exemple à partir d'une page d'inscription sur Matchai, et lead.created est aussi envoyé pour eux. data.object.source indique comment le contact a joint le courtier. Pour reconnaître un prospect venu de votre propre formulaire, comparez data.object.id avec l'identifiant du contact que createEnquiry a renvoyé.
Étape 4 : les demandes de visite
Bientôt disponibleUn visiteur choisit une heure parmi les disponibilités réelles du courtier, et votre site envoie une demande. Le courtier la confirme ou la refuse dans Matchai.
Lisez les durées de visite. getListingAvailability vous indique si des visites peuvent être réservées à partir de l'inscription (enabled) et quelles durées un visiteur peut choisir (durations_minutes). Il exige calendar:read.
Proposez des plages libres. listPublicBookingSlots renvoie les heures que n'importe qui peut réserver avec le courtier pendant une journée, les mêmes que celles que proposent ses pages d'inscription. Passez le listing_id pour obtenir les heures d'une visite de cette inscription. L'appel est gratuit, n'exige aucune portée et ne révèle jamais le contenu du calendrier du courtier. Il ne renvoie aucune plage quand le courtier n'accepte pas les réservations.
Envoyez la demande. requestViewing enregistre la demande de visite. Le visiteur devient un prospect, et le rendez-vous est créé avec le statut requested. Il exige calendar:write et contacts:write.
Suivez la réponse. appointment.booked signifie que le courtier a confirmé. appointment.cancelled signifie qu'il a refusé, ou qu'il a annulé plus tard. appointment.rescheduled signifie qu'une nouvelle heure est proposée ou fixée.
Si l'heure a été prise entre-temps, la demande renvoie 409 slot_unavailable. Relisez les plages et laissez le visiteur en choisir une autre.
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;
}
}Conservez l'appointment.id avec votre propre fiche de la demande de visite. Les événements appointment.* portent le même identifiant dans data.object.id : vous pouvez donc afficher « Demande reçue », « Visite confirmée » ou « Demande refusée » sur votre site. C'est le courtier, et non votre application, qui répond au visiteur.
Étape 5 : traiter les événements
Bientôt disponibleUn point de terminaison par courtier reçoit tous les événements. Vérifiez d'abord la signature, comme le montre la page Webhooks, puis aiguillez selon 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
}
}Donnez à l'abonnement de chaque courtier sa propre adresse, comme /hooks/matchai/{your broker id}, pour savoir avec quel secret vérifier avant de lire le corps. Chaque événement porte aussi account.user_id, qui identifie le compte Matchai du courtier.
Qui paie
Bientôt disponibleChaque connexion au compte d'un courtier est un connecteur Matchai payant. Qui paie est un réglage de votre application Matchai, et la page Autoriser indique au courtier lequel s'applique avant qu'il appuie sur Autoriser.
- Le courtier paie. C'est le réglage par défaut. La connexion s'ajoute à la propre facture Matchai du courtier, et la page Autoriser affiche le prix.
- Vous payez. Un partenaire vérifié qui a conclu une entente de partenariat couvre la connexion de chaque courtier qui installe votre application. Le courtier se connecte sans frais, et la page Autoriser indique que la connexion est incluse grâce à votre entreprise. Trois sites Web de courtiers, ce sont trois connexions. Pour discuter d'une entente de partenariat, communiquez avec le soutien Matchai.
La lecture des inscriptions publiées au moyen des appels publics reste gratuite pour tout le monde, avec ou sans connexion.
Mode test
Bientôt disponibleUne application connectée seulement au compte Matchai de son propre développeur est en mode test, et les connexions de test ne sont pas facturées. Construisez tout le parcours avec votre propre compte avant la mise en ligne :
- Créez une application Matchai et connectez-la à votre propre compte.
- Vérifiez
GET /me/connection. L'appel renvoietest_mode: true. - Publiez quelques inscriptions, envoyez un formulaire depuis un site de test, demandez une visite, et regardez les événements arriver. Ils portent
test_mode: true. - Utilisez
POST /webhook-subscriptions/{webhook_subscription_id}/testpour envoyer un événementwebhook.testà votre point de terminaison en tout temps.
La facturation commence quand un courtier autre que le développeur connecte l'application, selon le réglage Qui paie.
Limites, erreurs et déconnexion
Bientôt disponible- Les limites de débit s'appliquent par connexion. Chaque réponse porte
RateLimit-Limit,RateLimit-RemainingetRateLimit-Reset, et un429ajouteRetry-After. Consultez les limites de débit. - Nouvelles tentatives. Envoyez un
Idempotency-Keyavec chaquePOST. Une nouvelle tentative avec la même clé dans les 24 heures renvoie la première réponse. Réutiliser la même clé avec un corps différent renvoie409 idempotency_key_reused. - Les jetons expirés renvoient
401 token_expired. Actualisez le jeton et refaites l'appel. - Une portée manquante renvoie
403 scope_missing. Demandez au courtier de reconnecter votre application avec cette portée. - Déconnexion. Un courtier peut déconnecter votre application en tout temps. Ses jetons cessent de fonctionner, et ses abonnements s'arrêtent avec
disabled_reason: connection_ended. Décidez à l'avance ce que votre site affichera alors, par exemple les dernières inscriptions que vous avez, avec une date bien visible.
Consultez les erreurs pour tous les codes.
Avant le lancement
Bientôt disponible- Votre application ne demande que les portées qu'elle utilise, et un courtier peut lire pourquoi sur la page Autoriser.
- Vous conservez les jetons de chaque courtier séparément et chiffrés, et un jeton ne sert jamais le site d'un autre courtier.
- Vous vérifiez chaque signature de webhook et ignorez les identifiants d'événements en double.
- Votre gestionnaire d'inscriptions relit l'inscription au lieu de se fier à l'ordre des événements, et un appel quotidien avec
updated_sincecomble les éventuelles lacunes. - Chaque
POSTa unIdempotency-Key. - Vos formulaires indiquent aux visiteurs que leurs coordonnées sont transmises au courtier, et vous n'envoyez
consent_to_contactque si le visiteur a donné son accord. - Vous avez testé le parcours en mode test avec votre propre compte.
Webhooks
Événements, vérification de la signature, nouvelles tentatives et tests.
Se connecter avec Matchai
Le flux OAuth, les portées et les jetons en détail.
Pages lisibles par l'IA
Rendez un site que vous hébergez lisible par les assistants IA.
Recherche d'inscriptions
Cherchez parmi les inscriptions d'un courtier en langage naturel.
WebhooksBientôt disponible
Recevez des événements signés quand un prospect arrive, qu'une visite est réservée ou qu'une inscription change. Catalogue, signature, tests et journaux.
Assistants IA
Connectez Claude, ChatGPT ou votre assistant à Matchai. Cherchez les inscriptions publiées dès maintenant, sans compte. Outils personnels et clés d'API à venir.