Développeurs · API v1

L'API MyHostKit, pour relier votre CRM

Version 1 · Mise à jour le 2 octobre 2026

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.

Adresse de basehttps://www.myhostkit.com/api/v1
FormatJSON, 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

  1. 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 : read pour lire et, si besoin, write pour é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.

  2. 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
      }
    }
  3. Synchronisez avec updated_since

    Lisez une première fois toutes les pages de chaque liste. Ensuite, ne demandez que ce qui a changé depuis votre dernier passage :

    bash
    curl "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 :

en-tête
Authorization: Bearer mhk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Droits. read ouvre les routes GET. write ouvre les routes POST, PATCH et DELETE. Une clé peut avoir l'un, l'autre ou les deux. Sans le bon droit, la réponse est 403 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

forme générale
// 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_at et updated_at.
  • Les horodatages sont en ISO 8601, en UTC : 2026-10-02T08:15:00.123Z. Les jours sont au format AAAA-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 (EUR par 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 id ni dates de création, puisqu'ils ne sont pas enregistrés.
  • updated_at vaut null pour /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ètreDescription
limitNombre d'éléments par page, de 1 à 200 (50 par défaut)
cursorValeur next_cursor de la page précédente, recopiée telle quelle
updated_sinceNe 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 :

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

  1. Première synchronisation : lisez toutes les pages de chaque liste.
  2. Retenez la plus grande valeur updated_at reçue, ou l'heure de début de la synchronisation moins une minute de marge.
  3. 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).

javascript · module ES, Node.js 18+, Vercel, Supabase Edge
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 passage

updated_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

réponse 400
{
  "error": {
    "code": "validation_error",
    "message": "date_to doit être égale ou postérieure à date_from.",
    "field": "date_to"
  }
}
  • code est stable : votre code peut s'y fier.
  • message est 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.
