Ajouter un point de terminaison
Connectez-vous, ouvrez Intégrations à partir de votre compte, puis ajoutez l’URL de votre point de terminaison sous Webhooks. Elle doit commencer par https. Choisissez les événements qu’il reçoit, ou tous.
Nolan affiche alors une seule fois le secret de signature du point de terminaison. Rangez-le avec vos autres secrets : il sert à vérifier qu’une requête vient bien de Nolan. « Nouveau secret » le remplace, et l’ancien cesse aussitôt de fonctionner.
« Envoyer un événement test » livre tout de suite un événement ping à ce point de terminaison, pour vérifier votre récepteur avant l’arrivée des vrais événements.
Ce que Nolan envoie
Une requête POST avec un corps JSON et ces en-têtes :
Content-Type | application/json |
LeaseOwl-Event-Id | L’identifiant de l’événement, le même à chaque nouvelle tentative. Servez-vous-en pour ignorer un événement déjà traité. |
LeaseOwl-Event-Type | Le type d’événement, par exemple lead.created. |
LeaseOwl-Delivery-Id | Cette livraison de l’événement à ce point de terminaison. |
LeaseOwl-Timestamp | Le moment de cette tentative, en secondes Unix. |
LeaseOwl-Signature | t=<horodatage>,v1=<signature>. Voir plus bas. |
Vérifier la signature
La signature est un HMAC-SHA256, en hexadécimal minuscule, de l’horodatage, d’un point et du corps brut de la requête, avec le secret de signature du point de terminaison comme clé. Calculez-la sur le corps tel que reçu, avant toute lecture du JSON.
Refusez la requête si aucune valeur v1 ne correspond, ou si l’horodatage s’écarte de plus de cinq minutes de votre horloge. Comparez les signatures en temps constant. Pendant le remplacement d’un secret, vérifiez avec les deux.
Node.js
import crypto from 'node:crypto';
// rawBody: the request body exactly as received, as a string.
export function isFromLeaseOwl(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(signatureHeader.split(',').map((part) => part.trim().split('=', 2)));
const timestamp = Number(parts.t);
if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? '', 'hex');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}Nouvelles tentatives et échecs
- Une livraison réussit quand votre point de terminaison répond avec un statut 2xx en moins de 10 secondes. Répondez d’abord, faites le travail ensuite.
- Toute autre réponse est relivrée après environ 1 minute, 5 minutes, 30 minutes, 2 heures, 6 heures, 12 heures et 24 heures : huit tentatives sur environ deux jours. Les relivraisons partent toutes les 15 minutes, alors les courts délais peuvent s’allonger.
- La livraison se fait au moins une fois. Un même événement peut arriver deux fois, et les événements peuvent arriver dans le désordre : utilisez l’identifiant de l’événement pour écarter les doublons, et les dates dans les données pour les ordonner.
- Les redirections ne sont pas suivies. Utilisez l’URL finale.
- Après la dernière tentative, la livraison est marquée en échec. Les livraisons en échec sont listées sous Webhooks dans votre page Intégrations, avec un bouton Réessayer.
- Un point de terminaison qui répond 410 Gone est désactivé. C’est ainsi que Zapier et Make se désabonnent.
L’événement
Chaque événement a la même enveloppe. data contient les objets propres à son type.
{
"id": "evt_Qm3c9rT2xV8kLw1sZp4bNg",
"type": "showing.booked",
"created_at": "2026-10-07T23:47:12.311Z",
"api_version": "2026-10-05",
"organization_id": "4c0d6f3e-2b8a-4f61-9a57-0f3c1d2e8b90",
"data": {
"showing": {
"id": "b7c1f0a2-5d3e-4c8b-8e21-6a9f0d4c3b12",
"status": "booked",
"starts_at": "2026-10-09T22:00:00.000Z",
"ends_at": null,
"when": "Thursday, October 9 at 6 p.m.",
"previous_starts_at": null,
"address": "Rue Saint-Denis near Rue Rachel",
"listing": { "id": "7f3a1c2d-...", "address": "Rue Saint-Denis near Rue Rachel", "rent": 1950 }
},
"lead": {
"id": "b1e4a9d0-...",
"name": "Maya Tremblay",
"phone": "+15145550142",
"email": "",
"status": "showing_booked",
"lost_reason": null,
"channel": "phone",
"source": "Kijiji",
"source_line": "+15145550100",
"source_page": null,
"listing": { "id": "7f3a1c2d-...", "address": "Rue Saint-Denis near Rue Rachel", "rent": 1950 },
"wants": { "move_in": "November 1", "household": "2 adults", "pets": "1 cat", "unit": "Rue Saint-Denis near Rue Rachel" },
"summary": "Wants the Plateau 4½ for November 1, works nearby, has one cat.",
"callback_requested": false,
"owner": null,
"booking_id": "b7c1f0a2-5d3e-4c8b-8e21-6a9f0d4c3b12",
"showing_at": "2026-10-09T22:00:00.000Z",
"showing_outcome": null,
"created_at": "2026-10-07T23:41:55.120Z",
"last_contact_at": "2026-10-07T23:47:10.004Z"
}
}
}Types d’événements
lead.created | Un locataire vous joint pour la première fois, ou un prospect est ajouté par importation, par l’API ou par Zapier. data : lead. |
lead.updated | Un élément du prospect a changé : coordonnées, responsable, résumé, statut ou visite, que Nolan ou votre équipe l’ait changé. data : lead, changed (les champs modifiés, nommés comme dans l’objet lead : name, wants.move_in, owner...). |
lead.status_changed | Un prospect a changé d’étape : new, contacted, qualified, showing_booked, showed, applied, leased ou lost. Aussi envoyé comme lead.updated. data : lead, previous_status, status, detail. |
conversation.message | Un message a été reçu ou envoyé dans un fil de textos, de clavardage ou de courriels. Un appel arrive en un seul message avec son résumé. Un brouillon de Nolan part une fois que votre équipe l’approuve. data : conversation, message (direction inbound ou outbound, author renter, charlie ou team), lead. |
showing.booked | Une visite a été réservée, ou déplacée (showing.status vaut rescheduled). data : showing, lead. |
showing.cancelled | Une visite a été annulée. data : showing, lead. |
showing.showed | Le locataire s’est présenté à la visite. data : showing, lead. |
L’objet lead
id | L’identifiant Nolan du prospect. |
name, phone, email | Tels que le locataire les a donnés. Les numéros sont au format E.164 (+15145550142). |
status, lost_reason | new, contacted, qualified, showing_booked, showed, applied, leased ou lost, et la raison donnée par l’équipe quand un prospect est perdu (sinon null). |
channel | Comment le locataire vous a joint la première fois : phone, sms, chat, email, messenger ou instagram. |
source, source_line, source_page | D’où vient le prospect, si on le sait : le site d’annonces (ou une importation CSV, Zapier), le numéro Nolan appelé ou texté, et la page où le clavardage a été ouvert. Chacun vaut null si on ne le sait pas. |
listing | Le logement demandé (id, address, rent), ou null. |
wants | Ce que le locataire cherche : move_in, household, pets, et unit (l’adresse du logement voulu, une fois connue). |
summary | Le résumé de Nolan des conversations jusqu’ici. |
owner | Le membre de l’équipe responsable du prospect (id, email, name), ou null. |
callback_requested | true quand le locataire a demandé que l’équipe le rappelle. |
booking_id, showing_at, showing_outcome | La visite en cours : son identifiant, son début (ISO 8601), et showed ou no_show une fois que l’équipe l’indique. null s’il n’y en a pas. |
created_at, last_contact_at | La création du prospect, et le dernier appel ou message du locataire (ISO 8601). |
API REST
Zapier, Make et vos propres scripts utilisent la même API, à https://getnolan.ai/api/v1/. Créez une clé sous « Clés d’API » dans votre page Intégrations. Elle n’est affichée qu’une fois.
Envoyez la clé dans l’en-tête Authorization comme jeton Bearer, jamais dans une URL. Une clé donne accès à une seule organisation : tout le reste répond 404.
| GET | /api/v1/me | L’organisation à laquelle appartient la clé. Pratique pour tester une clé. |
| GET | /api/v1/events?type=lead.created&limit=50 | Les événements récents, du plus récent au plus ancien, gardés 30 jours. Pour interroger l’API au lieu de recevoir des webhooks. |
| POST | /api/v1/hooks | Abonner une URL à un type d’événement (REST Hooks). Corps : url, event. La réponse donne l’identifiant et le secret de signature. |
| DELETE | /api/v1/hooks/{id} | Se désabonner. |
| GET | /api/v1/leads?status=new&channel=sms&limit=50 | Les prospects, du dernier contact au plus ancien. Les prospects fusionnés ou supprimés sont exclus. |
| POST | /api/v1/leads | Ajouter un prospect. Il faut un numéro de téléphone ou un courriel. Un prospect au même numéro ou au même courriel est complété plutôt que dédoublé : seulement les champs vides, jamais ce que Nolan ou votre équipe a noté. |
| GET | /api/v1/listings | Vos logements, les actifs d’abord. |
Ajouter un prospect
curl https://getnolan.ai/api/v1/leads \
-H "Authorization: Bearer $LEASEOWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Maya Tremblay", "phone": "514-555-0142", "source": "Kijiji", "summary": "Asked about the Plateau 4½"}'Les erreurs répondent avec un statut et un corps JSON : { "error": { "code": "...", "message": "..." } }.
Versions
Chaque événement porte api_version. De nouveaux champs et de nouveaux types d’événements peuvent apparaître en tout temps : ignorez ce que vous ne connaissez pas. Un changement qui retire ou renomme un champ reçoit une nouvelle version, annoncée d’abord sur cette page.