Middleware entrant
Une intégration est un middleware : en plus de recevoir les webhooks de cycle de vie de ShopiMind et de pousser ses données via la synchronisation, elle peut exposer des routes entrantes que votre propre application appelle — pour déclencher un événement ou pousser des données en temps réel, sans attendre le prochain cycle de synchronisation.
your application ──(signed call)──► your integration ──(ctx.spm)──► ShopiMindC'est l'outil idéal lorsqu'un événement se produit de votre côté (un client gagne des points, un avis est publié, une commande est passée en magasin) et que vous voulez le refléter immédiatement dans ShopiMind.
Déclarer une route — le champ inbound
Ajoutez un champ inbound à votre defineIntegration. Chaque clé est une action ; chaque valeur est un handler qui reçoit le ctx (avec ctx.spm déjà authentifié) et le payload reçu :
import { SpmEvents } from '@shopimind/integration-kit-js';
export const myIntegration = defineIntegration({
// …
inbound: {
'loyalty.points_earned': async (ctx, payload) => {
const { email, points } = payload as { email: string; points: number };
await SpmEvents.trigger(ctx.spm, 'points_earned', { email, points });
},
'review.created': async (ctx, payload) => {
const review = payload as { email: string; rating: number };
await ctx.customData('reviews').save([
{ email: review.email, rating: review.rating },
]);
},
},
});Chaque action <action> est exposée par le kit sous POST /inbound/{action}. Le handler est générique : faites-y tout ce dont vous avez besoin via ctx.spm + les méthodes statiques du SDK (SpmEvents.trigger, …), ctx.sendBulk pour un bulk sûr, et ctx.customData(name).save(...) pour les enregistrements de données personnalisées.
TIP
ctx.customData(name) résout la définition déclarée dans provisioning.customData et effectue un upsert sûr (par lots, sûr pour le transport, en remontant les rejets par enregistrement). Pour les bulks à paramètre de chemin, encapsulez l'appel du SDK avec ctx.sendBulk(() => SpmCustomDataRecords.bulkSave(ctx.spm, defId, [/* … */])).
Sécurité : une signature HMAC par installation
Contrairement aux webhooks de cycle de vie (signés par ShopiMind avec un seul webhook_secret), les appels entrants viennent de votre application. Le kit attribue donc à chaque installation son propre secret HMAC, exposé sous ctx.inboundSecret. Transmettez-le à votre application — typiquement dans onActivate :
hooks: {
onActivate: async (ctx) => {
await myApp.registerShopimindEndpoint({
installationId: ctx.installationId, // opaque token, send it back as-is
inboundUrl: `${MY_BASE_URL}/inbound`,
secret: ctx.inboundSecret, // the secret of THIS installation
});
},
}Votre application signe ensuite chaque appel, exactement comme ShopiMind signe ses webhooks, mais avec ce secret et des en-têtes x-integration-* :
| En-tête | Contenu |
|---|---|
x-integration-installation | L'installationId opaque (route l'appel et résout le secret). |
x-integration-timestamp | L'horodatage en secondes. |
x-integration-signature | HMAC-SHA256("${timestamp}.${raw_body}", inboundSecret) en hexadécimal. |
x-idempotency-key | (optionnel) une clé unique par appel — voir Idempotence. |
// on YOUR application's side
const body = JSON.stringify({ email, points: 50 });
const ts = Math.floor(Date.now() / 1000);
const signature = hmacSha256Hex(`${ts}.${body}`, inboundSecret);
await fetch(`${inboundUrl}/loyalty.points_earned`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-integration-installation': installationId,
'x-integration-timestamp': String(ts),
'x-integration-signature': signature,
'x-idempotency-key': eventId, // optional
},
body,
});Le kit vérifie la signature sur les octets bruts, en temps constant, avec une fenêtre anti-rejeu (300 s par défaut). Le token de l'API ShopiMind n'est jamais exposé à votre application : elle ne connaît que le secret entrant de l'installation.
Idempotence
Si votre appel porte un en-tête x-idempotency-key, le kit journalise l'appel et court-circuite tout rejeu d'une clé déjà traitée avec succès (il répond 200 avec { "replayed": true }). Un appel qui n'a pas encore abouti est re-tenté normalement. Utilisez une clé stable par événement (l'id de l'événement de votre côté) pour pouvoir re-tenter sans risque de double traitement.
Limitation de débit
Chaque installation dispose d'un seau de jetons : au-delà du débit autorisé, le kit répond 429 Too Many Requests. Faites des appels groupés ou ré-essayez avec un léger délai.
Codes de réponse
| Code | Signification |
|---|---|
200 | Traité ({ success: true }) — ou rejeu déjà traité ({ success: true, replayed: true }). |
400 | En-tête d'installation manquant, ou corps JSON invalide. |
401 | Installation inconnue, ou signature invalide / trop ancienne. |
404 | Action inconnue (aucune clé inbound correspondante). |
429 | Limite de débit atteinte pour cette installation. |
500 | Le handler a échoué (vous pouvez re-tenter ; l'idempotence dédoublonne au succès). |
Tester
Le harnais de test du kit signe un appel entrant pour vous :
import { makeTestApp } from '@shopimind/integration-kit-js';
const app = makeTestApp(myIntegration);
// … activate an installation …
const { body, headers } = app.signInbound(installationId, { email: 'a@b.co', points: 50 });
const res = await app.server.inject({ method: 'POST', url: '/inbound/loyalty.points_earned', payload: body, headers });
// res.statusCode === 200Pour aller plus loin
- Le kit d'intégration —
ctx.inboundSecret, routes exposées. - Cycle de vie & webhooks — l'autre flux entrant (émis par ShopiMind).
- Synchroniser les données — pour les volumes par lots (initial + incrémental).
- Événements · Données personnalisées — ce que vos handlers poussent.