Référence d'intégration · v1

Bloomy Stock API

Synchronisez automatiquement le stock d'une boutique fleuriste depuis votre ERP, votre caisse ou votre logiciel de gestion.

Base URL
https://app.bloomy.one/api/v1
REST · JSON Clé API par boutique 738 variantes

Principes

Quatre règles gouvernent toute l'API. Les comprendre évite 90 % des surprises d'intégration.

Le système externe est la source de vérité du stock

Vous poussez des quantités absolues, jamais des deltas. Bloomy applique la valeur reçue telle quelle.

Le catalogue Bloomy est en lecture seule

Aucun endpoint ne crée, ne renomme ni ne supprime une variante du catalogue global. Une intégration ne peut pas polluer le catalogue partagé par tous les fleuristes.

Une clé = une boutique

Une clé API est liée à un floristId unique. Elle ne peut ni lire ni écrire le stock d'une autre boutique.

Tout est idempotent

PUT /stock/{variantId} avec stock: 12 rejoué dix fois donne le même résultat. Aucun identifiant d'idempotence à gérer.

Ce que l'API ne permettra jamais

  • Créer, modifier ou supprimer une entrée du catalogue global.
  • Modifier le prix, le nom, la photo ou le statut d'un article — seul le fleuriste le fait depuis l'app Bloomy.
  • Accéder aux commandes, aux clients, ou aux données d'une autre boutique.

Les écritures se limitent strictement à la quantité et à son indicateur de disponibilité, sur les articles de la boutique liée à la clé.

Authentification

Chaque requête porte une clé API de boutique.

Authorization: Bearer blm_sk_f20976162e1e4edd59c9_Xj4mPq…

L'en-tête X-Api-Key: blm_sk_… est également accepté pour les ERP qui ne savent pas envoyer d'en-tête Authorization.

Format de la clé

blm_sk_<keyId 20 hex>_<secret 43 caractères base64url>

Seul le SHA-256 de la clé est stocké côté Bloomy. Une clé perdue ne peut pas être retrouvée : il faut la révoquer et en émettre une nouvelle.

Cycle de vie d'une clé

C'est le fleuriste qui crée sa clé lui-même, depuis l'application Bloomy : Réglages boutique → Synchronisation externe → Gérer mes clés API. Aucune intervention de Bloomy n'est nécessaire. Un admin Bloomy peut le faire à sa place en cas de besoin.

Créer

Le fleuriste nomme la clé — le nom de votre logiciel, par exemple — et le secret s'affiche une seule fois. Il vous le transmet ; personne, pas même Bloomy, ne peut le relire ensuite.

Renouveler

Une clé ne se « modifie » pas : elle se renouvelle. Bloomy génère une nouvelle clé et laisse l'ancienne fonctionner pendant une période de grâce — 1 h ou 24 h au choix — le temps que vous installiez la nouvelle. La synchronisation ne s'interrompt jamais.

Révoquer

Coupe l'accès immédiatement et définitivement. À utiliser si la clé a fuité. Le renouvellement avec « couper tout de suite » produit le même effet tout en fournissant la clé de remplacement.

Clé perdue ?

Seul le hash est stocké : il n'existe aucun moyen de retrouver un secret égaré. Le fleuriste renouvelle la clé depuis l'app et vous transmet la nouvelle. Prévoyez donc, côté logiciel, un champ « clé API » modifiable sans redéploiement.

Callables (côté application)

CallableRôle
createStockApiKeyV2Émet une clé. Retourne le secret une seule fois.
listStockApiKeysV2Liste les clés : préfixe, label, scopes, statut, dernière utilisation, nombre d'appels. Jamais le secret.
rotateStockApiKeyV2Renouvelle une clé. graceMinutes — 1440 par défaut, 0 pour couper l'ancienne immédiatement, 30 jours maximum.
revokeStockApiKeyV2Révoque une clé immédiatement et définitivement.

Un fleuriste ne peut agir que sur sa propre boutique ; un admin peut passer un floristId pour agir au nom d'une boutique.

Statuts d'une clé

