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/orders with 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:

  1. An SP-API developer profile at https://developer.amazon.com/.
  2. An LWA refresh token for the seller account (long-lived; rotated only when the seller revokes access).
  3. 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 — builds productType + attributes JSON 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 coverageapps/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:

  1. POSTs to https://api.amazon.com/auth/o2/token with grant_type=refresh_token + the stored refresh token + AMAZON_LWA_CLIENT_ID + AMAZON_LWA_CLIENT_SECRET env vars.
  2. Persists the new access token onto the integration row.
  3. 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/amazonListingSync and /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.credentials under channelProductIds.

Limitations & roadmap

  • Inventory updates — currently flow through the same listing-sync path (the fulfillment_availability attribute 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}/returns and /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.