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.
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.
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.
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)
| Callable | Rôle |
|---|---|
createStockApiKeyV2 | Émet une clé. Retourne le secret une seule fois. |
listStockApiKeysV2 | Liste les clés : préfixe, label, scopes, statut, dernière utilisation, nombre d'appels. Jamais le secret. |
rotateStockApiKeyV2 | Renouvelle une clé. graceMinutes — 1440 par défaut, 0 pour couper l'ancienne immédiatement, 30 jours maximum. |
revokeStockApiKeyV2 | Ré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é
| Statut | Requêtes acceptées ? | Réponse en cas de refus |
|---|---|---|
active | Oui | — |
rotating | Oui, jusqu'à expiresAt | 403 key_expired une fois la période de grâce écoulée |
revoked | Non | 403 key_revoked |
Scopes
| Scope | Autorise |
|---|---|
catalog:read | GET /catalog/variants, GET /catalog/variants/{id} |
stock:read | GET /stock, GET /stock/{variantId} |
stock:write | PUT /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.
| Cas | Format | Exemple |
|---|---|---|
| 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.
- Récupérer le catalogue.
GET /catalog/variantsen paginant jusqu'àpagination.hasMore == false. StockezvariantId,name,type,colorHex,imageUrl. - Associer vos références. Ajoutez un champ « ID Bloomy » sur chaque article de votre logiciel.
GET /catalog/variants?q=rose+rougeaide à retrouver une variante par nom — recherche insensible à la casse et aux accents. - Vérifier le périmètre réel.
GET /stockretourne 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. - Premier envoi.
POST /stock/bulkavec l'ensemble des variantes mappées. - Synchronisation continue. Rejouez
POST /stock/bulkà chaque changement de stock, ou à intervalle régulier — toutes les 15 minutes par exemple. - 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.
- Le fleuriste ouvre Inventaire dans l'application Bloomy.
- Une bannière indique combien d'articles sont synchronisés et à quand remonte la dernière mise à jour.
- Vous lancez une synchronisation depuis votre logiciel.
- 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.
Vérifie la clé et la boutique associée.
Requête
curl -H "Authorization: Bearer $BLOOMY_KEY" \
https://app.bloomy.one/api/v1/pingRé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"
}Liste et recherche les variantes du catalogue global.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
q | string | — | Recherche plein texte sur le nom, insensible à la casse et aux accents. Tous les mots doivent correspondre. |
type | enum | — | flower, foliage, ornament, extra. |
color | int | — | Filtre exact sur colorValue. |
limit | int | 50 | Maximum 200. |
offset | int | 0 | Pagination. |
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é.
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"
}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
| Champ | Ce qu'il vous dit |
|---|---|
available | L'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. |
consumedByBloomySinceSync | Unité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. |
externalRef | La référence que vous avez transmise lors de la dernière écriture. |
duplicateVariants | Plusieurs 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. |
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"
}Fixe la quantité exacte d'une variante. PATCH est accepté comme alias.
| Champ | Type | Défaut | Description |
|---|---|---|---|
stock | int | requis | Quantité absolue. Entier, de 0 à 100000. |
reference | string | — | Votre référence interne, stockée et renvoyée dans les lectures. 120 caractères max. |
createIfMissing | bool | false | Cré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.
L'endpoint à utiliser en production. PUT est accepté comme alias.
| Champ | Type | Défaut | Description |
|---|---|---|---|
updates | array | requis | 1 à 500 entrées { variantId, stock }. |
reference | string | — | Référence commune, par exemple l'identifiant du job de sync. |
createIfMissing | bool | false | Identique au PUT, appliqué à toutes les entrées. |
missingBehaviour | enum | ignore | ignore 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"
}| HTTP | Code | Signification |
|---|---|---|
| 400 | invalid_stock | stock absent, non entier, négatif ou supérieur à 100000. |
| 400 | invalid_payload | updates absent ou vide. |
| 400 | invalid_type · invalid_limit | Paramètre de requête invalide. |
| 401 | missing_credentials | Aucune clé fournie. |
| 401 | invalid_credentials | Clé mal formée, inconnue, ou secret erroné. |
| 403 | key_revoked · key_expired | Clé désactivée. Émettez-en une nouvelle. |
| 403 | insufficient_scope | La clé n'a pas le scope requis. |
| 404 | variant_not_found | Le variantId n'existe pas au catalogue Bloomy. |
| 404 | not_in_inventory | La variante existe au catalogue mais pas dans cette boutique. Utilisez createIfMissing. |
| 404 | route_not_found | Chemin ou méthode inconnus. |
| 409 | item_deleted | L'article a été supprimé pendant l'écriture. Rejouez. |
| 413 | payload_too_large | Plus de 500 entrées dans updates. |
| 429 | rate_limited | Quota dépassé. Respectez l'en-tête Retry-After. |
| 500 | internal_error | Erreur 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
| Limite | Valeur |
|---|---|
| Lectures | 240 requêtes / minute / clé |
| Écritures | 60 requêtes / minute / clé |
Entrées par appel bulk | 500 |
| Quantité maximale par variante | 100 000 |
| Fraîcheur du catalogue | jusqu'à 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
bulkvaut mieux que NPUT. 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
consumedByBloomySinceSyncavant d'écrire et le déduire de votre propre stock. - Renseignez
referenceavec un identifiant de job : il ressort dansGET /stocket dans les journaux Bloomy, ce qui accélère beaucoup le support. - Surveillez
available. Un article àfalsemalgré 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 quandcatalogVersionchange.
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-Versionet dansGET /ping. - Le chemin
/api/v1est 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
requestIdde la requête concernée.