StatutRequêtes acceptées ?Réponse en cas de refus
activeOui—
rotatingOui, jusqu'à expiresAt403 key_expired une fois la période de grâce écoulée
revokedNon403 key_revoked

Scopes

ScopeAutorise
catalog:readGET /catalog/variants, GET /catalog/variants/{id}
stock:readGET /stock, GET /stock/{variantId}
stock:writePUT /stock/{variantId}, POST /stock/bulk

Une clé sans le scope requis reçoit 403 insufficient_scope. Pour un ERP qui ne fait que pousser du stock, catalog:read + stock:write suffit.

Sécurité

  • La clé est un secret serveur. Ne jamais l'embarquer dans une application mobile, un front web ou un dépôt public.
  • Transport HTTPS uniquement.
  • Révoquez immédiatement une clé suspectée d'être compromise ; les syncs reprennent dès qu'une nouvelle clé est configurée.

Le modèle de données : variantId

Le catalogue Bloomy est constitué d'articles — « Rose » — déclinés en variantes de couleur — « Rose Rouge », « Rose Blanche ». Au 16 août 2026 : 116 articles catalogue, 738 variantes adressables.

CasFormatExemple
Variante de couleur{svgKey}_{colorValue}rose_4294198070
Article de base, sans couleur{svgKey}eucalyptus
Variante déclarée sans couleur{svgKey}_{nomVariante}ruban_Kraft

colorValue est la couleur ARGB 32 bits non signée telle que stockée par Bloomy — 4294198070 vaut #F44336. Le champ colorHex est fourni en plus, pour lisibilité.

À retenir

C'est cet identifiant, et lui seul, que votre logiciel doit stocker en face de ses propres références. Il est stable dans le temps et partagé avec le moteur de reconnaissance visuelle de Bloomy.

Procédure de mapping

Les six étapes, dans l'ordre, d'un premier raccordement jusqu'à la synchronisation continue.

  1. Récupérer le catalogue. GET /catalog/variants en paginant jusqu'à pagination.hasMore == false. Stockez variantId, name, type, colorHex, imageUrl.
  2. Associer vos références. Ajoutez un champ « ID Bloomy » sur chaque article de votre logiciel. GET /catalog/variants?q=rose+rouge aide à retrouver une variante par nom — recherche insensible à la casse et aux accents.
  3. Vérifier le périmètre réel. GET /stock retourne ce que la boutique vend effectivement : items[] sont pilotables tout de suite ; unmappableItems[] sont les articles maison du fleuriste, sans équivalent catalogue, non pilotables ; une variante absente des deux signifie que le fleuriste ne la vend pas encore.
  4. Premier envoi. POST /stock/bulk avec l'ensemble des variantes mappées.
  5. Synchronisation continue. Rejouez POST /stock/bulk à chaque changement de stock, ou à intervalle régulier — toutes les 15 minutes par exemple.
  6. Rafraîchir le catalogue. Comparez catalogVersion, présent dans toutes les réponses catalogue, à celui de votre dernier import. S'il a changé, réimportez. Une variante ajoutée par Bloomy apparaît sous 5 minutes.

Vérifier que ça fonctionne

Dès qu'une clé API est active, l'inventaire Bloomy bascule automatiquement en suivi à la tige : le fleuriste voit la quantité exacte de chaque variante, au lieu du bouton En stock / Épuisé ou des niveaux Faible / Élevé. C'est délibéré, et c'est votre meilleur outil de test.

  1. Le fleuriste ouvre Inventaire dans l'application Bloomy.
  2. Une bannière indique combien d'articles sont synchronisés et à quand remonte la dernière mise à jour.
  3. Vous lancez une synchronisation depuis votre logiciel.
  4. Les nombres et l'horodatage changent sous ses yeux, en direct.

Chaque article piloté par l'API porte un badge API ; une info-bulle donne la date de sa dernière synchronisation. Un badge grisé signale un article que votre système n'a jamais alimenté — utile pour repérer un mapping incomplet.

Pourquoi le mode simple disparaît

