@hylmo/sdk
v0.1.0
Published
SDK storefront Hylmo — client HTTP typé (catalogue, disponibilité, panier local, checkout) pour intégrer Hylmo depuis un site tiers.
Readme
@hylmo/sdk
SDK storefront Hylmo : un client HTTP typé, isomorphe (navigateur / Node ≥ 18)
et sans dépendance runtime, pour intégrer la location Hylmo (catalogue,
disponibilité, panier, checkout, paiement, espace client, devis) depuis
n'importe quel site — sans passer par apps/web.
Licence propriétaire — l'usage du SDK est réservé aux clients Hylmo disposant d'un abonnement actif et d'une clé d'API valide (voir
LICENSE). Le paquet est distribué publiquement pour faciliter l'intégration, mais reste inerte sans clé d'API délivrée par Hylmo.
Installation
npm install @hylmo/sdk
# ou
pnpm add @hylmo/sdkNode ≥ 18 (fetch natif). Dans le navigateur, aucun polyfill requis.
Démarrage rapide
import { createHylmoClient, HylmoApiError } from '@hylmo/sdk';
const hylmo = createHylmoClient({
apiKey: 'pk_live_…', // clé publiable — résout le tenant côté serveur
agencyCode: 'LYON', // optionnel : injecté dans les lectures storefront
// baseUrl: 'https://api.hylmo.com' // par défaut
});
// 1. Résoudre l'agence (les actions attendent un UUID `agencyId`)
const agency = await hylmo.agencies.get('LYON');
// 2. Catalogue
const { data: items } = await hylmo.catalog.list({ search: 'enceinte', page: 1 });
const product = await hylmo.catalog.get(items[0].slug);
// 3. Panier local (pur — aucun réseau)
const cart = hylmo.cart.create({
start: '2026-08-01',
end: '2026-08-03',
agencyId: agency.id,
});
cart.add({ catalogItemId: product.id, quantity: 2, label: product.name });
// 4. Disponibilité
const dispo = await hylmo.availability.check({
catalogItemId: product.id,
agencyId: agency.id,
startDate: '2026-08-01',
endDate: '2026-08-03',
quantity: 2,
});
// 5. Tarification + totaux locaux
const pricing = await hylmo.pricing.calculate({
items: cart.items.map((i) => ({
catalogItemId: i.catalogItemId,
catalogItemVariantId: i.catalogItemVariantId,
quantity: i.quantity,
})),
agencyId: agency.id,
startDate: '2026-08-01',
endDate: '2026-08-03',
});
const totals = cart.computeTotals(pricing.items, { mode: 'ttc' });
// 6. Code promo (optionnel)
try {
const promo = await hylmo.promoCodes.validate('ETE2026', { agencyId: agency.id });
cart.setPromo(promo);
} catch (err) {
if (err instanceof HylmoApiError) console.warn(err.message);
}
// 7. Checkout — le serveur RECALCULE tout ; le body vient du panier
const { orderNumber, orderId } = await hylmo.checkout.submit(
cart.toCheckoutPayload(
{ name: 'Jean Dupont', email: '[email protected]', phone: '0600000000' },
{ isQuote: false },
),
);Un parcours complet compilable est fourni dans examples/vanilla.ts.
Tableau des méthodes
Lectures storefront (/api/v1/storefront/*)
| Méthode | Description |
|---|---|
| catalog.list(params?) | Liste paginée (filtres catégorie/agence/recherche/tri) |
| catalog.get(slug) | Fiche produit complète (variantes, packs, accessoires, similaires) |
| categories.list() | Arborescence des catégories |
| agencies.list() | Liste des agences |
| agencies.get(code) | Profil public d'une agence (horaires, FAQ, note…) |
| reviews.list(params?) | Avis publiés, paginés |
| config.get(params?) | Réglages publics (mode commande/devis, paiement en ligne, devise…) |
Actions (/api/web/*)
| Méthode | Description |
|---|---|
| availability.check(params) | Stock disponible pour une période/quantité |
| pricing.calculate(params) | Tarifie chaque item (ordre préservé) |
| pricing.cartDiscount(params) | Remise multi-produits du panier |
| promoCodes.validate(code, { agencyId }) | Valide un code promo |
| delivery.listModes(params) | Modes de livraison disponibles |
| delivery.simulate(params) | Prix simulé des modes de livraison |
| checkout.submit(payload) | Checkout atomique (commande ou devis) |
| contact.send(payload) | Formulaire de contact |
| payment.createIntent(orderId) | Crée un PaymentIntent Stripe Connect pour une commande |
Espace client (/api/customers/auth/*)
| Méthode | Description |
|---|---|
| auth.requestMagicLink({ email, type? }) | Envoie un email de connexion (lien magique) |
| auth.verifyMagicLink({ token }) | Échange le token contre une CustomerSession |
| auth.requestOtp({ email }) | Envoie un code de connexion à 6 chiffres |
| auth.verifyOtp({ email, code }) | Vérifie le code et retourne une CustomerSession |
Portail client (session requise — hylmo.withSession(session).portal)
| Méthode | Description |
|---|---|
| portal.getProfile() / portal.updateProfile(params) | Profil du client |
| portal.orders.list(params?) / .get(id) | Commandes du client (paginé / détail) |
| portal.orders.cancel(id, params?) | Annule une commande (si le statut le permet) |
| portal.orders.acceptQuote(id) | Accepte un devis depuis le portail |
| portal.quotes.list() | Devis du client (métadonnées, sans accessToken) |
| portal.invoices.list() | Factures du client (métadonnées + URL PDF) |
| portal.addresses.list() / .create() / .update(id, params) / .remove(id) | Carnet d'adresses |
Devis par token (/api/quotes/:accessToken* — public, sans clé requise)
| Méthode | Description |
|---|---|
| quotes.get(accessToken) | Détail du devis (lignes, validité, dispo temps réel) |
| quotes.accept(accessToken, params?) | Accepte le devis (quote → confirmed) |
| quotes.pdfUrl(accessToken) | Construit l'URL du PDF (aucun appel réseau) |
Panier local (pur — aucun réseau)
| Méthode | Description |
|---|---|
| hylmo.cart.create(options) / createCart(options) | Crée un panier local |
| cart.add(item) | Ajoute une ligne (fusionne les lignes identiques) |
| cart.remove(id, variantId?, selKey?) | Retire une ligne |
| cart.updateQuantity(id, qty, variantId?, selKey?) | Change la quantité (0 = retrait) |
| cart.setDates(start, end) | Change la période |
| cart.setPromo(promo) / cart.clearPromo() | Applique/retire un code promo validé |
| cart.items / cart.dates / cart.promoState / cart.agency | État courant (copies) |
| cart.computeTotals(pricingResults, options?) | Totaux (pur) : sous-total, remises, TVA blended |
| cart.toCheckoutPayload(customer, options?) | Body EXACT de POST /api/web/checkout |
Gestion des erreurs
Toutes les méthodes réseau rejettent une erreur typée :
import { HylmoApiError, HylmoNetworkError } from '@hylmo/sdk';
try {
await hylmo.checkout.submit(payload);
} catch (err) {
if (err instanceof HylmoApiError) {
// L'API a répondu ≥ 400 : err.status (HTTP) + err.code métier
// (INSUFFICIENT_STOCK, INVALID_INPUT, NOT_FOUND, INVALID_API_KEY…)
console.error(err.status, err.code, err.message, err.details);
} else if (err instanceof HylmoNetworkError) {
// La requête n'a jamais abouti (offline, DNS, TLS…)
}
}Les GET sont retentés une fois (backoff court) sur erreur réseau ou 5xx. Les POST/PATCH/DELETE ne sont jamais retentés (non idempotents).
Espace client (auth + portail)
Flux type : magic link ou OTP → CustomerSession (sessionToken opaque,
signé par l'API — le SDK ne détient jamais SESSION_SECRET) → client portail
lié via hylmo.withSession(session).
import { createHylmoClient } from '@hylmo/sdk';
const hylmo = createHylmoClient({ apiKey: 'pk_live_…' });
// 1a. Magic link (email avec lien de connexion)
await hylmo.auth.requestMagicLink({ email: '[email protected]' });
// … le client clique le lien, votre page `/auth/verify?token=…` appelle :
const session = await hylmo.auth.verifyMagicLink({ token: '<depuis l’URL>' });
// 1b. OU code à 6 chiffres (connexion inline, ex. sans quitter le checkout)
const { customerExists } = await hylmo.auth.requestOtp({ email: '[email protected]' });
if (customerExists) {
// const session = await hylmo.auth.verifyOtp({ email: '[email protected]', code: '123456' });
}
// 2. `session` = { customerId, email, firstName, lastName, sessionToken }.
// À persister côté intégrateur (cookie, storage…) puis à repasser tel quel
// (ou juste `session.sessionToken`) à `withSession` :
const { portal } = hylmo.withSession(session);
// 3. Portail — la clé d'API part TOUJOURS en `Authorization: Bearer`, la
// session TOUJOURS en `x-customer-session` (jamais l'inverse).
const profile = await portal.getProfile();
await portal.updateProfile({ phone: '0600000000' });
const { data: orders } = await portal.orders.list({ page: 1, status: 'confirmed' });
const order = await portal.orders.get(orders[0].id);
await portal.orders.cancel(order.id, { reason: 'Changement de programme' });
const { data: quotes } = await portal.quotes.list();
const { data: invoices } = await portal.invoices.list();
const address = await portal.addresses.create({
type: 'delivery',
line1: '12 rue de la Paix',
postalCode: '75002',
city: 'Paris',
isDefault: true,
});
await portal.addresses.update(address.id, { label: 'Domicile' });
await portal.addresses.remove(address.id);hylmo.withSession(session) accepte le sessionToken brut (string) ou
l'objet CustomerSession complet — les deux sont équivalents.
Paiement (Stripe)
payment.createIntent(orderId) crée un PaymentIntent Stripe Connect pour une
commande website en attente de paiement (new/confirmed, non payée). Le
checkout (ou le devis accepté) renvoie déjà orderId pour enchaîner. Montage
de Stripe Elements avec le compte connecté du loueur :
import { loadStripe } from '@stripe/stripe-js';
import { createHylmoClient } from '@hylmo/sdk';
const hylmo = createHylmoClient({ apiKey: 'pk_live_…' });
const { orderId } = await hylmo.checkout.submit(payload); // ou depuis un devis accepté
const { clientSecret, connectedAccountId } = await hylmo.payment.createIntent(orderId);
// `stripeAccount` = compte Connect du loueur — indispensable, sinon le
// paiement part sur le compte plateforme au lieu du compte du loueur.
const stripe = await loadStripe(STRIPE_PUBLISHABLE_KEY, { stripeAccount: connectedAccountId });
const elements = stripe.elements({ clientSecret });
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// Au submit du formulaire :
const { error } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: 'https://votre-site.example/merci' },
});Devis
Une commande créée en mode devis (checkout.submit({ ..., isQuote: true }))
renvoie quoteAccessToken : un token opaque, secret, qui donne accès au devis
sans authentification (ni clé d'API, ni session client) — à transmettre au
client par email/lien, jamais à afficher publiquement.
const quote = await hylmo.quotes.get(accessToken);
console.log(quote.canAccept, quote.total, quote.lines.length);
if (quote.canAccept) {
await hylmo.quotes.accept(accessToken);
}
// URL du PDF — à ouvrir dans un nouvel onglet ou définir en `href`, aucun
// appel réseau n'est fait par le SDK.
const pdfUrl = hylmo.quotes.pdfUrl(accessToken);Le portail authentifié (portal.orders.acceptQuote(id)) applique les mêmes
garde-fous (validité, expiration, disponibilité temps réel) que ce lien public.
Invariants
- Prix en centimes (entiers) partout — jamais de float.
- Percent en basis points :
1000= 10 %. - Remise en HT (
discountAmount) : elle réduit la base de TVA (conforme FR). - Les totaux calculés localement par
cart.computeTotalssont une estimation d'affichage : le serveur recalcule tout au checkout et ne fait jamais confiance au body envoyé.
