Développeurs · API v1
L'API MyHostKit, pour relier votre CRM
L'API relie votre CRM, votre outil de facturation ou vos tableaux de bord à votre compte MyHostKit. Vos logements, réservations, ménages, dépenses, tarifs et relevés propriétaires arrivent chez vous en JSON, et MyHostKit prévient votre serveur, en général dans la minute, quand une réservation, un ménage ou une dépense change.
https://www.myhostkit.com/api/v1JSON, UTF-8, en entrée comme en sortieÀ quoi sert l'API
Elle s'adresse aux conciergeries et aux agences clientes de MyHostKit qui ont leur propre outil (CRM, back-office, facturation, tableaux de bord) et un développeur, salarié ou prestataire, pour le brancher.
- Lire : logements, disponibilités et prix jour par jour, réservations, arrivées et départs du jour, règles tarifaires, propriétaires, relevés propriétaires, dépenses, missions de ménage, incidents.
- Écrire : dépenses, règles tarifaires (envoyées aussitôt à Airbnb, Booking.com et aux autres plateformes quand le logement est relié au channel manager), incidents, consignes de ménage.
- Être prévenu : des webhooks signés pour les réservations, les ménages, les incidents, les dépenses et les tarifs.
L'API est disponible pour les comptes MyHostKit directs dont l'accès est actif (abonnement, essai ou licence). Les comptes ouverts sous une marque partenaire n'y ont pas accès. Seul le titulaire du compte crée des clés : ni un agent de ménage, ni un membre d'équipe.
Elle ne renvoie jamais les codes d'accès, les mots de passe Wi-Fi, les liens voyageurs, les adresses iCal ni les identifiants internes du channel manager.
Démarrage en 3 étapes
- Créez une clé dans l'app
Dans l'app MyHostKit, ouvrez Réglages, puis API et webhooks, et créez une clé. Donnez-lui un nom (par exemple « CRM ») et ses droits :
readpour lire et, si besoin,writepour écrire.Copiez la clé tout de suite. Elle commence par
mhk_live_et n'est affichée qu'une fois : MyHostKit n'en garde qu'une empreinte. Une clé perdue ne se retrouve pas, elle se révoque et se remplace. - Faites un premier appel
bash
curl https://www.myhostkit.com/api/v1/me \ -H "Authorization: Bearer mhk_live_VOTRE_CLE"réponse 200{ "data": { "id": "0b6f3c2e-5d1a-4e8f-9a7b-1c2d3e4f5a6b", "company_name": "Conciergerie Lagon Bleu", "email": "contact@lagonbleu.example", "currency": "EUR", "scopes": ["read", "write"], "api_key": { "id": "9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a", "name": "CRM", "prefix": "mhk_live_k3J" }, "created_at": "2026-03-14T09:12:44.000Z", "updated_at": null } } - Synchronisez avec
updated_sinceLisez une première fois toutes les pages de chaque liste. Ensuite, ne demandez que ce qui a changé depuis votre dernier passage :
bashcurl "https://www.myhostkit.com/api/v1/bookings?updated_since=2026-10-02T08:00:00Z&limit=200" \ -H "Authorization: Bearer $MHK_KEY"Le détail est plus bas, dans Synchronisation incrémentale, avec un exemple complet en JavaScript.
Authentification
Chaque requête porte la clé dans l'en-tête Authorization :
Authorization: Bearer mhk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- Droits.
readouvre les routesGET.writeouvre les routesPOST,PATCHetDELETE. Une clé peut avoir l'un, l'autre ou les deux. Sans le bon droit, la réponse est403 insufficient_scope. - Périmètre. Une clé donne accès aux données du seul compte qui l'a créée : ses logements, réservations, ménages, dépenses, tarifs, propriétaires et incidents. Elle ne voit jamais un autre compte.
- Révocation. Une clé révoquée dans l'app répond aussitôt
401 revoked_api_key. - Conservation. Traitez la clé comme un mot de passe : gardez-la côté serveur (variable d'environnement, coffre de secrets), jamais dans une page web ni dans une application mobile. L'API n'envoie pas d'en-têtes CORS sur ses routes à clé : un navigateur ne peut pas l'appeler directement.
- Accès au compte. Si l'accès du compte à MyHostKit n'est plus actif, la réponse est
403 acces_inactif. Pour un compte ouvert sous une marque partenaire :403 api_non_incluse. Pour la clé d'un agent de ménage ou d'un membre d'équipe :403 owner_only. Les webhooks sont alors suspendus eux aussi (voir Webhooks).
Les réservations contiennent le nom et l'e-mail des voyageurs : dans votre CRM, traitez-les avec les mêmes précautions que dans MyHostKit.
Format des réponses
// Liste
{ "data": [ { ... }, { ... } ], "next_cursor": "eyJ2Ijoi..." }
// Objet seul
{ "data": { ... } }
// Erreur
{ "error": { "code": "not_found", "message": "Ressource introuvable dans ce compte." } }- Chaque objet enregistré porte
id(UUID),created_atetupdated_at. - Les horodatages sont en ISO 8601, en UTC :
2026-10-02T08:15:00.123Z. Les jours sont au formatAAAA-MM-JJ:2026-10-02. - Les montants sont des nombres décimaux (deux décimales au plus), toujours accompagnés de
currency, la devise du compte (EURpar défaut). Seule la caution d'une réservation a sa propre devise,EUR, parce qu'elle est encaissée en euros. - Un champ sans valeur vaut
null. Un champ n'est jamais omis. - Les objets calculés à la demande (disponibilités, mouvements du jour, relevé propriétaire) n'ont ni
idni dates de création, puisqu'ils ne sont pas enregistrés. updated_atvautnullpour/me: la date de modification du compte n'est pas suivie.- Les réservations, ménages, incidents, dépenses, règles tarifaires et propriétaires créés avant l'ouverture de l'API (2 octobre 2026) portent cette date comme
updated_at.
Pagination
Les listes sont paginées par curseur. Elles sont triées par updated_at croissant, puis par id. Seuls les relevés générés (/owner-reports) sont triés par created_at, puis par id.
| Paramètre | Description |
|---|---|
limit | Nombre d'éléments par page, de 1 à 200 (50 par défaut) |
cursor | Valeur next_cursor de la page précédente, recopiée telle quelle |
updated_since | Ne renvoie que ce qui a été créé ou modifié depuis cette date ISO 8601 (incluse) |
Tant que next_cursor n'est pas null, il reste une page à lire :
curl "https://www.myhostkit.com/api/v1/bookings?limit=100" -H "Authorization: Bearer $MHK_KEY"
# ... "next_cursor": "eyJ2IjoiMjAyNi0xMC0wMlQwODoxNTowMC4xMjM0NTYrMDA6MDAiLCJpZCI6Ii4uLiJ9"
curl "https://www.myhostkit.com/api/v1/bookings?limit=100&cursor=eyJ2IjoiMjAyNi0xMC0wMlQwODoxNTowMC4xMjM0NTYrMDA6MDAiLCJpZCI6Ii4uLiJ9" \
-H "Authorization: Bearer $MHK_KEY"Le curseur est opaque : ne le construisez pas vous-même. Un curseur modifié répond 400 invalid_cursor. Gardez les mêmes filtres d'une page à l'autre.
Synchronisation incrémentale
Pour tenir un CRM à jour sans tout relire :
- Première synchronisation : lisez toutes les pages de chaque liste.
- Retenez la plus grande valeur
updated_atreçue, ou l'heure de début de la synchronisation moins une minute de marge. - Ensuite, appelez chaque liste avec
updated_since=<cette valeur>et lisez toutes les pages.
updated_since est inclusif : un objet peut revenir deux fois. Enregistrez les objets par leur id (mise à jour s'il existe, création sinon).
const BASE = "https://www.myhostkit.com/api/v1";
const KEY = process.env.MHK_KEY; // Deno / Supabase Edge : Deno.env.get("MHK_KEY")
// Lit toutes les pages d'une liste, en respectant la limite de 120 requêtes par minute.
async function toutLire(chemin, params = {}) {
const objets = [];
let cursor = null;
for (;;) {
const qs = new URLSearchParams({ ...params, limit: "200" });
if (cursor) qs.set("cursor", cursor);
const rep = await fetch(`${BASE}${chemin}?${qs}`, { headers: { Authorization: `Bearer ${KEY}` } });
if (rep.status === 429) { // limite atteinte : attendre, puis redemander la même page
const attente = Number(rep.headers.get("Retry-After") || 60);
await new Promise((r) => setTimeout(r, attente * 1000));
continue;
}
const corps = await rep.json();
if (!rep.ok) throw new Error(`${rep.status} ${corps.error.code} : ${corps.error.message}`);
objets.push(...corps.data);
if (!corps.next_cursor) return objets;
cursor = corps.next_cursor;
}
}
// Date du passage précédent, gardée dans votre base (null la première fois).
let derniereSynchro = null;
const debut = new Date(Date.now() - 60_000).toISOString(); // une minute de marge
const reservations = await toutLire("/bookings", derniereSynchro ? { updated_since: derniereSynchro } : {});
for (const r of reservations) {
// enregistrer r dans votre CRM par r.id (mise à jour ou création)
}
derniereSynchro = debut; // à sauvegarder pour le prochain passageupdated_at ne change que lorsqu'un champ renvoyé par l'API change : une écriture technique de notre côté (proposition d'une mission aux agents, consultation du portail par un propriétaire, facture jointe à une dépense...) ne fait pas revenir l'objet. Exception : pour les logements, updated_at suit aussi les changements techniques, dont la synchronisation des calendriers (toutes les 15 minutes pour un logement relié en iCal). Un logement peut donc revenir dans /properties?updated_since= sans changement visible : comparez les champs avant de le traiter comme une modification.
Une liste ne montre pas ce qui a été supprimé. Pour le savoir, abonnez-vous aux webhooks : expense.deleted, rate.deleted, et deleted: true dans l'enveloppe des autres événements. Si des événements ont été manqués (point de terminaison désactivé, livraison en échec définitif, accès du compte inactif), seule une synchronisation complète rattrape les suppressions : relisez toutes les pages de chaque liste, sans updated_since, puis effacez de votre côté ce qui n'y figure plus (voir Réponse attendue et nouveaux essais).
Sur /owner-reports, updated_since porte sur la date de génération (created_at) : un relevé ne change plus après sa génération, sauf sa date d'envoi.
Erreurs et limites
{
"error": {
"code": "validation_error",
"message": "date_to doit être égale ou postérieure à date_from.",
"field": "date_to"
}
}codeest stable : votre code peut s'y fier.messageest en français, lisible par une personne. Il peut changer.field, quand il est présent, nomme le paramètre ou le champ en cause.
| Statut | code | Quand |
|---|---|---|
| 400 | validation_error | Paramètre ou champ invalide (voir field) |
| 400 | invalid_json | Corps JSON illisible, ou qui n'est pas un objet |
| 400 | invalid_cursor | Curseur modifié ou illisible |
| 400 | field_not_editable | Champ inconnu ou non modifiable dans le corps |
| 401 | missing_api_key | En-tête Authorization absent |
| 401 | invalid_api_key | Clé inconnue ou mal formée |
| 401 | revoked_api_key | Clé révoquée |
| 403 | insufficient_scope | La clé n'a pas le droit read ou write demandé |
| 403 | acces_inactif | L'accès du compte à MyHostKit n'est pas actif |
| 403 | api_non_incluse | Compte ouvert sous une marque partenaire : pas d'API |
| 403 | owner_only | Clé d'un agent de ménage ou d'un membre d'équipe |
| 404 | not_found | Objet absent de votre compte, ou identifiant mal formé |
| 404 | route_not_found | Route inconnue |
| 405 | method_not_allowed | Méthode non acceptée sur cette route (voir l'en-tête Allow) |
| 413 | payload_too_large | Corps de plus de 100 ko |
| 422 | unknown_property | Le property_id envoyé n'est pas un logement de votre compte |
| 422 | unknown_booking | Le booking_id envoyé n'est pas une réservation de votre compte |
| 422 | booking_property_mismatch | La réservation concerne un autre logement |
| 422 | invalid_reference | Référence refusée par la base |
| 429 | rate_limited | Plus de 120 requêtes par minute, ou plus de 10 tests de webhook par minute depuis l'app (voir Retry-After) |
| 500 | internal_error | Erreur de notre côté : réessayez un peu plus tard |
Limites
| Limite | Valeur |
|---|---|
| Requêtes | 120 par minute et par clé (fenêtre glissante de 60 secondes) |
| Au-delà | 429 rate_limited, avec l'en-tête Retry-After (en secondes) |
| En-têtes renvoyés | X-RateLimit-Limit: 120 et X-RateLimit-Remaining (requêtes restantes) |
| Taille d'une page | limit de 1 à 200, 50 par défaut |
| Disponibilités | 90 jours par appel au plus |
| Corps d'une requête | 100 ko au plus |
| Clés actives par compte | 10 |
| Points de terminaison webhook par compte | 10 |
| Tests de webhook (bouton « Tester » de l'app) | 10 par minute et par compte |
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : 120 par minute et par clé au plus." } }Sur un 429, attendez la durée indiquée par Retry-After avant de réessayer.
Toutes les routes
Les chemins partent de l'adresse de base https://www.myhostkit.com/api/v1.
| Méthode | Route | Droit |
|---|---|---|
| GET | /me | read |
| GET | /properties, /properties/{id} | read |
| GET | /properties/{id}/availability | read |
| GET | /bookings, /bookings/{id} | read |
| GET | /movements | read |
| GET | /rates, /rates/{id} | read |
| GET | /owners, /owners/{id} | read |
| GET | /owner-statements | read |
| GET | /owner-reports, /owner-reports/{id} | read |
| GET | /expenses, /expenses/{id} | read |
| GET | /cleanings, /cleanings/{id} | read |
| GET | /incidents, /incidents/{id} | read |
| POST | /expenses | write |
| PATCH DELETE | /expenses/{id} | write |
| POST | /rates | write |
| PATCH DELETE | /rates/{id} | write |
| POST | /incidents | write |
| PATCH | /incidents/{id} | write |
| PATCH | /cleanings/{id} | write |
Routes en lecture
Toutes ces routes demandent le droit read. Les listes acceptent limit, cursor et updated_since (voir Pagination).
Le compte
GET/me
Le compte de la clé : id, company_name (nom de la société), email, currency (devise du compte), scopes (droits de la clé), api_key (id, name, prefix), created_at, updated_at. Exemple de réponse au démarrage.
Logements
GET/properties
GET/properties/{id}
| Champ | Description |
|---|---|
name, address, city, type | Texte. type est saisi librement dans l'app (« Villa », « Appartement »...) |
capacity | Nombre de voyageurs |
bedrooms | Nombre de chambres |
price_per_night, currency | Prix de base par nuit |
check_in_time, check_out_time | Heures d'arrivée et de départ (« 15:00 ») |
commission_rate | Taux de commission de la conciergerie, en %. Vide dans l'app : 20. Un logement à 100 % est un logement que la conciergerie loue pour son propre compte |
status | active... |
platforms | Plateformes reliées : calendriers iCal ajoutés (airbnb, booking, vrbo, abritel, other...) et annonce Airbnb importée |
channel_manager | Booléen : le logement est relié au channel manager (synchronisation Airbnb, Booking.com) |
{
"data": [
{
"id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "Villa Azur",
"address": "12 chemin des Filaos",
"city": "Grand Baie",
"type": "Villa",
"capacity": 8,
"bedrooms": 4,
"price_per_night": 350,
"currency": "EUR",
"check_in_time": "15:00",
"check_out_time": "11:00",
"commission_rate": 20,
"status": "active",
"platforms": ["airbnb", "booking"],
"channel_manager": true,
"created_at": "2026-04-02T10:00:00.000Z",
"updated_at": "2026-09-28T16:41:07.512Z"
}
],
"next_cursor": null
}Disponibilités et prix jour par jour
GET/properties/{id}/availability
| Paramètre | Description |
|---|---|
from | Premier jour (AAAA-MM-JJ). Par défaut : aujourd'hui, heure de Paris |
to | Dernier jour, inclus. Par défaut : from + 29 jours. 90 jours au plus |
Chaque jour : date, available (la nuit est libre), booking_id (réservation ou période bloquée qui occupe la nuit, sinon null), price (prix de la nuit), min_stay (séjour minimum en nuits), currency. Pas de pagination : next_cursor vaut null.
Le calcul est le même que dans l'app et que pour les tarifs envoyés aux plateformes :
- une nuit est occupée par une réservation confirmée ou une période bloquée dont l'arrivée est au plus tard ce jour-là et le départ après ce jour-là ;
- prix : la règle tarifaire la plus récemment créée qui couvre la nuit ; son prix week-end s'applique les vendredis et samedis s'il est renseigné ; hors règle, le prix de base du logement (
nulls'il n'y en a pas) ; - séjour minimum : celui de la règle, sinon 1. Seul un séjour minimum fixé par une règle est envoyé aux plateformes ; un minimum posé directement sur les plateformes, hors règle, n'apparaît pas ici (voir Règles tarifaires).
curl "https://www.myhostkit.com/api/v1/properties/5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b/availability?from=2026-11-06&to=2026-11-07" \
-H "Authorization: Bearer $MHK_KEY"{
"data": [
{ "date": "2026-11-06", "available": false, "booking_id": "a1b2c3d4-1111-4222-8333-444455556666", "price": 200, "min_stay": 1, "currency": "EUR" },
{ "date": "2026-11-07", "available": true, "booking_id": null, "price": 180, "min_stay": 3, "currency": "EUR" }
],
"next_cursor": null
}Réservations
GET/bookings
GET/bookings/{id}
Les réservations, ainsi que les périodes bloquées (status: "blocked", qui ne sont pas des voyageurs).
| Paramètre | Description |
|---|---|
property_id | Un logement |
from, to | Séjours qui touchent cette période : départ le from ou après, arrivée le to ou avant |
status | Un statut, ou une liste séparée par des virgules : confirmed,blocked |
updated_since, limit, cursor | Voir Pagination |
| Champ | Description |
|---|---|
property_id | Logement |
status | confirmed, cancelled, blocked... |
platform | airbnb, booking, vrbo, abritel, direct... |
source | Origine de la saisie : channel_manager (channel manager), ical, manual, direct (réservation directe payée en ligne)... |
check_in, check_out, nights | Dates du séjour et nombre de nuits |
amount, currency | Montant du séjour |
guests_count | Nombre de voyageurs |
payment_status, paid_at | Paiement en ligne d'une réservation directe : paid..., sinon null |
deposit | Caution : { status, amount, captured_amount, currency }, ou null sans caution |
guest | { name, email, guests_count } |
curl "https://www.myhostkit.com/api/v1/bookings?from=2026-10-01&to=2026-10-31&status=confirmed" \
-H "Authorization: Bearer $MHK_KEY"{
"data": {
"id": "a1b2c3d4-1111-4222-8333-444455556666",
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "confirmed",
"platform": "airbnb",
"source": "channel_manager",
"check_in": "2026-10-10",
"check_out": "2026-10-14",
"nights": 4,
"amount": 812.4,
"currency": "EUR",
"guests_count": 3,
"payment_status": null,
"paid_at": null,
"deposit": null,
"guest": { "name": "Marie Dupont", "email": "marie.dupont@example.com", "guests_count": 3 },
"created_at": "2026-09-20T08:59:00.123Z",
"updated_at": "2026-09-21T08:00:00.000Z"
}
}Arrivées et départs du jour
GET/movements
Arrivées et départs d'un jour, regroupés par logement. Ni les annulations ni les périodes bloquées n'y figurent.
| Paramètre | Description |
|---|---|
date | Le jour (AAAA-MM-JJ). Par défaut : aujourd'hui, heure de Paris |
Chaque élément : date, property (id, name, check_in_time, check_out_time), check_ins et check_outs (réservations complètes, même forme que /bookings). Pas de pagination : next_cursor vaut toujours null.
{
"data": [
{
"date": "2026-10-14",
"property": { "id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b", "name": "Villa Azur", "check_in_time": "15:00", "check_out_time": "11:00" },
"check_ins": [],
"check_outs": [
{
"id": "a1b2c3d4-1111-4222-8333-444455556666",
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "confirmed",
"platform": "airbnb",
"source": "channel_manager",
"check_in": "2026-10-10",
"check_out": "2026-10-14",
"nights": 4,
"amount": 812.4,
"currency": "EUR",
"guests_count": 3,
"payment_status": null,
"paid_at": null,
"deposit": null,
"guest": { "name": "Marie Dupont", "email": "marie.dupont@example.com", "guests_count": 3 },
"created_at": "2026-09-20T08:59:00.123Z",
"updated_at": "2026-09-21T08:00:00.000Z"
}
]
}
],
"next_cursor": null
}Règles tarifaires
GET/rates
GET/rates/{id}
Les règles tarifaires saisonnières (écran « Tarifs saisonniers » de l'app). Paramètres : property_id, updated_since, limit, cursor.
Champs : property_id, name, date_from, date_to (inclus), price, weekend_price (vendredi et samedi, ou null), min_stay (ou null), currency.
{
"data": [
{
"id": "c0ffee00-1234-4abc-9def-001122334455",
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "Haute saison",
"date_from": "2026-12-15",
"date_to": "2027-01-05",
"price": 420,
"weekend_price": 480,
"min_stay": 5,
"currency": "EUR",
"created_at": "2026-09-01T10:00:00.000Z",
"updated_at": "2026-09-01T10:00:00.000Z"
}
],
"next_cursor": null
}Propriétaires
GET/owners
GET/owners/{id}
Les propriétaires, un par portail propriétaire créé dans l'app. Paramètres : updated_since, limit, cursor.
Champs : name, email, property_ids (logements du propriétaire), portal_active (le portail en ligne est ouvert). Le lien secret du portail n'est jamais renvoyé.
{
"data": [
{
"id": "7e57ab1e-0000-4000-8000-00000000a001",
"name": "M. et Mme Martin",
"email": "martin@example.com",
"property_ids": ["5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b"],
"portal_active": true,
"created_at": "2026-05-10T09:00:00.000Z",
"updated_at": "2026-05-10T09:00:00.000Z"
}
],
"next_cursor": null
}Relevé propriétaire calculé
GET/owner-statements
Le relevé d'un propriétaire pour un mois, calculé au moment de l'appel.
| Paramètre | Description |
|---|---|
owner_id | Obligatoire : id d'un propriétaire (/owners) |
year | Obligatoire : 2000 à 2100 |
month | Obligatoire : 1 à 12 |
C'est le même calcul que le bilan propriétaire de l'app (écran Bilans propriétaires) :
- réservations confirmées des logements du propriétaire dont l'arrivée a lieu dans le mois (ni annulation, ni période bloquée). Un séjour à cheval sur deux mois compte en entier le mois de l'arrivée ;
- revenus : montant de ces séjours ;
- commission : montant de chaque séjour multiplié par le taux de commission de son logement (
commission_rate, 20 % s'il est vide), arrondie au centime par séjour ; - dépenses : tous les ménages du mois sur ces logements, sauf les ménages annulés ou refusés, au montant payé, sinon au tarif de l'agent. Comme dans l'app, les autres dépenses (
/expenses) ne sont pas déduites du relevé ; - net à reverser : revenus, moins dépenses, moins commission ;
- occupation : nuits occupées dans le mois (y compris les séjours arrivés le mois précédent), divisées par le nombre de jours du mois multiplié par le nombre de logements, en %.
Seule différence avec l'app : l'app permet de modifier le taux d'un logement au moment de générer un bilan ; l'API applique toujours le taux enregistré sur le logement.
curl "https://www.myhostkit.com/api/v1/owner-statements?owner_id=7e57ab1e-0000-4000-8000-00000000a001&year=2026&month=9" \
-H "Authorization: Bearer $MHK_KEY"{
"data": {
"owner_id": "7e57ab1e-0000-4000-8000-00000000a001",
"owner_name": "M. et Mme Martin",
"year": 2026,
"month": 9,
"currency": "EUR",
"period_start": "2026-09-01",
"period_end": "2026-09-30",
"properties": [
{
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "Villa Azur",
"commission_rate": 20,
"reservations_count": 2,
"nights": 7,
"revenue": 599.99,
"commission": 120,
"expenses": 40,
"cleanings_count": 1,
"net": 439.99
}
],
"totals": {
"reservations_count": 2,
"nights": 7,
"revenue": 599.99,
"commission": 120,
"expenses": 40,
"cleanings_count": 1,
"net": 439.99,
"occupation_rate": 17
}
}
}Bilans propriétaires déjà générés
GET/owner-reports
GET/owner-reports/{id}
Les bilans propriétaires générés dans l'app, tels qu'ils ont été figés à leur génération. Le document lui-même n'est pas renvoyé.
Paramètres : owner_id, year, month, updated_since (porte sur la date de génération), limit, cursor.
Champs : owner_id (propriétaire rattaché, ou null pour un ancien bilan), owner_name, owner_email, year, month, revenue, commission, expenses (ménages déduits), net, reservations_count, occupation_rate, currency, sent_at (envoi par e-mail au propriétaire), created_at, updated_at (date d'envoi, sinon de génération).
{
"data": [
{
"id": "0e0e0e0e-3333-4444-8555-666677778888",
"owner_id": "7e57ab1e-0000-4000-8000-00000000a001",
"owner_name": "M. et Mme Martin",
"owner_email": "martin@example.com",
"year": 2026,
"month": 9,
"revenue": 599.99,
"commission": 120,
"expenses": 40,
"net": 439.99,
"reservations_count": 2,
"occupation_rate": 17,
"currency": "EUR",
"sent_at": "2026-10-01T07:30:00.000Z",
"created_at": "2026-10-01T07:29:12.000Z",
"updated_at": "2026-10-01T07:30:00.000Z"
}
],
"next_cursor": null
}Dépenses
GET/expenses
GET/expenses/{id}
Les dépenses du compte. Certaines sont créées automatiquement par MyHostKit (auto_generated: true) : commission des plateformes, taxe de séjour estimée, ménage payé.
| Paramètre | Description |
|---|---|
property_id | Un logement |
from, to | Dépenses datées dans cette période (incluse) |
category | Une des 9 catégories ci-dessous. Une autre valeur répond 400 validation_error avec field: "category" |
updated_since, limit, cursor | Voir Pagination |
Catégories, liste fermée (la même en lecture et en écriture) :
category | Dépense |
|---|---|
cleaning | Ménage |
platform_fee | Commission d'une plateforme |
myhostkit_fee | Frais MyHostKit |
tourist_tax | Taxe de séjour |
maintenance | Entretien, réparation |
supplies | Fournitures, consommables |
utilities | Eau, électricité, internet |
insurance | Assurance |
other | Autre |
Champs : property_id, booking_id, cleaning_id, category, amount, currency, description, date, auto_generated.
{
"data": [
{
"id": "e4e4e4e4-1111-4aaa-8bbb-cccccccccccc",
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"booking_id": null,
"cleaning_id": null,
"category": "maintenance",
"amount": 85,
"currency": "EUR",
"description": "Remplacement du mitigeur",
"date": "2026-09-18",
"auto_generated": false,
"created_at": "2026-09-18T12:00:00.000Z",
"updated_at": "2026-09-18T12:00:00.000Z"
}
],
"next_cursor": null
}Missions de ménage
GET/cleanings
GET/cleanings/{id}
| Paramètre | Description |
|---|---|
property_id | Un logement |
from, to | Missions datées dans cette période (incluse) |
status | Un statut ou une liste : pending, confirmed, report_sent, validated, cancelled... |
updated_since, limit, cursor | Voir Pagination |
| Champ | Description |
|---|---|
property_id | Logement |
date, time | Jour et heure de la mission |
status | Statut de la mission |
cleaner | { name } : le nom de l'agent seulement, ou null si personne n'est encore affecté |
amount, currency | Montant de la mission : montant payé, sinon tarif de l'agent |
notes | Consignes de l'hôte à l'agent |
report_sent, report_sent_at, report_notes | Rapport de fin de ménage |
photos | Adresses des photos du rapport |
ai_score | Note de contrôle des photos par l'IA, sur 10 |
{
"data": {
"id": "c1c1c1c1-2222-4333-8444-555566667777",
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"date": "2026-10-14",
"time": "11:00",
"status": "validated",
"cleaner": { "name": "Awa Diallo" },
"amount": 45,
"currency": "EUR",
"notes": "Linge de lit dans le placard du couloir",
"report_sent": true,
"report_sent_at": "2026-10-14T13:02:11.000Z",
"report_notes": "RAS",
"photos": ["https://www.myhostkit.com/fichiers/cleaning-reports/c1c1c1c1-2222-4333-8444-555566667777/salon.jpg"],
"ai_score": 8.8,
"created_at": "2026-10-10T08:00:00.000Z",
"updated_at": "2026-10-14T15:30:00.000Z"
}
}Incidents
GET/incidents
GET/incidents/{id}
Les incidents signalés. Paramètres : property_id, status (new, seen, resolved), updated_since, limit, cursor.
Champs : property_id, guest_name, description, status, resolved, resolved_at.
{
"data": {
"id": "0d0d0d0d-5555-4666-8777-888899990000",
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"guest_name": "Marie Dupont",
"description": "La climatisation de la chambre 2 ne démarre plus.",
"status": "resolved",
"resolved": true,
"resolved_at": "2026-10-12T10:05:00.000Z",
"created_at": "2026-10-11T19:42:00.000Z",
"updated_at": "2026-10-12T10:05:00.000Z"
}
}Routes en écriture
Toutes ces routes demandent le droit write. Le corps est un objet JSON, envoyé avec Content-Type: application/json. Un champ inconnu répond 400 field_not_editable. La réponse renvoie l'objet complet, dans la même forme qu'en lecture ; une création répond 201.
Dépenses
POST/expenses
| Champ | Requis | Description |
|---|---|---|
property_id | oui | Un logement de votre compte |
category | oui | Une des 9 catégories : cleaning, platform_fee, myhostkit_fee, tourist_tax, maintenance, supplies, utilities, insurance, other (détail). Une autre valeur répond 400 validation_error avec field: "category" et la liste permise |
amount | oui | Nombre supérieur à 0, arrondi au centime |
date | non | AAAA-MM-JJ. Par défaut : aujourd'hui, heure de Paris |
description | non | 500 caractères au plus |
booking_id | non | Une réservation de votre compte, sur le même logement |
curl -X POST https://www.myhostkit.com/api/v1/expenses \
-H "Authorization: Bearer $MHK_KEY" -H "Content-Type: application/json" \
-d '{"property_id":"5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b","category":"maintenance","amount":85,"date":"2026-09-18","description":"Remplacement du mitigeur"}'PATCH/expenses/{id}
Les mêmes champs, tous facultatifs. booking_id: null détache la réservation.
DELETE/expenses/{id}
{ "data": { "id": "e4e4e4e4-1111-4aaa-8bbb-cccccccccccc", "deleted": true } }Règles tarifaires
POST/rates
Les contrôles sont les mêmes que dans l'écran Tarifs saisonniers de l'app.
| Champ | Requis | Description |
|---|---|---|
property_id | oui | Un logement de votre compte |
name | oui | 100 caractères au plus |
date_from, date_to | oui | AAAA-MM-JJ. date_to égale ou postérieure à date_from, 3 ans au plus |
price | oui | Supérieur à 0 |
weekend_price | non | Prix des vendredis et samedis : supérieur à 0, ou null |
min_stay | non | 1 à 60 nuits, ou null |
PATCH/rates/{id}
Les mêmes champs sauf property_id (une règle ne change pas de logement), tous facultatifs.
DELETE/rates/{id}
Répond { "data": { "id": "...", "deleted": true }, "pushed": ..., "push_reason": ... }. Après une suppression, les nuits reviennent au prix d'une autre règle ou au prix de base.
Envoi aux plateformes
Si le logement est relié au channel manager et que son calendrier a été initialisé, les nuits concernées (ancienne et nouvelle période d'une règle modifiée, à partir d'aujourd'hui et sur 500 jours) partent aussitôt vers Airbnb, Booking.com et les autres plateformes reliées, avec le prix résolu nuit par nuit comme dans les disponibilités.
- Séjour minimum. Il n'est envoyé que pour les nuits où une règle en fixe un (
min_stayde la règle la plus récente qui couvre la nuit). Sur les autres nuits, rien n'est envoyé : le séjour minimum déjà posé sur les plateformes, par exemple depuis l'écran Tarifs et dispo de l'app, est conservé. Une exception : sur les nuits où une règle fixait un séjour minimum avant l'écriture et où aucune n'en fixe plus (règle supprimée ou modifiée, ou règle plus récente sans séjour minimum qui la remplace), l'envoi remet 1 nuit, comme dans les disponibilités, pour ne pas laisser l'ancien minimum en place. - Modification sans effet sur les prix. Un
PATCHqui ne porte niprice, niweekend_price, nimin_stay, nidate_from, nidate_to(le nom seul) n'envoie rien :pushed: false,push_reason: "rien_a_pousser". Renvoyer le même prix relance l'envoi, ce qui sert après uncanal_occupeou uncanal_erreur.
La réponse l'indique à côté de data :
| pushed | push_reason | Signification |
|---|---|---|
true | null | Envoyé aux plateformes |
false | logement_non_relie | Logement non relié au channel manager : rien à envoyer |
false | calendrier_non_initialise | Calendrier pas encore initialisé : la règle partira à l'initialisation |
false | rien_a_pousser | Période entièrement passée, nuits sans prix connu, ou modification du nom seul |
false | canal_occupe | Le channel manager limite les envois : refaites la modification plus tard |
false | canal_erreur | L'envoi a échoué : la règle est enregistrée, refaites la modification plus tard |
Dans tous les cas, la règle est enregistrée.
curl -X POST https://www.myhostkit.com/api/v1/rates \
-H "Authorization: Bearer $MHK_KEY" -H "Content-Type: application/json" \
-d '{"property_id":"5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b","name":"Haute saison","date_from":"2026-12-15","date_to":"2027-01-05","price":420,"weekend_price":480,"min_stay":5}'{
"data": {
"id": "c0ffee00-1234-4abc-9def-001122334455",
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "Haute saison",
"date_from": "2026-12-15",
"date_to": "2027-01-05",
"price": 420,
"weekend_price": 480,
"min_stay": 5,
"currency": "EUR",
"created_at": "2026-10-02T09:00:00.000Z",
"updated_at": "2026-10-02T09:00:00.000Z"
},
"pushed": true,
"push_reason": null
}Incidents
POST/incidents
| Champ | Requis | Description |
|---|---|---|
property_id | oui | Un logement de votre compte |
description | oui | 5 000 caractères au plus |
guest_name | non | 200 caractères au plus |
status | non | new (par défaut), seen ou resolved |
PATCH/incidents/{id}
Champs : status (new, seen, resolved), ou resolved: true pour résoudre (resolved: false le rouvre en new), description, guest_name. resolved_at est posé automatiquement.
curl -X PATCH https://www.myhostkit.com/api/v1/incidents/0d0d0d0d-5555-4666-8777-888899990000 \
-H "Authorization: Bearer $MHK_KEY" -H "Content-Type: application/json" \
-d '{"resolved": true}'Missions de ménage
PATCH/cleanings/{id}
Seul notes est modifiable : les consignes à l'agent, 2 000 caractères au plus, ou null.
curl -X PATCH https://www.myhostkit.com/api/v1/cleanings/c1c1c1c1-2222-4333-8444-555566667777 \
-H "Authorization: Bearer $MHK_KEY" -H "Content-Type: application/json" \
-d '{"notes": "Linge de lit dans le placard du couloir"}'Webhooks
Les webhooks préviennent votre serveur quand un objet change, en général dans la minute qui suit, sans qu'il ait à interroger l'API.
Ils ne fonctionnent que tant que le compte a droit à l'API : accès à MyHostKit actif, compte MyHostKit direct. Pendant que l'accès est inactif (ou après un passage sous une marque partenaire), aucun événement n'est enregistré, et les livraisons encore en attente passent en échec avec le motif acces_inactif ou api_non_incluse. Ces événements ne sont pas rejoués quand l'accès revient : refaites alors une synchronisation complète. Même sans accès, le titulaire du compte peut toujours, dans l'app, voir ses clés et ses points de terminaison, révoquer une clé, désactiver ou supprimer un point de terminaison.
Mise en place
Dans l'app, au même endroit que les clés (Réglages, puis API et webhooks), ajoutez un point de terminaison : une adresse https:// de votre serveur et les événements voulus. Le secret de signature (whsec_...) s'affiche une seule fois : gardez-le côté serveur. Le bouton « Tester » envoie un événement ping et montre la réponse de votre serveur.
Adresses acceptées : https:// uniquement, ports 443 ou 8443, nom de domaine public. Sont refusés : localhost, les noms sans domaine ou en .local et .internal, les adresses IP privées, locales ou réservées, et les noms de domaine qui pointent vers elles. Le contrôle est refait à chaque envoi. Les redirections ne sont pas suivies.
Événements
| Événement | Quand |
|---|---|
booking.created | Nouvelle réservation ou période bloquée |
booking.updated | Réservation modifiée |
booking.cancelled | Réservation passée au statut annulé, ou supprimée (deleted: true) |
cleaning.created | Nouvelle mission de ménage |
cleaning.updated | Mission modifiée (affectation, statut, rapport...), ou supprimée (deleted: true) |
incident.created | Nouvel incident |
incident.updated | Incident modifié ou résolu, ou supprimé (deleted: true) |
expense.created, expense.updated, expense.deleted | Dépense créée, modifiée, supprimée |
rate.created, rate.updated, rate.deleted | Règle tarifaire créée, modifiée, supprimée |
ping | Test depuis l'app |
Les événements sont émis quelle que soit l'origine du changement : l'app, l'API, une plateforme reliée, un agent de ménage. Une écriture qui ne change rien de visible n'en émet pas.
Requête envoyée
Un POST vers votre adresse, avec ce corps JSON :
{
"id": "4c1f0a2b-9d8e-4f7a-b6c5-d4e3f2a1b0c9",
"type": "booking.created",
"created_at": "2026-10-02T08:15:00.000Z",
"deleted": false,
"data": {
"id": "a1b2c3d4-1111-4222-8333-444455556666",
"property_id": "5f0e8c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "confirmed",
"platform": "airbnb",
"source": "channel_manager",
"check_in": "2026-10-10",
"check_out": "2026-10-14",
"nights": 4,
"amount": 812.4,
"currency": "EUR",
"guests_count": 3,
"payment_status": null,
"paid_at": null,
"deposit": null,
"guest": { "name": "Marie Dupont", "email": "marie.dupont@example.com", "guests_count": 3 },
"created_at": "2026-10-02T08:15:00.000Z",
"updated_at": "2026-10-02T08:15:00.000Z"
}
}id: identifiant de l'événement, le même à chaque nouvel essai.data: l'objet, exactement dans la forme renvoyée par l'API (/bookings,/cleanings,/incidents,/expenses,/rates), tel qu'il était au moment du changement. Pour unping,datacontient un court message de test.deleted: true: l'objet a été supprimé ;dataest son dernier état.
| En-tête | Contenu |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | MyHostKit-Webhooks/1.0 |
X-MyHostKit-Event | Type d'événement (booking.created...) |
X-MyHostKit-Delivery | Identifiant de cette livraison |
X-MyHostKit-Signature | t=<horodatage unix>,v1=<signature> |
Réponse attendue et nouveaux essais
- Répondez un statut 2xx dans les 10 secondes. Faites le traitement long après avoir répondu, dans une file d'attente.
- Tout autre résultat (statut 3xx, 4xx ou 5xx, délai dépassé, erreur réseau) est un échec. Nouvel essai après 1 min, 5 min, 30 min, 2 h, puis 12 h. Après ce sixième essai, la livraison est en échec définitif.
- Après 20 échecs définitifs consécutifs, le point de terminaison est désactivé et le motif est visible dans l'app. Un succès remet le compteur à zéro. Réactivez-le dans l'app une fois votre serveur réparé.
- Un point de terminaison désactivé ne reçoit plus rien, et rien n'est gardé pour lui. Tant qu'il est désactivé (après 20 échecs, ou à la main dans l'app), aucun événement n'est enregistré pour lui, et ses livraisons encore en attente passent en échec. La réactivation ne rejoue rien, et une livraison en échec définitif n'est pas renvoyée non plus. Après une réactivation ou un échec définitif, refaites une synchronisation complète : relisez toutes les pages de chaque liste, sans
updated_since, puis effacez de votre côté ce qui n'y figure plus. C'est la seule façon de retrouver les suppressions manquées (expense.deleted,rate.deleted,deleted: true). - Les envois partent par lots, chaque minute : 100 livraisons au plus par passage, en tour de rôle entre les comptes. Un gros volume (par exemple l'import de centaines de réservations) peut retarder les livraisons de plusieurs minutes.
- Le même événement peut arriver plus d'une fois, et l'ordre d'arrivée n'est pas garanti. Dédoublonnez par
idd'événement, et comparezdata.updated_atavec ce que vous avez déjà. - Les événements et l'historique des livraisons sont gardés 30 jours. Les 50 dernières livraisons de chaque point de terminaison (statut, code HTTP reçu, erreur) sont consultables dans l'app. L'erreur est un code :
http_NNN(statut reçu, par exemplehttp_500),delai_depasse(pas de réponse en 10 s),echec_connexion(toute erreur réseau ou TLS),adresse_refusee(l'adresse ne passe plus le contrôle ci-dessus), ouacces_inactif/api_non_incluse/owner_only(le compte n'a plus droit à l'API : la livraison n'est pas partie), ou « point de terminaison désactivé » (la livraison n'est pas partie).
Vérifier la signature
v1 est le HMAC-SHA256, en hexadécimal, du texte <t>.<corps brut> calculé avec votre secret. Calculez-le sur le corps brut, avant tout décodage JSON, et refusez une requête de plus de 5 minutes.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
// rawBody : corps brut (chaîne) ; header : en-tête X-MyHostKit-Signature ; secret : whsec_...
export function verifierWebhook(rawBody, header, secret, nowSec = Math.floor(Date.now() / 1000)) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(nowSec - t) > 300) return false; // plus de 5 minutes : refusé
const attendu = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(attendu, "hex");
const b = Buffer.from(String(parts.v1 || ""), "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// Lire le corps brut, avant tout décodage JSON.
app.post("/webhooks/myhostkit", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
if (!verifierWebhook(rawBody, req.get("X-MyHostKit-Signature") || "", process.env.MHK_WEBHOOK_SECRET)) {
return res.status(401).end();
}
const evt = JSON.parse(rawBody);
res.status(200).end(); // répondre tout de suite...
// ...puis traiter evt dans votre file d'attente, en dédoublonnant par evt.id
});
app.listen(3000);Sur Vercel ou avec Next.js (App Router), lisez le corps brut avec await request.text(), puis appelez la même fonction verifierWebhook.
Supabase Edge Functions, Deno, Cloudflare Workers
async function verifierWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const cle = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", cle, new TextEncoder().encode(`${t}.${rawBody}`));
const attendu = [...new Uint8Array(sig)].map((o) => o.toString(16).padStart(2, "0")).join("");
const recu = String(parts.v1 || "");
if (recu.length !== attendu.length) return false;
let diff = 0; // comparaison en temps constant
for (let i = 0; i < attendu.length; i++) diff |= attendu.charCodeAt(i) ^ recu.charCodeAt(i);
return diff === 0;
}
Deno.serve(async (req) => {
const secret = Deno.env.get("MHK_WEBHOOK_SECRET");
if (!secret) return new Response("secret manquant", { status: 500 });
const rawBody = await req.text();
const ok = await verifierWebhook(rawBody, req.headers.get("X-MyHostKit-Signature") ?? "", secret);
if (!ok) return new Response("signature invalide", { status: 401 });
const evt = JSON.parse(rawBody);
// enregistrer evt.id (dédoublonnage), puis traiter evt.type et evt.data
return new Response("ok");
});Une Edge Function Supabase qui reçoit nos webhooks se déploie avec --no-verify-jwt : nos requêtes ne portent pas de jeton Supabase, la signature en tient lieu. Le secret se range avec supabase secrets set MHK_WEBHOOK_SECRET=whsec_....
supabase secrets set MHK_WEBHOOK_SECRET=whsec_VOTRE_SECRET
supabase functions deploy myhostkit-webhook --no-verify-jwtContact
Une question, un champ qui vous manque, besoin d'aide pour brancher votre CRM ? Écrivez à contact@myhostkit.com. C'est le développeur de MyHostKit qui répond. Joignez à votre message le code d'erreur et l'heure de l'appel, jamais votre clé.
Pas encore client ?
L'API est disponible avec un compte MyHostKit actif. Premier mois offert, sans engagement.