Le bouton En stock / Épuisé écrit la valeur du quota — 50 par défaut — ce qui écraserait silencieusement la quantité que vous venez d'envoyer. Les deux modèles sont incompatibles : quand une API pilote le stock, elle est seule maître des quantités.

Techniquement, Bloomy écrit stockApiEnabled: true sur boutique/info dès qu'une clé est utilisable, et repasse à false quand la dernière est révoquée.

Endpoints

Toutes les réponses sont en JSON UTF-8 et contiennent un requestId, également renvoyé dans l'en-tête X-Request-Id. Communiquez-le au support Bloomy en cas de problème.

GET /ping aucun scope requis

Vérifie la clé et la boutique associée.

Requête

curl -H "Authorization: Bearer $BLOOMY_KEY" \
  https://app.bloomy.one/api/v1/ping

Réponse

{
  "ok": true,
  "apiVersion": "1.0.0",
  "floristId": "8xKq2mPz",
  "scopes": ["catalog:read", "stock:read", "stock:write"],
  "keyLabel": "Pixelfleurs Prod",
  "serverTime": "2026-08-16T07:39:57.630Z"
}
GET /catalog/variants catalog:read

Liste et recherche les variantes du catalogue global.

ParamètreTypeDéfautDescription
qstring—Recherche plein texte sur le nom, insensible à la casse et aux accents. Tous les mots doivent correspondre.
typeenum—flower, foliage, ornament, extra.
colorint—Filtre exact sur colorValue.
limitint50Maximum 200.
offsetint0Pagination.

Requête

curl -H "Authorization: Bearer $BLOOMY_KEY" \
  "https://app.bloomy.one/api/v1/catalog/variants?q=rose&limit=2"

Réponse

{
  "variants": [
    {
      "variantId": "rose",
      "catalogItemId": "rose",
      "svgKey": "rose",
      "name": "Rose",
      "itemName": "Rose",
      "variantName": null,
      "type": "flower",
      "category": "Fleurs",
      "colorValue": null,
      "colorHex": null,
      "imageUrl": "https://firebasestorage.googleapis.com/.../rose.png",
      "isBaseVariant": true
    },
    {
      "variantId": "rose_4294198070",
      "catalogItemId": "rose",
      "svgKey": "rose",
      "name": "Rose Rouge",
      "itemName": "Rose",
      "variantName": "Rouge",
      "type": "flower",
      "category": "Fleurs",
      "colorValue": 4294198070,
      "colorHex": "#F44336",
      "imageUrl": "https://firebasestorage.googleapis.com/.../rose_rouge.png",
      "isBaseVariant": false
    }
  ],
  "pagination": { "total": 3, "offset": 0, "limit": 2, "hasMore": true, "nextOffset": 2 },
  "catalogVersion": "62d4a92dc829bd90",
  "catalogWarnings": [],
  "requestId": "0695abdfa06f1c03"
}

L'en-tête ETag reprend catalogVersion : un client qui gère les ETags évite de retélécharger un catalogue inchangé.

catalogWarnings signale les variantes qui partagent leur identifiant avec une autre — deux déclinaisons enregistrées avec la même couleur — et ne sont donc pas adressables individuellement. Purement informatif : c'est une anomalie de saisie côté Bloomy, rien à faire de votre côté.

GET /catalog/variants/{variantId} catalog:read

Détail d'une variante.

{
  "variant": {
    "variantId": "rose_4294198070",
    "catalogItemId": "rose",
    "svgKey": "rose",
    "name": "Rose Rouge",
    "itemName": "Rose",
    "variantName": "Rouge",
    "type": "flower",
    "category": "Fleurs",
    "colorValue": 4294198070,
    "colorHex": "#F44336",
    "imageUrl": "https://firebasestorage.googleapis.com/.../rose_rouge.png",
    "isBaseVariant": false
  },
  "catalogVersion": "62d4a92dc829bd90",
  "requestId": "1e74037816cadf0a"
}
GET /stock stock:read

Stock courant de la boutique. Paramètre optionnel mapped=true pour n'obtenir que les articles rattachés au catalogue.

