Le guide pas-à-pas d'un ingénieur pour une synchronisation d'inventaire prête pour la production entre Shopify et NetSuite sur n8n auto-hébergé. Architecture webhook, GraphQL Admin API, gestion des limites de taux et pattern de déduplication qui garde les stocks justes à l'échelle.

Tout retailer multicanal passé une certaine taille se heurte au même mur. NetSuite est la source de vérité de l'inventaire. Shopify est le storefront client. Les deux dérivent dans les heures qui suivent la mise en ligne, et le mode d'échec est du pire genre : les clients achètent des produits qui ne sont pas réellement disponibles, l'équipe opérations ajuste manuellement les stocks deux fois par jour, et quelqu'un est toujours à une alerte près de la survente. Les réponses standards (Celigo entre 400 et 1 500 $ par mois, NetSuite Connector avec une personnalisation limitée, licences Boomi à 129 K$) fonctionnent, mais elles coûtent un multiple de ce que le vrai travail demande pour beaucoup de grossistes mid-market.
Cet article est le pas-à-pas pour la sync d'inventaire Shopify NetSuite que nous déployons sur n8n chez les clients qui ont décidé de posséder cette couche de leur stack. Il s'inscrit dans la suite de notre setup n8n auto-hébergé sur AWS et de notre comparaison architecture d'intégration Shopify NetSuite. Le workflow garde les stocks justes à 60 secondes près sur des milliers de SKU, coûte 33 $ par mois d'infrastructure plus un build une seule fois, et survit en production parce que l'ingénierie est dans les modes d'échec, pas dans le happy path.
Un workflow bidirectionnel sur n8n, déclenché par les événements d'ajustement d'item NetSuite et réconcilié sur un calendrier, qui :
Le tout tient dans 12 à 18 nodes n8n une fois découpé en quelques workflows reliés. L'ingénierie est dans la gestion des parties que les démos ne montrent pas : que se passe-t-il quand Shopify vous rate-limite en plein burst, que se passe-t-il quand un webhook se déclenche deux fois, que se passe-t-il quand NetSuite renvoie un 500 en plein milieu d'une sync.
Les décisions d'architecture à souligner dès le départ :
Événementiel en primaire, réconciliation planifiée. Un workflow purement événementiel rate des changements quand les webhooks tombent. Un workflow purement planifié tourne en permanence et réagit lentement. La bonne réponse est les deux : événements pour la vitesse, planification pour la justesse.
La déduplication est un sujet de première classe. NetSuite peut déclencher deux événements pour le même changement logique d'inventaire (un fulfillment qui déclenche à la fois un événement de réception et un événement de fulfillment). Sans déduplication, vous pouvez vous faire la course à vous-même et finir par pousser la mauvaise quantité. La table Postgres qui log chaque push est aussi la table qui empêche les doublons.
Idempotence par réconciliation de quantité, pas par suppression d'événement. Plutôt que d'essayer de supprimer les événements doublons (qui est fragile), chaque push lit d'abord la quantité Shopify actuelle, calcule le delta, et applique le delta. Pousser "régler la quantité à 42" deux fois est sûr ; pousser "décrémenter de 3" deux fois ne l'est pas. Le workflow utilise des quantités absolues partout.
Dans NetSuite, déployez un User Event Script (SuiteScript 2.x) sur les types d'enregistrement Inventory Adjustment, Item Receipt, Item Fulfillment et Bin Transfer. Le script se déclenche sur afterSubmit et poste vers votre webhook n8n.
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/https', 'N/runtime'], (https, runtime) => {
function afterSubmit(context) {
if (context.type === context.UserEventType.VIEW) return;
const record = context.newRecord;
const webhookUrl = runtime.getCurrentScript()
.getParameter('custscript_n8n_webhook_url');
const secret = runtime.getCurrentScript()
.getParameter('custscript_n8n_webhook_secret');
// Payload : type d'enregistrement, ID interne, items affectés
const payload = {
eventType: record.type,
recordId: record.id,
timestamp: Date.now(),
items: extractInventoryImpact(record)
};
const body = JSON.stringify(payload);
const hmac = computeHmacBase64(body, secret);
try {
https.post({
url: webhookUrl,
body: body,
headers: {
'Content-Type': 'application/json',
'X-NetSuite-Hmac-Sha256': hmac
}
});
} catch (e) {
log.error('échec webhook n8n', e.message);
// ne pas lever : ne jamais bloquer la transaction NetSuite
}
}
return { afterSubmit };
});Le détail critique dans ce script : le try-catch autour de l'appel webhook. Si n8n est down ou lent, la transaction NetSuite doit quand même se terminer. Les mises à jour d'inventaire dans NetSuite sont la source de vérité ; on ne les bloque jamais sur une sync en aval.
Dans votre instance n8n (celle qui tourne sur le setup AWS que nous avons déployé), créez un nouveau workflow. Ajoutez les nodes dans cet ordre :
/netsuite-inventoryCopiez l'URL de webhook de production dans le paramètre SuiteScript custscript_n8n_webhook_url dans NetSuite.
Un node Code immédiatement après le trigger qui valide le HMAC. Ne jamais faire confiance à un webhook entrant sans vérification de signature.
const crypto = require('crypto');
const NETSUITE_WEBHOOK_SECRET = $env.NETSUITE_WEBHOOK_SECRET;
const body = JSON.stringify($input.first().json);
const hmac = $input.first().headers['x-netsuite-hmac-sha256'];
const computed = crypto
.createHmac('sha256', NETSUITE_WEBHOOK_SECRET)
.update(body, 'utf8')
.digest('base64');
if (computed !== hmac) {
throw new Error('Signature webhook NetSuite invalide');
}
return $input.first().json;Un node Postgres qui interroge la table de log pour voir si cet événement exact a déjà été traité. Nous loggons chaque push réussi par eventType + recordId + timestamp et vérifions une correspondance avant de traiter.
SELECT id FROM inventory_sync_log
WHERE event_type = $1
AND record_id = $2
AND processed_at > NOW() - INTERVAL '5 minutes'
LIMIT 1;Si une ligne revient, l'événement est un doublon et le workflow sort proprement. Si aucune ligne, on continue et on écrit une ligne de log à la fin.
Le payload NetSuite nous dit quels items ont été affectés. Pour chaque item (SKU), nous interrogeons la GraphQL Admin API de Shopify pour obtenir la quantité available actuelle à la location concernée.
query GetInventoryLevel($sku: String!, $locationId: ID!) {
inventoryItems(first: 1, query: $sku) {
edges {
node {
id
sku
inventoryLevel(locationId: $locationId) {
id
quantities(names: ["available"]) {
name
quantity
}
}
}
}
}
}Nous utilisons le GID inventoryItem (un identifiant global comme gid://shopify/InventoryItem/12345) et le GID de location. Les deux sont mappés depuis le SKU et depuis le code de location NetSuite respectivement, à travers une petite table de lookup maintenue dans Postgres ou dans la static data n8n.
Un node Code compare la quantité NetSuite (depuis le payload du webhook, joint au lookup) à la quantité Shopify (depuis la requête GraphQL). Si elles correspondent à une tolérance près, pas besoin de push. Si elles divergent, on calcule la nouvelle valeur absolue et on prépare la mutation.
const netsuiteQty = $('Parse NetSuite payload').first().json.availableQty;
const shopifyQty = $('Get Shopify inventory').first().json.data
.inventoryItems.edges[0].node.inventoryLevel
.quantities[0].quantity;
const tolerance = 0; // correspondance exacte requise pour l'inventaire
const delta = netsuiteQty - shopifyQty;
if (Math.abs(delta) <= tolerance) {
return { skip: true, reason: 'in sync' };
}
return {
skip: false,
inventoryItemId: $('Get Shopify inventory').first().json.data
.inventoryItems.edges[0].node.id,
locationId: $('Lookup location').first().json.shopifyLocationGID,
delta: delta,
reason: $('Parse NetSuite payload').first().json.eventType
};Si le node précédent a renvoyé skip: false, un node HTTP Request appelle la GraphQL Admin API de Shopify avec la mutation inventoryAdjustQuantities.
mutation AdjustInventory($input: InventoryAdjustQuantitiesInput!) {
inventoryAdjustQuantities(input: $input) {
userErrors { field message }
inventoryAdjustmentGroup {
createdAt
reason
changes { name delta }
}
}
}Avec les variables :
{
"input": {
"reason": "correction",
"name": "available",
"changes": [{
"delta": "{{delta from previous node}}",
"inventoryItemId": "{{inventoryItemId}}",
"locationId": "{{locationId}}"
}]
}
}Le champ reason accepte un petit ensemble de valeurs enum (correction, cycle_count_available, damaged, movement_created, etc.). Pour les ajustements pilotés par NetSuite, "correction" est la correspondance sémantique la plus proche.
C'est la partie que la plupart des templates omettent. L'Admin API Shopify utilise un leaky-bucket rate limiter. Le défaut pour les boutiques Plus est de 1 000 points de coût par minute, chaque requête coûtant des montants différents. Une mutation d'inventaire coûte environ 10 à 20 points ; une requête complexe peut coûter 50+.
Deux patterns que nous utilisons pour rester sous la limite :
// Après chaque appel Shopify, lire le champ
// extensions.cost.throttleStatus de la réponse et décider d'attendre
const cost = $input.first().json.extensions?.cost?.throttleStatus;
if (cost && cost.currentlyAvailable < 100) {
const restoreTime = (1000 - cost.currentlyAvailable)
/ cost.restoreRate * 1000;
await new Promise(r => setTimeout(r, restoreTime));
}
return $input.first().json;Le second pattern : les opérations bulk. Pour les runs de réconciliation qui touchent des centaines de SKU, utilisez l'API de mutations bulk de Shopify plutôt que de tirer une mutation par SKU. Une seule opération bulk respecte les limites de taux comme un seul appel.
Dernier node, écrire le résultat dans la table de log Postgres. Champs critiques : event_type, record_id, SKU, ancienne quantité, nouvelle quantité, timestamp du push, drapeau succès/échec, message d'erreur le cas échéant. C'est votre source de déduplication ET votre source de réconciliation.
L'échec de production le plus courant pour les workflows de sync d'inventaire, c'est la table de log qui grossit sans limite. Après 6 mois de trafic élevé, la requête de déduplication contre une table de log sans index peut prendre des secondes. Ajoutez un index sur (event_type, record_id, processed_at) et un job quotidien de nettoyage qui archive les lignes plus vieilles que 30 jours. Le workflow démarrera rapide et restera rapide.
Les événements ne suffisent pas. Les webhooks tombent en silence. Des blips réseau perdent des payloads. Quelqu'un modifie l'inventaire manuellement dans NetSuite sans déclencher l'événement utilisateur. Le workflow de réconciliation est le filet de sécurité.
Dans un workflow n8n séparé :
Toutes les heures. Expression cron 0 * * * *.
Un RESTlet que vous déployez dans NetSuite qui renvoie l'état d'inventaire actuel pour les items modifiés dans les 2 dernières heures :
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*/
define(['N/search'], (search) => {
return {
get: (params) => {
const since = params.since || (Date.now() - 7200000);
const itemSearch = search.create({
type: 'inventoryitem',
filters: [
['lastmodifieddate', 'after', new Date(since)]
],
columns: [
'itemid', 'inventoryLocation', 'locationquantityavailable'
]
});
const results = [];
itemSearch.run().each(result => {
results.push({
sku: result.getValue('itemid'),
location: result.getValue('inventoryLocation'),
quantity: result.getValue('locationquantityavailable')
});
return results.length < 1000; // pagination si besoin
});
return results;
}
};
});Le node HTTP de n8n appelle ce RESTlet avec une authentification par token.
Pour les mêmes SKU et locations, interroger la GraphQL Admin API de Shopify. Utilisez une opération bulk pour l'efficacité :
mutation {
bulkOperationRunQuery(
query: """
{
inventoryItems(query: "updated_at:>2026-02-19T20:00:00Z") {
edges {
node {
sku
inventoryLevels(first: 10) {
edges {
node {
location { id }
quantities(names: ["available"]) {
quantity
}
}
}
}
}
}
}
}
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}L'opération bulk renvoie une URL de fichier JSONL une fois terminée. Le workflow poll jusqu'à la complétion, télécharge le fichier et le parse.
Un node Code joint les deux datasets par SKU et location, identifie les écarts de quantité au-dessus du seuil de tolérance, et met en file des pushes de correction pour toute dérive. Ces corrections passent par la même mutation inventoryAdjustQuantities que nous avons utilisée dans le chemin événementiel.
Si le même SKU apparaît dans le rapport de dérive sur trois runs de réconciliation consécutifs, alerter. Une dérive persistante signifie que le chemin événementiel est cassé pour ce SKU, et la correction silencieuse cache un vrai bug. L'alerte pousse vers Slack ou PagerDuty avec le SKU et la taille de la dérive.
La production révèle des modes d'échec que les démos ne montrent pas. Cas que nous avons ingénieré :
Split multi-location. Un produit disponible à trois locations Shopify et routé différemment dans NetSuite. Le push doit savoir quelle location Shopify mappe à quelle location NetSuite, et la réconciliation doit comparer par location. La table de lookup est critique ici.
Parent de bundle vs composants. Un bundle dans NetSuite peut ne pas exister comme un SKU unique dans Shopify ; au lieu de ça, il fait plusieurs lignes. L'inventaire du bundle est le minimum des inventaires de composants. Le workflow doit savoir quels SKU sont des composants de bundle et recalculer l'inventaire du bundle quand un composant change.
Précommandes et back-orders. Les niveaux d'inventaire négatifs sont une fonctionnalité, pas un bug. Shopify supporte la survente avec un drapeau "continue selling when out of stock". Le workflow doit respecter ce drapeau plutôt que de refuser de pousser des quantités négatives.
Inventaire engagé vs disponible. NetSuite distingue le "on hand" du "available" (on hand moins les commandes engagées). L'available de Shopify est le chiffre côté client. Le push doit utiliser le locationquantityavailable de NetSuite, pas le locationquantityonhand, sinon vos clients verront du stock gonflé.
Casse et trim des SKU. Les SKU Shopify sont sensibles à la casse. Les IDs d'item NetSuite sont parfois en majuscule par convention, parfois mixtes. Un workflow qui pousse "WIDGET-001" quand Shopify a "widget-001" échoue silencieusement à chaque événement. Normalisez les SKU à la frontière de lookup.
Le jalon "la sync d'inventaire fonctionne" est quand le rapport de réconciliation montre zéro dérive sur sept jours consécutifs sur tous les SKU. Atteindre ce jalon est l'objectif de la phase de durcissement de production. Avant ça, vous avez une sync d'inventaire qui fonctionne la plupart du temps ; après ça, vous en avez une qui fonctionne.
Une comparaison de coût réaliste pour un grossiste mid-market à 20 000 commandes par mois entre Shopify Plus et NetSuite :
| Chemin | Coût année 1 | Coût annuel années 2+ |
|---|---|---|
| Connecteur Celigo NetSuite-Shopify | 25 à 40 K$ | 15 à 30 K$ |
| Boomi pour les mêmes flux | 80 à 150 K$ | 50 à 100 K$ |
| NetSuite Connector (SuiteApp) | 5 à 15 K$ (config) | inclus dans la licence NetSuite |
| Workflow n8n custom auto-hébergé AWS | 30 à 60 K$ (build) | 5 à 10 K$ (maintenance + AWS) |
Le chemin n8n est le plus compétitif face à Celigo et Boomi. Face au NetSuite Connector natif, le coût de build se rembourse rarement à moins que vous n'ayez sincèrement besoin d'une logique custom que le Connector ne peut pas modéliser. Nous avons couvert la comparaison complète dans notre article architecture d'intégration Shopify NetSuite.
Les contre-exemples honnêtes. Une sync d'inventaire n8n auto-hébergée n'est pas la bonne réponse pour chaque boutique.
Faible volume. Une boutique sous 1 000 commandes par mois ne stresse pas le NetSuite Connector natif. Le coût de construire custom ne se rembourse pas. Utilisez la SuiteApp.
Modèle de données standard. Une boutique dont le modèle d'inventaire rentre dans les défauts du NetSuite Connector (une location Shopify pour une location NetSuite, mono-subsidiary, items standards) n'a pas besoin de logique custom. Utilisez la SuiteApp.
Aucune capacité d'ingénierie. Le n8n auto-hébergé signifie que vous possédez le déploiement, les mises à niveau, les modes d'échec. Pour les équipes sans capacité DevOps, le service managé Celigo vaut son prix.
Vous avez besoin d'une UI pour le personnel ops. Le workflow n8n n'a pas d'interface admin. Si votre équipe opérations doit consulter le statut de sync ou déclencher des corrections manuelles depuis un dashboard, vous devez construire une petite app admin par-dessus, ou choisir un outil qui en livre une. Nous avons couvert cet arbitrage dans notre article app custom vs publique.
Missions récentes de sync d'inventaire :
Le pattern : la bonne architecture est rarement "tout n8n" ou "tout Celigo". C'est la combinaison qui colle à la forme opérationnelle et à la capacité d'ingénierie du business spécifique.
En production, notre latence typique event-vers-Shopify est de 5 à 15 secondes pour le chemin piloté par webhook. La réconciliation horaire rattrape tout ce qui a été manqué. Pour les boutiques où une sync en moins de 5 secondes est sincèrement requise (événements live, ventes flash), l'architecture scale en ajoutant plus de workers n8n et en utilisant les abonnements webhook Shopify côté commande pour éviter le polling.
Oui. Les commandes B2B consomment l'inventaire de la même façon que les commandes D2C, et les ajustements d'inventaire passent par les mêmes APIs. La complexité spécifique au B2B est dans la couche catalogue et pricing, pas dans la couche sync d'inventaire. Nous avons couvert Shopify B2B dans notre article Shopify Plus B2B.
Le pattern architectural est le même. Remplacez le RESTlet NetSuite et le User Event Script par l'équivalent dans votre ERP (SAP, Microsoft Dynamics, Sage, Acumatica ont tous des capacités de webhook ou d'émission d'événements). La logique du workflow n8n, les appels GraphQL Shopify, et la gestion de la déduplication et des limites de taux se transposent proprement.
Oui, avec des réserves. n8n Cloud gère la même logique de workflow, mais vous perdez la capacité de déployer dans une région UE de votre choix (pertinent pour la conformité RGPD ou PIPEDA), et vous perdez le contrôle direct sur l'infrastructure sous-jacente. Pour la plupart des boutiques mid-market, n8n Cloud est un point de départ correct qui peut migrer vers l'auto-hébergé plus tard si besoin.
L'alternative Lambda est un service Node.js ou Python complètement custom qui fait tourner la même logique. C'est l'option "build" dans la comparaison d'architecture d'intégration NetSuite. L'approche n8n est plus rapide à livrer (la canvas visuelle plus les nodes Shopify et HTTP pré-construits font gagner du vrai temps) et légèrement plus lente à l'échelle. Nous utilisons l'approche Lambda pour les clients qui scorent très haut en volume ou ont des exigences de performance très spécifiques.
Le workflow retry avec backoff exponentiel. Après trois échecs, l'événement part en dead-letter vers une table Postgres séparée et une alerte Slack se déclenche. La réconciliation horaire finira par rattraper la sync manquée, donc un downtime Shopify persistant cause un retard d'inventaire plutôt qu'une perte de données.
L'inventaire est basé sur les unités, pas sur les devises, donc le multi-devises n'est pas directement pertinent pour ce workflow. Le multi-taxes est aussi en dehors de la couche sync (géré à la couche commande). Le workflow synchronise les quantités d'inventaire physiques, qui sont indépendantes de la juridiction.
Pas directement, et c'est par design. La sync d'inventaire, c'est de la plomberie back-office. Les flux côté client (récupération, segmentation, personnalisation) sont une couche différente. Les deux partagent la même boutique Shopify mais adressent des problèmes différents. Gardez-les séparés ; l'architecture est plus propre comme ça.
Si vous cadrez une sync d'inventaire Shopify NetSuite et que le cadre ci-dessus vous a pointé vers le chemin n8n, c'est ce que couvrent ensemble nos pratiques automatisation workflow et intégration API. Nous avons livré cette architecture exacte pour des clients français, britanniques et canadiens, et la réalité d'ingénierie de la faire tourner en production a façonné chaque ligne du workflow ci-dessus. Si la question d'intégration plus large est "quel chemin est bon pour notre business spécifique", notre article architecture d'intégration Shopify NetSuite est le complément.

Un vrai cleanup client. Quatorze apps qui faisaient automatisation, panier abandonné, sync de stock et import d'avis. Trois workflows n8n les ont remplacées en deux semaines. Voici l'architecture, les chiffres, et ce que nous referions différemment.

La comparaison d'ingénieur sans parti pris pour intégrer Shopify et NetSuite. NetSuite Connector vs Celigo vs Boomi vs RESTlets sur mesure. Vrais modes d'échec, logique de mapping et décisions d'architecture qui décident si l'intégration tient à l'échelle.

Un guide de production pour déployer n8n sur AWS EC2 avec PostgreSQL, SSL, sauvegardes automatisées et résidence des données RGPD. Le setup réel que nous déployons pour nos clients européens, pas un tutoriel hello-world.