Sync lifecycle
A sync is not a push — it is a reconciliation. What you send updates, what you stop sending goes to zero, and what stays at zero long enough eventually leaves the platform.
Anatomy of a run
- Fetch your source. Whatever your catalog of record is — a database, a supplier API, a file.
- Resolve. Turn your values into BrandsGateway term IDs. Unmapped values are recorded as failures, not guessed at.
- Validate. Run each payload through the schema before it leaves your process.
- Diff. Skip anything byte-identical to what you last sent successfully.
- Upsert. Send what changed, paced at roughly two per second.
- Reconcile. Zero anything BrandsGateway holds that your feed no longer offers.
- Report. Every send is recorded for you — the outcome of each product, with its reason, on your dashboard.
Upsert semantics
The endpoint resolves {external_id} against your
vendor's products. If it finds one, the request is an update; if not,
a create. You never need to track BrandsGateway's product IDs at all:
your own external id is the only identity the portal asks for, on every
call.
images is dropped from update requests unless you pass
&force_images=true, because re-uploading unchanged
imagery is the single largest cost in a sync. Everything else in the
payload is applied as sent — including omissions, so always send the
complete product rather than a patch.
Change detection
Sending your entire catalog every run wastes hours and produces no change. Fingerprint the payload and skip what matches. This is exactly what BrandsGateway's own importers do, and it is why a 5,000-product vendor typically writes only a few hundred products per run.
// Change detection: hash the payload you WOULD send and compare it
// with the hash of the payload you last successfully sent.
// Most catalogs change by well under 20% between runs.
import { createHash } from "node:crypto";
const fingerprint = (payload) =>
createHash("md5").update(JSON.stringify(payload)).digest("hex");
for (const product of catalog) {
const payload = transform(product);
const hash = fingerprint(payload);
if (hash === lastSent.get(payload.meta_data_external_id)) {
record("unchanged", payload); // nothing sent, no rate limit spent
continue;
}
await send(payload);
lastSent.set(externalId, hash);
}
Report the skipped products as unchanged to telemetry.
Otherwise the dashboard shows a catalog that appears to have shrunk.
Going out of stock
A product leaves the customer-facing catalog the moment its stock hits zero — but the record survives. Visibility and deletion are separate events, months apart. This matters because it means:
- You never delete a product to take it down. You set its stock to zero.
- A product that comes back into stock is republished automatically, keeping its URL, its history and its reviews.
- Dropping a product from your feed without zeroing it leaves phantom stock the shop will happily sell.
// Closing the loop: what you sent last run, minus what you just sent,
// is what needs zeroing. Keep the previous run's ids yourself — reading
// your catalog back from the portal is not available yet.
const sent = new Set(catalog.map((p) => p.externalId));
const departed = [...lastRunExternalIds].filter((id) => !sent.has(id));
for (const externalId of departed) {
await api(`/api/v1/products/${externalId}`, {
method: "PUT",
body: { product: { type: "simple", manage_stock: true, stock_quantity: 0 } },
});
record("zeroed", externalId);
}
Archival
A product that has been out of stock and untouched for 90 days is marked pending archival; shortly after, it is copied to long-term storage and removed from the platform. For a variable product, every variation must be out of stock to qualify.
Restocking a pending-archival product republishes it. And because
BrandsGateway remembers the _external_id of archived
products, sending one again after archival restores it rather than
creating a stranger — provided you kept the identifier stable.
Cadence
- Stock and price: as often as your source changes. Hourly is common; more frequent is fine if you are only sending changes.
- Full reconciliation: at least daily, so departed products are zeroed promptly.
-
Imagery: only with
force_images=true, and only when it genuinely changed.
Never run two syncs for the same vendor concurrently. Take a lock at the start of a run and release it at the end — overlapping runs produce contradictory stock and confusing telemetry.