{
  "floristId": "8xKq2mPz",
  "items": [
    {
      "variantId": "eucalyptus",
      "itemId": "Mk3pL9",
      "name": "Eucalyptus",
      "type": "foliage",
      "colorValue": null,
      "stock": 0,
      "price": 2,
      "available": false,
      "inCatalog": true,
      "lastSyncedAt": null,
      "lastSyncSource": null,
      "externalRef": null,
      "consumedByBloomySinceSync": null
    },
    {
      "variantId": "rose_4294198070",
      "itemId": "Xy7bQ2",
      "name": "Rose Rouge",
      "type": "flower",
      "colorValue": 4294198070,
      "stock": 24,
      "price": 3.5,
      "available": true,
      "inCatalog": true,
      "lastSyncedAt": null,
      "lastSyncSource": null,
      "externalRef": null,
      "consumedByBloomySinceSync": null
    }
  ],
  "unmappableItems": [
    { "itemId": "Zz1cV4", "name": "Vase artisanal", "type": "extra", "stock": 4, "reason": "no_catalog_variant" }
  ],
  "duplicateVariants": [],
  "counts": { "total": 2, "mapped": 2, "available": 1 },
  "catalogVersion": "62d4a92dc829bd90",
  "requestId": "768aa73f32d6ed85"
}

Champs notables

ChampCe qu'il vous dit
availableL'article est réellement proposé aux clients. Vaut true seulement si stock > 0 et price > 0 et l'article est validé. Un stock à 40 avec available: false signifie presque toujours que le fleuriste n'a pas renseigné le prix.
consumedByBloomySinceSyncUnités décomptées par Bloomy depuis votre dernière écriture — commandes acceptées. Permet de réconcilier sans interroger les commandes. null tant qu'aucune sync n'a eu lieu.
externalRefLa référence que vous avez transmise lors de la dernière écriture.
duplicateVariantsPlusieurs articles de la boutique pointent vers la même variante — doublons créés dans l'app. Bloomy écrit sur un seul d'entre eux, de façon déterministe ; à nettoyer côté fleuriste.
GET /stock/{variantId} stock:read

Stock d'une seule variante — même structure qu'un élément de items[].

{
  "variantId": "rose_4294198070",
  "itemId": "Xy7bQ2",
  "name": "Rose Rouge",
  "type": "flower",
  "colorValue": 4294198070,
  "stock": 36,
  "price": 3.5,
  "available": true,
  "inCatalog": true,
  "lastSyncedAt": "2026-08-16T07:39:57.634Z",
  "lastSyncSource": "external_api",
  "externalRef": "SYNC-2026-08-16T07:00Z",
  "consumedByBloomySinceSync": 0,
  "requestId": "09de52b3d783592e"
}
PUT /stock/{variantId} stock:write

Fixe la quantité exacte d'une variante. PATCH est accepté comme alias.

ChampTypeDéfautDescription
stockintrequisQuantité absolue. Entier, de 0 à 100000.
referencestring—Votre référence interne, stockée et renvoyée dans les lectures. 120 caractères max.
createIfMissingboolfalseCrée l'article dans la boutique s'il n'y est pas encore.

Requête

curl -X PUT \
  -H "Authorization: Bearer $BLOOMY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"stock": 36, "reference": "PXF-ROSE-RED-60"}' \
  https://app.bloomy.one/api/v1/stock/rose_4294198070

Réponse

{
  "variantId": "rose_4294198070",
  "itemId": "Xy7bQ2",
  "stock": 36,
  "previousStock": 24,
  "created": false,
  "warnings": [],
  "syncedAt": "2026-08-16T07:39:57.633Z",
  "requestId": "adb694d716ad13f5"
}

createIfMissing

Si la variante existe au catalogue mais n'est pas encore dans la boutique, createIfMissing: true l'ajoute automatiquement. L'article est créé à partir du catalogue officiel uniquement — nom, type, photo, couleur : rien de votre payload autre que la quantité n'entre en base.

Le prix reste à zéro