StatutcodeQuand
400validation_errorParamètre ou champ invalide (voir field)
400invalid_jsonCorps JSON illisible, ou qui n'est pas un objet
400invalid_cursorCurseur modifié ou illisible
400field_not_editableChamp inconnu ou non modifiable dans le corps
401missing_api_keyEn-tête Authorization absent
401invalid_api_keyClé inconnue ou mal formée
401revoked_api_keyClé révoquée
403insufficient_scopeLa clé n'a pas le droit read ou write demandé
403acces_inactifL'accès du compte à MyHostKit n'est pas actif
403api_non_incluseCompte ouvert sous une marque partenaire : pas d'API
403owner_onlyClé d'un agent de ménage ou d'un membre d'équipe
404not_foundObjet absent de votre compte, ou identifiant mal formé
404route_not_foundRoute inconnue
405method_not_allowedMéthode non acceptée sur cette route (voir l'en-tête Allow)
413payload_too_largeCorps de plus de 100 ko
422unknown_propertyLe property_id envoyé n'est pas un logement de votre compte
422unknown_bookingLe booking_id envoyé n'est pas une réservation de votre compte
422booking_property_mismatchLa réservation concerne un autre logement
422invalid_referenceRéférence refusée par la base
429rate_limitedPlus de 120 requêtes par minute, ou plus de 10 tests de webhook par minute depuis l'app (voir Retry-After)
500internal_errorErreur de notre côté : réessayez un peu plus tard

Limites

LimiteValeur
Requêtes120 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ésX-RateLimit-Limit: 120 et X-RateLimit-Remaining (requêtes restantes)
Taille d'une pagelimit de 1 à 200, 50 par défaut
Disponibilités90 jours par appel au plus
Corps d'une requête100 ko au plus
Clés actives par compte10
Points de terminaison webhook par compte10
Tests de webhook (bouton « Tester » de l'app)10 par minute et par compte
réponse 429
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éthodeRouteDroit
GET/meread
GET/properties, /properties/{id}read
GET/properties/{id}/availabilityread
GET/bookings, /bookings/{id}read
GET/movementsread
GET/rates, /rates/{id}read
GET/owners, /owners/{id}read
GET/owner-statementsread
GET/owner-reports, /owner-reports/{id}read
GET/expenses, /expenses/{id}read
GET/cleanings, /cleanings/{id}read
GET/incidents, /incidents/{id}read
POST/expenseswrite
PATCH DELETE/expenses/{id}write
POST/rateswrite
PATCH DELETE/rates/{id}write
POST/incidentswrite
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}

ChampDescription
name, address, city, typeTexte. type est saisi librement dans l'app (« Villa », « Appartement »...)
capacityNombre de voyageurs
bedroomsNombre de chambres
price_per_night, currencyPrix de base par nuit
check_in_time, check_out_timeHeures d'arrivée et de départ (« 15:00 »)
commission_rateTaux 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
statusactive...
platformsPlateformes reliées : calendriers iCal ajoutés (airbnb, booking, vrbo, abritel, other...) et annonce Airbnb importée
channel_managerBooléen : le logement est relié au channel manager (synchronisation Airbnb, Booking.com)
réponse 200
{
  "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ètreDescription
fromPremier jour (AAAA-MM-JJ). Par défaut : aujourd'hui, heure de Paris
toDernier 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 (null s'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).
bash
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"
réponse 200
{
  "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ètreDescription
property_idUn logement
from, toSéjours qui touchent cette période : départ le from ou après, arrivée le to ou avant
statusUn statut, ou une liste séparée par des virgules : confirmed,blocked
updated_since, limit, cursorVoir Pagination
ChampDescription
property_idLogement
statusconfirmed, cancelled, blocked...
platformairbnb, booking, vrbo, abritel, direct...
sourceOrigine de la saisie : channel_manager (channel manager), ical, manual, direct (réservation directe payée en ligne)...
check_in, check_out, nightsDates du séjour et nombre de nuits
amount, currencyMontant du séjour
guests_countNombre de voyageurs
payment_status, paid_atPaiement en ligne d'une réservation directe : paid..., sinon null
depositCaution : { status, amount, captured_amount, currency }, ou null sans caution
guest{ name, email, guests_count }
bash
curl "https://www.myhostkit.com/api/v1/bookings?from=2026-10-01&to=2026-10-31&status=confirmed" \
  -H "Authorization: Bearer $MHK_KEY"
réponse 200 · GET /bookings/{id}
{
  "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ètreDescription
dateLe 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.

réponse 200 · GET /movements?date=2026-10-14
{
  "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.

réponse 200
{
  "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é.

réponse 200
{
  "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ètreDescription
owner_idObligatoire : id d'un propriétaire (/owners)
yearObligatoire : 2000 à 2100
monthObligatoire : 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.

bash
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"
réponse 200
{
  "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).

réponse 200
{
  "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ètreDescription
property_idUn logement
from, toDépenses datées dans cette période (incluse)
categoryUne des 9 catégories ci-dessous. Une autre valeur répond 400 validation_error avec field: "category"
updated_since, limit, cursorVoir Pagination

Catégories, liste fermée (la même en lecture et en écriture) :

categoryDépense
cleaningMénage
platform_feeCommission d'une plateforme
myhostkit_feeFrais MyHostKit
tourist_taxTaxe de séjour
maintenanceEntretien, réparation
suppliesFournitures, consommables
utilitiesEau, électricité, internet
insuranceAssurance
otherAutre

Champs : property_id, booking_id, cleaning_id, category, amount, currency, description, date, auto_generated.

réponse 200
{
  "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ètreDescription
property_idUn logement
from, toMissions datées dans cette période (incluse)
statusUn statut ou une liste : pending, confirmed, report_sent, validated, cancelled...
updated_since, limit, cursorVoir Pagination
ChampDescription
property_idLogement
date, timeJour et heure de la mission
statusStatut de la mission
cleaner{ name } : le nom de l'agent seulement, ou null si personne n'est encore affecté
amount, currencyMontant de la mission : montant payé, sinon tarif de l'agent
notesConsignes de l'hôte à l'agent
report_sent, report_sent_at, report_notesRapport de fin de ménage
photosAdresses des photos du rapport
ai_scoreNote de contrôle des photos par l'IA, sur 10
réponse 200 · GET /cleanings/{id}
{
  "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.

réponse 200 · GET /incidents/{id}
{
  "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

ChampRequisDescription
property_idouiUn logement de votre compte
categoryouiUne 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
amountouiNombre supérieur à 0, arrondi au centime
datenonAAAA-MM-JJ. Par défaut : aujourd'hui, heure de Paris
descriptionnon500 caractères au plus
booking_idnonUne réservation de votre compte, sur le même logement
bash
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}

réponse 200
{ "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.

ChampRequisDescription
property_idouiUn logement de votre compte
nameoui100 caractères au plus
date_from, date_toouiAAAA-MM-JJ. date_to égale ou postérieure à date_from, 3 ans au plus
priceouiSupérieur à 0
weekend_pricenonPrix des vendredis et samedis : supérieur à 0, ou null
min_staynon1 à 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_stay de 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 PATCH qui ne porte ni price, ni weekend_price, ni min_stay, ni date_from, ni date_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 un canal_occupe ou un canal_erreur.

La réponse l'indique à côté de data :

pushedpush_reasonSignification
truenullEnvoyé aux plateformes
falselogement_non_relieLogement non relié au channel manager : rien à envoyer
falsecalendrier_non_initialiseCalendrier pas encore initialisé : la règle partira à l'initialisation
falserien_a_pousserPériode entièrement passée, nuits sans prix connu, ou modification du nom seul
falsecanal_occupeLe channel manager limite les envois : refaites la modification plus tard
falsecanal_erreurL'envoi a échoué : la règle est enregistrée, refaites la modification plus tard

Dans tous les cas, la règle est enregistrée.

bash
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}'
réponse 201
{
  "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

ChampRequisDescription
property_idouiUn logement de votre compte
descriptionoui5 000 caractères au plus
guest_namenon200 caractères au plus
statusnonnew (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.

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

bash
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énementQuand
booking.createdNouvelle réservation ou période bloquée
booking.updatedRéservation modifiée
booking.cancelledRéservation passée au statut annulé, ou supprimée (deleted: true)
cleaning.createdNouvelle mission de ménage
cleaning.updatedMission modifiée (affectation, statut, rapport...), ou supprimée (deleted: true)
incident.createdNouvel incident
incident.updatedIncident modifié ou résolu, ou supprimé (deleted: true)
expense.created, expense.updated, expense.deletedDépense créée, modifiée, supprimée
rate.created, rate.updated, rate.deletedRègle tarifaire créée, modifiée, supprimée
pingTest 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 :

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 un ping, data contient un court message de test.
  • deleted: true : l'objet a été supprimé ; data est son dernier état.
En-têteContenu
Content-Typeapplication/json; charset=utf-8
User-AgentMyHostKit-Webhooks/1.0
X-MyHostKit-EventType d'événement (booking.created...)
X-MyHostKit-DeliveryIdentifiant de cette livraison
X-MyHostKit-Signaturet=<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 id d'événement, et comparez data.updated_at avec 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 exemple http_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), ou acces_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)

javascript · Node.js
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

javascript · Web Crypto
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_....

bash
supabase secrets set MHK_WEBHOOK_SECRET=whsec_VOTRE_SECRET
supabase functions deploy myhostkit-webhook --no-verify-jwt

Contact

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.

Activer mon compte