Amazon Seller Central
Mercentia’s Amazon integration ships against the modern JSON-only SP-API (the legacy XML POST_PRODUCT_DATA flat-file Feeds API was deprecated 31 July 2025). Two workers cover the two flows:
amazon-listing-sync— per-product PUT against/listings/2021-08-01/items/{sellerId}/{sku}.amazon-order-import— order pull from/orders/v0/orderswith LWA refresh-on-401.
A /channels/amazon dashboard page surfaces the connect form, sync controls, and live status.
Architecture
┌─────────────────────┐ ┌──────────────────────┐ ┌────────────────────┐
│ Dashboard │ POST │ Gateway │ enqueue│ Worker │
│ /channels/amazon │────────▶│ /api/v1/amazon/ │────────▶│ amazon-listing- │
│ │ │ sync-products │ BullMQ │ sync │
│ │ │ import-orders │ │ amazon-order- │
└─────────────────────┘ └──────────────────────┘ │ import │
└─────────┬──────────┘
│
▼ HTTPS
sellingpartnerapi-
{eu,na,fe}.amazon.com
The gateway never calls Amazon directly — every write fans out through BullMQ. Idempotent retries, rate-limit backoff, and LWA refresh live in the worker.
Connecting a merchant
The merchant needs:
- An SP-API developer profile at https://developer.amazon.com/.
- An LWA refresh token for the seller account (long-lived; rotated only when the seller revokes access).
- The seller ID and marketplace ID(s).
Dashboard: Channels → Amazon → Set up. The form captures sellerId + marketplaceId + region + refresh token and POSTs to /api/v1/amazon/connect:
POST /api/v1/amazon/connect
x-store-id: <uuid>
{
"sellerId": "A1B2C3D4E5F6G7",
"marketplaceId":"A1F83G8C2ARO7P", // UK
"marketplaceIds":["A1F83G8C2ARO7P"], // optional multi-marketplace
"refreshToken": "Atzr|IwEBI...",
"accessToken": "Atza|IwEBI...", // optional; worker refreshes on 401
"region": "eu" // eu | na | fe
}
Credentials are stored in the integrations table with provider='amazon_sp_api', JSON-encrypted at rest.
Marketplace IDs (commonly used)
| Marketplace | ID |
|---|---|
| United Kingdom | A1F83G8C2ARO7P |
| United States | ATVPDKIKX0DER |
| Germany | A1PA6795UKMFR9 |
| France | A13V1IB3VIYZZH |
| Italy | APJ6JRA9NG5V4 |
| Spain | A1RKKUPIHCS9HS |
| Canada | A2EUQ1WTGCTBG2 |
| Australia | A39IBJ37TRP1C6 |
| Japan | A1VC38T7YXB528 |
Gateway endpoints
All require x-store-id header.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/amazon/connect |
Persist credentials (see above). |
GET |
/api/v1/amazon/status |
Connection state + last sync. |
POST |
/api/v1/amazon/sync-products |
Fan out one amazon-listing-sync job per active product. |
POST |
/api/v1/amazon/sync-product/:id |
Re-sync one product. |
DELETE |
/api/v1/amazon/listings/:productId |
Delist (worker calls DELETE on the SP-API). |
POST |
/api/v1/amazon/import-orders |
Trigger an amazon-order-import run. Optional body { sinceIso } overrides the watermark. |
POST |
/api/v1/amazon/disconnect |
Flip integration status='disconnected'. Credentials retained for re-enable. |
Listing sync — amazon-listing-sync
Per-product PUT to /listings/2021-08-01/items/{sellerId}/{sku}.
Operations
upsert— buildsproductType + attributesJSON from the Mercentia product row, PUTs to the marketplace endpoint. Idempotent — same SKU on retry produces the same listing.delete— DELETE; 404 from Amazon is treated as success.
SKU strategy — defaults to the Mercentia product id (truncated to 40 chars, Amazon’s cap). Merchants can override via product.sku.
Rate limiting — Amazon’s Listings API allows 5 req/sec per seller with burst of 10. Worker concurrency is hard-capped at 4 in apps/worker/src/worker.ts; horizontal replicas share the BullMQ queue so global rate naturally tracks deployment size. 429 responses honour Retry-After, exponential backoff up to 5 attempts.
Payload structure — the worker emits the universally-required subset of attributes (item_name, product_description, brand, condition_type, fulfillment_availability, purchasable_offer with our_price.value_with_tax as a decimal). Each attribute is the SP-API’s standard [{ value, marketplace_id, language_tag? }] envelope.
Test coverage — apps/worker/src/jobs/amazon-listing-sync.test.ts covers payload structure, GTIN handling, brand omission rules, condition defaulting, productType fallback, and title truncation.
Order import — amazon-order-import
Pulls from /orders/v0/orders with LastUpdatedAfter = integrations.lastSyncAt (24h fallback if null).
Idempotency — Mercentia’s orders table has no natural unique index on (storeId, source, externalId), so the worker runs a pre-insert SELECT to skip already-imported Amazon order ids. Re-running the import is safe.
LWA refresh on 401 — when Amazon returns 401, the worker:
- POSTs to
https://api.amazon.com/auth/o2/tokenwithgrant_type=refresh_token+ the stored refresh token +AMAZON_LWA_CLIENT_ID+AMAZON_LWA_CLIENT_SECRETenv vars. - Persists the new access token onto the integration row.
- Retries the original request once.
After that, throws so BullMQ’s retry policy applies.
Rate limit — Amazon’s Orders endpoint is ~1 req/min sustained per seller with burst of 20. Worker concurrency is hard-capped at 1 in worker.ts — any horizontal parallelism would burn 429s for every replica.
Watermark — on success the watermark advances to the most-recent LastUpdateDate observed across the batch (not “now”) so the next run doesn’t re-fetch already-imported orders.
Order header → Mercentia mapping
| Amazon field | Mercentia field |
|---|---|
AmazonOrderId |
order_number + external_id |
OrderStatus |
status (Pending → pending, Unshipped/Partial → open, Shipped → fulfilled, Canceled → cancelled) |
BuyerInfo.BuyerEmail |
email (synthesised to <orderId>@marketplace.amazon if absent — common for anonymised marketplaces) |
OrderTotal.Amount × 100 |
total_amount (rounded to minor units) |
OrderTotal.CurrencyCode |
currency |
ShippingAddress |
shipping_address JSONB |
PurchaseDate |
paid_at |
FulfillmentChannel, IsPrime, IsBusinessOrder, MarketplaceId |
metadata JSONB |
Line items — NOT pulled in this initial pass. Amazon’s /orders/v0/orders/{id}/orderItems endpoint is rate-limited separately; the order header (totals + addresses + status + buyer) lands in this pass. A follow-up enrichment worker will fill order_items rows once volume justifies the complexity.
Hard page cap — 20 pages × 100 orders = 2k orders per run. Enough for any merchant’s catch-up window; larger backfills require multiple runs.
Environment variables
| Variable | Tier | Notes |
|---|---|---|
AMAZON_LWA_CLIENT_ID |
required in prod (worker) | LWA client id from the SP-API developer console |
AMAZON_LWA_CLIENT_SECRET |
required in prod (worker) | LWA client secret |
The gateway boots without these — only the worker needs them, and only when an Amazon merchant exists. env-preflight marks them as recommended so a non-Amazon deployment doesn’t fail boot.
Monitoring
- BullMQ admin (
/api/v1/admin/queues/amazonListingSyncand/api/v1/admin/queues/amazonOrderImport) shows queue depth, failure rate, last error. - DLQ monitor (
apps/worker/src/jobs/dlq-monitor.ts) raises a Sentry alert at >1k stuck jobs and FATAL at >5k. - Per-product status (last submission id + approval state) lives in
integrations.credentialsunderchannelProductIds.
Limitations & roadmap
- Inventory updates — currently flow through the same listing-sync path (the
fulfillment_availabilityattribute carries the live cross-warehouse quantity). A dedicated quantity-only sync (cheaper than full payload) is on the roadmap. - Order items — see above. Pulled by header today, line items in a follow-up.
- Returns/refunds — Amazon return events arrive via
/orders/v0/orders/{id}/returnsand/finances/v0/financialEvents. Not yet imported; the storefront’s standard returns workflow is the workaround. - Buy Box pricing — not yet automated. Merchant sets the price; Amazon’s Buy Box algorithm decides eligibility.
Related
apps/worker/src/jobs/amazon-listing-sync.ts— listing sync handler.apps/worker/src/jobs/amazon-order-import.ts— order import handler.apps/gateway/src/routes/amazon.ts— merchant control surface.apps/dashboard/pages/channels/amazon.vue— UI.- Channel sync architecture — how the multi-channel workers share an enqueue pattern.