L'article créé est invisible pour les clients jusqu'à ce que le fleuriste renseigne son prix dans l'app. La réponse le signale via warnings: ["item_created_without_price"]. C'est volontaire : une intégration ne doit jamais pouvoir mettre en vente un produit à un prix qu'elle aurait choisi.

POST /stock/bulk stock:write

L'endpoint à utiliser en production. PUT est accepté comme alias.

ChampTypeDéfautDescription
updatesarrayrequis1 à 500 entrées { variantId, stock }.
referencestring—Référence commune, par exemple l'identifiant du job de sync.
createIfMissingboolfalseIdentique au PUT, appliqué à toutes les entrées.
missingBehaviourenumignoreignore ou zero — voir ci-dessous.

Requête

curl -X POST \
  -H "Authorization: Bearer $BLOOMY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "reference": "SYNC-2026-08-16T07:00Z",
        "updates": [
          { "variantId": "rose_4294198070", "stock": 36 },
          { "variantId": "eucalyptus",      "stock": 12 },
          { "variantId": "rose_4294967295", "stock": 5  },
          { "variantId": "tulipe_999",      "stock": 3  }
        ]
      }' \
  https://app.bloomy.one/api/v1/stock/bulk

Réponse

{
  "floristId": "8xKq2mPz",
  "summary": { "received": 4, "updated": 2, "created": 0, "failed": 2, "zeroedByMissingBehaviour": 0 },
  "results": [
    { "variantId": "rose_4294198070", "itemId": "Xy7bQ2", "status": "updated", "stock": 36, "previousStock": 36 },
    { "variantId": "eucalyptus", "itemId": "Mk3pL9", "status": "updated", "stock": 12, "previousStock": 0 },
    {
      "variantId": "rose_4294967295",
      "status": "error",
      "code": "not_in_inventory",
      "message": "Not in this boutique's inventory. Retry with createIfMissing: true to add it."
    },
    {
      "variantId": "tulipe_999",
      "status": "error",
      "code": "variant_not_found",
      "message": "No catalog variant with id 'tulipe_999'."
    }
  ],
  "syncedAt": "2026-08-16T07:39:57.635Z",
  "requestId": "1baf2aacf4bcce63"
}

Sémantique partielle

Le lot n'est jamais rejeté en bloc pour une ligne invalide : les lignes valides sont appliquées, les autres rapportées une à une dans results[] avec un code. Le statut HTTP reste 200 — contrôlez toujours summary.failed, pas seulement le code HTTP. Statuts possibles par ligne : updated, created, error.

missingBehaviour

  • ignore (défaut) — les variantes absentes du payload ne sont pas touchées.
  • zero — toute variante de la boutique rattachée au catalogue et absente du payload est mise à 0. C'est le mode « inventaire complet » : votre système décrit l'intégralité du stock à chaque envoi.

Avertissement — mode zero

Un payload partiel — échec en amont dans votre ERP, filtre appliqué par erreur — vide la boutique. À n'activer que si votre export est garanti exhaustif. Les articles maison du fleuriste ne sont jamais affectés.

Erreurs

Format uniforme sur toute l'API.

{
  "error": {
    "code": "variant_not_found",
    "message": "No catalog variant with id 'tulipe_999'.",
    "details": { "variantId": "tulipe_999" }
  },
  "requestId": "1332686604f24974"
}
HTTPCodeSignification
400invalid_stockstock absent, non entier, négatif ou supérieur à 100000.
400invalid_payloadupdates absent ou vide.
400invalid_type · invalid_limitParamètre de requête invalide.
401missing_credentialsAucune clé fournie.
401invalid_credentialsClé mal formée, inconnue, ou secret erroné.
403key_revoked · key_expiredClé désactivée. Émettez-en une nouvelle.
403insufficient_scopeLa clé n'a pas le scope requis.
404variant_not_foundLe variantId n'existe pas au catalogue Bloomy.
404not_in_inventoryLa variante existe au catalogue mais pas dans cette boutique. Utilisez createIfMissing.
404route_not_foundChemin ou méthode inconnus.
409item_deletedL'article a été supprimé pendant l'écriture. Rejouez.
413payload_too_largePlus de 500 entrées dans updates.
429rate_limitedQuota dépassé. Respectez l'en-tête Retry-After.
500internal_errorErreur serveur. Rejouez avec backoff, puis contactez le support avec le requestId.

