Matchai Docs
Guides

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 :

  1. 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.
  2. 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 deUtilisezVous obtenezLimite
Une boîte de recherche sur le site du courtierGET https://{broker host}/ask (recherche d'inscriptions)Jusqu'à 10 inscriptions, la meilleure correspondance d'abord, chacune avec des données schema.org60 requêtes par minute pour chaque adresse appelante
Les inscriptions actuelles du courtierL'outil get_professional du serveur MCPLe 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 completL'outil get_listingLa description, les points forts et jusqu'à 8 photosLa même
Toutes les inscriptions, avec la détection des changementsLe 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 changementEnviron une requête par seconde pour chaque hôte
Une copie lisible de tout le siteLe llms-full.txt de l'hôte du courtierLes renseignements du courtier et jusqu'à 1 000 inscriptions, en texteMis 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.

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.

search-box.ts
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.

broker-listings.ts
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.

sync-from-sitemap.ts
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('&amp;', '&'), 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'url que 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, pas 0. 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-After après un 429. 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 disponible

La 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âcheCe que fait votre applicationAppels et événementsPortées
ConnexionObtient une connexion au compte de chaque courtier, en un clicSe connecter avec MatchaiLes portées ci-dessous
Inscriptions sortantesAffiche les inscriptions d'un courtier et les garde à jourlistListings, getListing, et listing.published, listing.updated, listing.unpublished, listing.archived, listing.deletedlistings:read
Prospects entrantsEnvoie les formulaires de votre site Web dans les contacts du courtiercreateEnquiry, et lead.createdcontacts:write
VisitesPropose de vraies plages libres et envoie les demandes de visitelistPublicBookingSlots, requestViewing, et appointment.requested, appointment.booked, appointment.rescheduled, appointment.cancelledcalendar:read, calendar:write et contacts:write
SynchronisationApprend les changements au moment où ils se produisentWebhookswebhooks: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 disponible
Once 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.cancelled

Chaque 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.

connect.ts
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 :

matchai.ts
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 disponible

Un flux comporte trois parties : une première importation, les changements en direct et un filet de sécurité.

  • Première importation. Parcourez GET /listings page par page avec limit=100, et suivez next_cursor jusqu'à ce qu'il vaille null. Ajoutez locale=fr-CA ou locale=es pour 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 le owner est le courtier.
  • Changements en direct. Abonnez-vous à listing.published, listing.updated, listing.unpublished, listing.archived et listing.deleted. listing.updated couvre aussi un changement de disponibilité, comme sold.
  • Filet de sécurité. Une fois par jour, appelez GET /listings avec updated_since réglé sur votre dernière synchronisation. Ne filtrez pas par statut : une inscription dont la publication a été retirée apparaît avec status: draft ou archived, 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.

listings-feed.ts
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 disponible

createEnquiry 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.created quand 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.

enquiry.ts
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 :

contact-form.ts
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 disponible

Un 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.

viewing.ts
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 disponible

Un 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.

partner-events.ts
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 disponible

Chaque 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 disponible

Une 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 :

  1. Créez une application Matchai et connectez-la à votre propre compte.
  2. Vérifiez GET /me/connection. L'appel renvoie test_mode: true.
  3. 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.
  4. Utilisez POST /webhook-subscriptions/{webhook_subscription_id}/test pour envoyer un événement webhook.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-Remaining et RateLimit-Reset, et un 429 ajoute Retry-After. Consultez les limites de débit.
  • Nouvelles tentatives. Envoyez un Idempotency-Key avec chaque POST. 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 renvoie 409 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_since comble les éventuelles lacunes.
  • Chaque POST a un Idempotency-Key.
  • Vos formulaires indiquent aux visiteurs que leurs coordonnées sont transmises au courtier, et vous n'envoyez consent_to_contact que si le visiteur a donné son accord.
  • Vous avez testé le parcours en mode test avec votre propre compte.

Sur cette page