Pushing data
Your connector decides when to synchronise and pushes to the API. You keep control over the cursor, the frequency and error recovery, and you have no route to expose: a full backfill on first connection, then incremental passes at your own pace.
The thirteen entities
| Entity | Endpoints | Business key |
|---|---|---|
| Customer groups | POST · PUT · DELETE | group_id |
| Customers | POST · PUT · DELETE | customer_id |
| Customer addresses | POST · DELETE | address_id |
| Newsletter subscribers | POST · PUT | email |
| Orders | POST · DELETE | order_id |
| Order statuses | POST · DELETE | status_id |
| Carriers | POST · DELETE | carrier_id |
| Products | POST · PUT · DELETE | product_id |
| Variations | POST · DELETE | variation_id |
| Product images | POST · DELETE | image_id |
| Categories | POST · PUT · DELETE | category_id |
| Manufacturers | POST · PUT · DELETE | manufacturer_id |
| Vouchers | POST · PUT · DELETE | voucher_id |
Carts are not in the list: they are not created through the data API. → Tracking script
Dependency order
An entity that references another must be pushed after it, otherwise the reference is orphaned. ShopiMind applies this order in three waves, and you should do the same for an initial backfill:
Wave 1 ─ no dependencies
customer groups · newsletter subscribers · statuses · carriers
categories · manufacturers
Wave 2 ─ depends on wave 1
customers (← groups) products (← categories, manufacturers)
Wave 3 ─ depends on wave 2
addresses (← customers) orders (← customers, products)
variations (← products) images (← products) vouchers (← customers)Within a wave entities are independent: parallelise freely.
Bulk writes
Every bulk* write takes an array and is asynchronous: a 200 confirms queueing, not read visibility. Allow a few seconds.
import { SpmProducts } from '@shopimind/sdk-js';
const res = await SpmProducts.bulkSave(client, products, { chunk: true });
if (!res.ok) throw new Error(res.error.message);{ chunk: true } splits automatically according to each resource's maximum batch size and aggregates the counters. Without the SDK, respect the sizes documented per endpoint in the API reference: beyond them the whole batch is refused with a 400.
Idempotence
Always push by upsert on a stable business key. A bulkSave with an already-known customer_id updates; it does not duplicate. That is what makes a resynchronisation safe, and you will need it the day a backfill fails halfway.
Consequence: your business key must be stable over time. Do not use an identifier that changes when the merchant renames a product or merges two records.
Incremental synchronisation
The pattern that works:
- Full backfill on first connection, in wave order.
- Then incremental passes filtered on your last-modified date.
- Keep a cursor per entity, on your side.
Only advance the cursor after a fully successful pass
If you advance the cursor while a page failed, that page's objects will never be resynchronised , you lose data silently. The cursor must only move after a complete success; on partial failure, replay from the previous cursor (this is safe, upserts are idempotent).
A few minutes of overlap on the cursor avoids missing objects modified during the pass itself.
Partial rejections
A batch containing invalid objects still returns 200: valid objects go through, the others are rejected individually.
{
"statusCode": 200,
"sent_count": 18,
"rejected_count": 2,
"rejected_items": [
{
"index": 4,
"item": { /* the EXACT object you sent */ },
"errors": [
{ "field": "email", "message": "Validation failed: email must be an email" }
]
}
]
}The original payload comes back in item: log that, not just the index, so you can replay or fix.
The "whole batch invalid" case
If every object in a batch is refused, the response is still 200 with sent_count: 0 and the full detail, there is no 400. This is deliberate: it prevents a synchronisation from looping forever on the same page. So do not read "HTTP 200" as "nothing to do".
Format conventions
These are the official modules' conventions. Following them avoids most rejections.
| Topic | Convention |
|---|---|
| Dates | ISO 8601 with microseconds and the real offset: 2026-08-04T11:42:07.000000+02:00. The offset must be the shop's, not Z, unless the shop really is on UTC. |
| Identifiers | Always sent as strings, never numbers, even when your database stores integers. |
| Amounts | Numbers at the currency's precision (2 for EUR/USD, 0 for JPY, 3 for BHD). |
| Languages | ISO 639-1 (fr, en). |
| Currencies | ISO 4217 (EUR, USD). |
| Booleans | JSON booleans, not 0/1 nor "true". |
Pace and volume
- Quota: per API key, shared across all
/v1endpoints. The counter resets on each civil minute. - Prefer batches: a
bulkSaveof 50 objects costs one request, not fifty. - On overrun, apply exponential backoff. The SDK does this by default (3 retries on 408/429/5xx and network errors).
Next
→ Tracking script: for carts and visitor behaviour.