Rappels entrants
Certaines actions ne peuvent pas être exécutées par ShopiMind : créer un bon de réduction dans votre boutique, ouvrir un compte client, inscrire quelqu'un à votre newsletter. ShopiMind vous appelle donc, sur des routes dérivées de url_client.
C'est le canal connecteur. Il a son schéma de signature propre, différent de celui des webhooks d'intégration, ne les confondez pas.
Les trois actions
Vous n'implémentez que celles dont vous voulez les fonctionnalités. Une route absente se traduit par une fonctionnalité indisponible, pas par une erreur globale.
| Action | Route | Déclenchée par | Priorité |
|---|---|---|---|
generateVouchers | POST {url_client}/vouchers | Un scénario qui génère un bon (nominatif ou non) | Haute : sans elle, les bons des scénarios n'existent pas |
createCustomer | POST {url_client}/customers | Un widget avec création de compte | Selon vos widgets |
subscribeCustomer | POST {url_client}/subscribe-customer | Un opt-in newsletter depuis un widget | Selon vos widgets |
L'en-tête type-request porte le nom de l'action, pratique pour router.
Transport
| Méthode | POST |
| Content-Type | application/x-www-form-urlencoded |
| Encodage | Notation crochet pour l'imbrication : voucherInfos[type]=percent, valeurs encodeURIComponent-ées |
| Timeout | 90 secondes |
| Relances | 3 tentatives, backoff exponentiel |
| Redirections | Suivies same-site uniquement (apex ↔ www tolérés), en conservant méthode et corps. Toute redirection cross-origin ou vers / est refusée avec une erreur explicite |
Le corps n'est pas du JSON
C'est du x-www-form-urlencoded avec notation crochet. Si votre framework parse automatiquement en JSON, vous recevrez un corps vide et la vérification de signature échouera. Le plus simple est de récupérer le corps brut, de le parser vous-même, puis de signer sur la structure obtenue.
En-têtes reçus
| En-tête | Contenu |
|---|---|
Shopimind-Client-Identifiant | L'identifiant de votre boutique, à comparer à celui que vous avez stocké |
Shopimind-Token | Le HMAC du corps (voir ci-dessous) |
Shopimind-Client-Version | Écho de ce que vous avez déclaré à la connexion |
Shopimind-Client-Build | Idem |
type-request | Nom de l'action |
Normalisez la casse
Les en-têtes HTTP sont insensibles à la casse, et selon le serveur ou le reverse-proxy (HTTP/2, proxy qui normalise), vous les recevrez en minuscules ou en casse mixte. Le plus sûr est de passer les clés en minuscules avant lecture.
Signature
Le secret
Le HMAC est calculé avec la clé primaire de la boutique : celle qui a exécuté le dernier POST /v1/shop/connection. Plus précisément :
secret = partie après le point de la clé API // "a1b2c3d4.xK9p…" → "xK9p…"
cléHMAC = sha256(secret) en HEXADÉCIMAL // la CHAÎNE hexa, pas les octetsL'aplatissement du corps
1. Prendre le corps DÉSÉRIALISÉ (l'objet, pas la chaîne brute)
2. Trier les clés de PREMIER NIVEAU seulement
→ les niveaux imbriqués gardent leur ordre d'insertion
3. Parcourir récursivement et concaténer en "clé;valeur;clé;valeur…"
→ les objets sont traversés SANS émettre leur propre clé
→ les valeurs strictement égales à '0' sont IGNORÉES
4. Retirer TOUTES les espaces de la chaîne obtenue
5. token = HMAC-SHA256(cléHMAC, chaîneAplatie) en hexadécimalTrier tous les niveaux casse la signature
Seul le premier niveau est trié. Trier récursivement produit une chaîne différente pour toute charge imbriquée, donc un rejet systématique de generateVouchers, qui est justement imbriquée. C'est le piège n°1 des réimplémentations.
Exemple de contrôle. Pour le corps { testConnection: 1785412468, shopIdShop: '1' }, la chaîne aplatie est exactement :
shopIdShop;1;testConnection;1785412468Vérification avec le SDK
Le SDK expose la primitive publique du canal connecteur, n'écrivez pas la vôtre si vous êtes sur Node :
import { SpmRequestValidator } from '@shopimind/sdk-js';
// Le corps arrive en x-www-form-urlencoded, notation crochet
const body = SpmRequestValidator.parseFormData(rawBody);
const check = SpmRequestValidator.validateRequest({
clientId: headers['shopimind-client-identifiant'],
hmacToken: headers['shopimind-token'],
body,
apiIdentification: process.env.SPM_SHOP_IDENTIFIER, // identifiant de la boutique
apiPassword: process.env.SPM_API_KEY, // clé complète "prefix.secret"
});
if (!check.valid) {
return res.json({ success: false, message: 'Unauthorized.' });
}parseFormData filtre __proto__, constructor et prototype : un corps encodé ne peut pas polluer Object.prototype. Si vous parsez vous-même, reproduisez cette protection.
Vérification sans dépendance (WebCrypto)
Pour un runtime edge (Workers, Deno) où node:crypto n'est pas disponible :
function implodeRecursive(obj) {
let out = '';
const walk = (data) => {
if (!data || typeof data !== 'object') return;
for (const k of Object.keys(data)) {
const v = data[k];
if (v !== null && typeof v === 'object') walk(v);
else if (v !== '0') out += out === '' ? `${k};${v}` : `;${k};${v}`;
}
};
const sorted = {};
Object.keys(obj).sort().forEach((k) => { sorted[k] = obj[k]; }); // 1er niveau seulement
walk(sorted);
return out.replace(/ /g, '');
}
const hex = (buf) => [...new Uint8Array(buf)]
.map((b) => b.toString(16).padStart(2, '0')).join('');
async function verifyShopimindToken(body, receivedToken, apiKey) {
const secret = String(apiKey).split('.')[1] ?? apiKey;
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));
const keyHex = hex(digest); // la CHAÎNE hexa est la clé HMAC
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode(keyHex),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const sig = await crypto.subtle.sign(
'HMAC', key, new TextEncoder().encode(implodeRecursive(body)),
);
// Comparaison à temps constant
const a = hex(sig), b = String(receivedToken ?? '');
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}Durcissement
Quelques bonnes pratiques pour renforcer la sécurité de vos routes de rappel :
- HTTPS strict, sans redirection sur les routes de rappel.
- Idempotence sur
codeToGeneratepour les bons : un rejeu ne doit pas créer un second bon. - Allowlist des IP sortantes ShopiMind si votre infrastructure le permet, demandez-nous la liste.
- Comparaison à temps constant du jeton, comme dans les exemples ci-dessus.
generateVouchers : bons de réduction
C'est l'action la plus utilisée : un scénario qui offre une remise appelle cette route pour que le bon existe réellement dans votre boutique.
// Corps reçu (présenté en JSON pour la lisibilité)
{
"testConnection": 1785412468,
"shopIdShop": "1", // seulement si multiboutique
"voucherInfos": {
"codeToGenerate": "SPM-4F2A9C", // le code à créer, VOTRE CLÉ D'IDEMPOTENCE
"type": "percent", // "amount" | "percent" | "shipping"
"amount": "10",
"nbDayValidate": "7", // durée de validité en jours
"minimumOrder": "50", // facultatif
"amountCurrency": "EUR", // facultatif
"duplicateCode": "PROMO_MODELE", // facultatif, dupliquer une règle existante
"dateStart": "…", // facultatif
"dynamicPrefix": "…" // facultatif
},
"voucherEmails": [ // PRÉSENT = bon NOMINATIF
{ "email": "client@exemple.fr", "description": "Relance panier" }
]
}Nominatif ou générique
voucherEmailsprésent → un bon par email, rattaché au compte client correspondant. Si l'email ne correspond à aucun compte, le plus simple est d'ignorer cette entrée pour éviter un bon orphelin.voucherEmailsabsent → un seul bon générique, utilisable par n'importe qui.
Réponse attendue
// Nominatif, un objet par email
{
"success": true,
"vouchers": {
"client@exemple.fr": {
"voucher_number": "SPM-4F2A9C",
"voucher_date_limit": "2026-08-11 23:59:59"
}
}
}
// Générique
{
"success": true,
"vouchers": { "voucher_number": "SPM-4F2A9C", "voucher_date_limit": "2026-08-11 23:59:59" }
}success: true ne suffit pas
ShopiMind ne considère l'opération réussie que si success vaut true et que vouchers est renseigné. Un { success: true, vouchers: {} } est traité comme un échec, et le message partira sans son code de réduction.
Les trois types de remise :
type | Sémantique |
|---|---|
amount | Remise en valeur absolue, dans amountCurrency (ou la devise par défaut). |
percent | Remise en pourcentage. |
shipping | Frais de port offerts. amount est alors ignoré. |
nbDayValidate est un nombre de jours à partir de maintenant. Par convention, fixez l'expiration à 23:59:59 du jour d'échéance.
duplicateCode, quand il est présent, désigne une règle de remise existante dans votre boutique à dupliquer : le marchand configure sa promotion une fois, ShopiMind en génère des copies à code unique. Si le code de référence n'existe pas, une erreur explicite dans la réponse est préférable.
createCustomer : création de compte
{
"testConnection": 1785412468,
"shopIdShop": "1",
"customer": {
"firstName": "Marie",
"lastName": "Dupont",
"email": "marie@exemple.fr",
"password": "…",
"lang": "fr",
"newsletter": 1,
"birthday": "1990-04-12" // facultatif
}
}{ "success": true, "message": "Customer created successfully.", "id_customer": "4212" }id_customer doit être votre identifiant client, le même que celui que vous poussez dans customer_id via l'API. C'est lui qui fait la jonction entre le compte créé et le contact ShopiMind.
En cas d'email déjà existant, renvoyez success: false : ShopiMind affiche alors un message « email déjà enregistré » plutôt qu'une erreur technique.
subscribeCustomer : opt-in newsletter
{ "testConnection": 1785412468, "shopIdShop": "1", "id_customer": "4212" }{ "success": true, "message": "Customer subscribed successfully." }Règles générales de réponse
- La réponse attendue est toujours un
HTTP 200avec un corps JSON ; le résultat métier est porté parsuccess. - Sur signature invalide :
{ "success": false, "message": "Unauthorized." }. - Mieux vaut intercepter vos exceptions plutôt que de laisser partir un 500 : ShopiMind le classerait comme incident de transport et retenterait l'appel trois fois, ce qui pourrait créer des doublons sur
generateVouchers. - Pensez à vérifier la signature avant tout traitement, même de lecture.
Étape suivante
→ Référence : terminologie, parité, diagnostic.