Stratégie de reprise

Rejouez sur 429, 500 et erreurs réseau avec un backoff exponentiel — 1s, 2s, 4s, 8s. Ne rejouez pas les autres 4xx : la requête est invalide, la rejouer ne changera rien.

Limites

LimiteValeur
Lectures240 requêtes / minute / clé
Écritures60 requêtes / minute / clé
Entrées par appel bulk500
Quantité maximale par variante100 000
Fraîcheur du cataloguejusqu'à 5 minutes de cache

Les quotas sont appliqués au mieux, par instance serveur. Le dépassement renvoie 429 avec un en-tête Retry-After en secondes.

Bonnes pratiques

Six réflexes qui font la différence entre une intégration qui tient et une qui dérive.

  • Un bulk vaut mieux que N PUT. Un seul appel pour 300 variantes plutôt que 300 appels : plus rapide, et hors quota d'écriture.
  • N'envoyez que les changements en régime nominal, et faites une resynchronisation complète — missingBehaviour: "zero" — une fois par jour ou après un incident.
  • Gérez la concurrence. Bloomy décrémente le stock quand un fleuriste accepte une commande. Si votre ERP calcule un stock à T0 et l'écrit à T0+5 min, une vente intervenue entre-temps est écrasée. Deux parades : synchroniser toutes les 5 à 15 minutes, ou lire consumedByBloomySinceSync avant d'écrire et le déduire de votre propre stock.
  • Renseignez reference avec un identifiant de job : il ressort dans GET /stock et dans les journaux Bloomy, ce qui accélère beaucoup le support.
  • Surveillez available. Un article à false malgré du stock indique un prix manquant côté fleuriste — c'est l'alerte la plus utile à remonter dans votre interface.
  • Ne codez pas en dur les variantId. Réimportez le catalogue quand catalogVersion change.

Exemple d'intégration

Node.js — import du catalogue puis synchronisation par lots.

const BASE = "https://app.bloomy.one/api/v1";
const KEY = process.env.BLOOMY_API_KEY;

async function bloomy(path, { method = "GET", body } = {}) {
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) {
    throw new Error(`${json.error.code}: ${json.error.message} (req ${json.requestId})`);
  }
  return json;
}

// 1. Import du catalogue — une fois, puis quand catalogVersion change
async function fetchCatalog() {
  const all = [];
  let offset = 0;
  for (;;) {
    const page = await bloomy(`/catalog/variants?limit=200&offset=${offset}`);
    all.push(...page.variants);
    if (!page.pagination.hasMore) return { variants: all, version: page.catalogVersion };
    offset = page.pagination.nextOffset;
  }
}

// 2. Synchronisation du stock — mapping = { refInterne: variantId }
async function syncStock(mapping, stockParRef) {
  const updates = Object.entries(mapping).map(([ref, variantId]) => ({
    variantId,
    stock: stockParRef[ref] ?? 0,
  }));

  // Découpage en lots de 500
  for (let i = 0; i < updates.length; i += 500) {
    const res = await bloomy("/stock/bulk", {
      method: "POST",
      body: {
        reference: `SYNC-${new Date().toISOString()}`,
        updates: updates.slice(i, i + 500),
      },
    });

    if (res.summary.failed > 0) {
      for (const r of res.results.filter((x) => x.status === "error")) {
        console.error(`[Bloomy] ${r.variantId} → ${r.code}: ${r.message}`);
      }
    }
    console.log(`[Bloomy] ${res.summary.updated} variantes mises à jour`);
  }
}

Support & versioning

  • Le numéro de version est renvoyé dans l'en-tête X-Bloomy-Api-Version et dans GET /ping.
  • Le chemin /api/v1 est stable : aucun changement cassant n'y sera introduit. Les ajouts de champs en réponse ne sont pas considérés comme cassants — votre parseur doit ignorer les champs inconnus.
  • Support : joignez le requestId de la requête concernée.