Amazon BFF Routes
Routes Covered
Section titled “Routes Covered”| Route | Purpose |
|---|---|
POST /api/amazon/import | Single-item lookup by ASIN or Amazon product URL |
POST /api/amazon/search | Flexible free-text or identifier-based product search |
Both routes share the same { ok, data } wire envelope and authentication model. The inner data shape differs: /import returns a single AmazonImportDto; /search returns { items: AmazonImportDto[], totalResultsHint? }.
For SPA integration guidance (how to call these routes from UI components, error-handling patterns, debouncing, and productUrl preservation) see Amazon BFF Integration Guide (SPA).
POST /api/amazon/import
Section titled “POST /api/amazon/import”Purpose
Section titled “Purpose”POST /api/amazon/import is a server-only BFF route in arda-frontend-app that accepts an Amazon ASIN or product page URL, calls Amazon’s Creators API, and returns a stable v1 DTO representing the product. It is the data-acquisition endpoint for the “Create Item from Amazon URL” workflow (PDEV-446 / PDEV-445).
The route acts as a faithful proxy/adaptor for the Amazon Creators API GetItems response: it maps the upstream fields into a stable Arda DTO without imposing stricter field-presence requirements than Amazon does. Amazon’s GetItems responses are sparse for some ASINs (particularly digital items); nullable DTO fields are passed through as null rather than rejected. The caller receives the best information Amazon provides.
Authentication
Section titled “Authentication”Tenant-scoped Cognito JWT — identical to all /api/arda/* routes. The Authorization: Bearer <token> header is required. Anonymous requests are rejected with HTTP 401 to protect Arda’s Amazon API quota.
Request
Section titled “Request”POST /api/amazon/importContent-Type: application/jsonAuthorization: Bearer <cognito-id-token>Body:
{ "input": "<ASIN or Amazon product URL>" }input is required. Accepted forms:
| Form | Example |
|---|---|
| Bare ASIN (uppercase) | B08N5WRWNW |
| Bare ASIN (lowercase or mixed-case) | b08n5wrwnw, B08n5wrwnW |
Canonical product URL (/dp/) | https://www.amazon.com/dp/B08N5WRWNW |
Slug + /dp/ URL | https://www.amazon.com/Some-Product/dp/B08N5WRWNW/ref=sr_1_1 |
/gp/product/ URL | https://www.amazon.com/gp/product/B08N5WRWNW |
/gp/aw/d/ URL (mobile) | https://www.amazon.com/gp/aw/d/B08N5WRWNW |
Old-form /exec/obidos/ASIN/ URL | https://www.amazon.com/exec/obidos/ASIN/B08N5WRWNW |
Old-form /o/ASIN/ URL | https://www.amazon.com/o/ASIN/B08N5WRWNW |
| Schemeless URL | www.amazon.com/dp/B08N5WRWNW, amazon.com/dp/B08N5WRWNW |
Path-only (with leading /) | /dp/B08N5WRWNW, /gp/product/B08N5WRWNW, /Some-Product/dp/B08N5WRWNW |
Path-only (without leading /) | dp/B08N5WRWNW, gp/product/B08N5WRWNW, Some-Product/dp/B08N5WRWNW |
| Extended US sub-hosts | https://m.amazon.com/dp/B08N5WRWNW, https://smile.amazon.com/dp/B08N5WRWNW, https://read.amazon.com/dp/B08N5WRWNW (schemeless forms of these sub-hosts are accepted too — see the Schemeless URL row above) |
| Plain text containing exactly one ASIN | I need B08N5WRWNW please |
Arbitrary query parameters are stripped before ASIN extraction. Only US Amazon hosts are accepted (www.amazon.com, amazon.com, m.amazon.com, smile.amazon.com, read.amazon.com). When the input is plain text, extraction succeeds only when exactly one ASIN-shaped token is found — two or more distinct ASINs in the same text return UNRECOGNIZED_AMAZON_URL. The plain-text fallback fires only when no Amazon URL was parseable in the input; a pasted search URL such as https://www.amazon.com/s?k=B08N5WRWNW is rejected even though the query string contains an ASIN-shaped token.
Response — Success
Section titled “Response — Success”HTTP 200 vs HTTP 206
Section titled “HTTP 200 vs HTTP 206”HTTP 200 is returned when all four primary fields — name, image, price, and productUrl — are populated in Amazon’s response.
HTTP 206 (Partial Content) is returned when any of name, image, price, or productUrl is null. The DTO is still well-formed (nullable fields preserved); the 206 status signals to callers that Amazon’s response was sparse for this ASIN. unitCount, unit, and upc being null does NOT trigger 206 — those are routinely absent even on fully-cooperative Amazon responses (e.g. digital items and software).
Example full response (HTTP 200):
{ "ok": true, "data": { "name": "Some Product Name", "image": { "url": "https://m.media-amazon.com/images/I/...", "width": 500, "height": 500 }, "price": { "amount": 19.99, "currency": "USD", "displayAmount": "$19.99" }, "unitCount": 12, "unit": "32 oz", "upc": "012345678901", "asin": "B08N5WRWNW", "productUrl": "https://www.amazon.com/dp/B08N5WRWNW?linkCode=ogi&tag=ardacards-20&th=1&psc=1" }}Example sparse response (HTTP 206 — price and productUrl absent from Amazon’s upstream response):
{ "ok": true, "data": { "name": "Some Digital Product", "image": { "url": "https://m.media-amazon.com/images/I/...", "width": 500, "height": 500 }, "price": null, "unitCount": null, "unit": null, "upc": null, "asin": "B08N5WRWNW", "productUrl": null }}Output DTO Shape
Section titled “Output DTO Shape”The v1 DTO type is defined in arda-frontend-app/src/lib/shared/amazon/types.ts (AmazonImportDto).
| Field | Type | Nullable | Notes |
|---|---|---|---|
name | string | null | Yes | Item display title; null when Amazon’s response omits itemInfo.title |
image | { url: string; width: number; height: number } | null | Yes | Primary image at Large size; null when Amazon’s response omits images.primary.large. When present, url is Amazon-CDN-hosted and passed through unchanged |
price | { amount: number; currency: string; displayAmount: string } | null | Yes | Current Buy Box price; null when Amazon returns no Buy Box listing. When present, currency is "USD" for US-only v1; displayAmount is pre-formatted (e.g. "$19.99") |
unitCount | number | null | Yes | Integer pack count when Amazon exposes one (routinely absent for digital items) |
unit | string | null | Yes | Free-text size / unit-of-measure string (routinely absent for digital items) |
upc | string | null | Yes | Universal Product Code; first value when Amazon returns multiple (routinely absent for digital items) |
asin | string | No | 10-character Amazon ASIN — always populated |
productUrl | string | null | Yes | Amazon detailPageURL verbatim when present — see Affiliate-Tag Rule below; null when Amazon’s response omits the field |
Response — Errors
Section titled “Response — Errors”Error responses use stable machine-readable codes plus human-readable default messages. The SPA can override the message copy; the code field is the stable contract.
{ "ok": false, "code": "AMAZON_ITEM_NOT_ACCESSIBLE", "message": "The requested Amazon item is not available via the API."}Error Code Matrix
Section titled “Error Code Matrix”| Code | HTTP Status | When |
|---|---|---|
AUTHENTICATION_REQUIRED | 401 | Authorization header missing or JWT invalid/expired |
INVALID_REQUEST | 400 | Request body is not valid JSON, or the input field is missing or not a string |
UNSUPPORTED_SHORT_LINK | 422 | input is a string but the value is an a.co or amzn.to short link — redirect-following not supported in v1 |
UNRECOGNIZED_AMAZON_URL | 422 | input is a string but cannot be parsed as a recognizable ASIN or Amazon product URL |
UNSUPPORTED_AMAZON_LOCALE | 422 | input resolves to a non-US Amazon host (e.g. amazon.co.uk, amazon.de) |
AMAZON_ITEM_NOT_ACCESSIBLE | 404 | Amazon returned HTTP 404 ResourceNotFoundException — item not found or restricted |
AMAZON_API_THROTTLED | 429 | Amazon returned HTTP 429 ThrottleException — TPS/TPD quota exceeded |
AMAZON_API_UNAVAILABLE | 502 | Amazon API failure (5xx), network failure, credential misconfiguration, or eligibility loss |
The user-facing default message for UNRECOGNIZED_AMAZON_URL is: “We could not identify an Amazon Reference in your input.”
INVALID_REQUEST (HTTP 400) is the structural guard: it fires before any ASIN parsing and covers malformed JSON, missing input, or a non-string input value. UNRECOGNIZED_AMAZON_URL and the other 422s only fire when the request body is structurally valid.
AMAZON_API_UNAVAILABLE on a 401 or 403 from Amazon is alarm-worthy and indicates a credential or eligibility issue, not a transient failure.
POST /api/amazon/search
Section titled “POST /api/amazon/search”Purpose
Section titled “Purpose”POST /api/amazon/search is a server-only BFF route that accepts a flexible search input — free-text query, keyword list, category filter, Prime restriction, sort preference, or a list of bare ASINs / UPC/EAN/ISBN identifiers — and returns up to MAX_RESULTS = 10 matching AmazonImportDto items. It was introduced in PDEV-457 as the sibling route to /import.
The route has four internal dispatch paths depending on the input content; callers do not need to know which path was taken — the response shape is always { items: AmazonImportDto[], totalResultsHint? }.
Authentication
Section titled “Authentication”Tenant-scoped Cognito JWT, identical to /api/amazon/import. The Authorization: Bearer <token> header is required.
Request
Section titled “Request”POST /api/amazon/searchContent-Type: application/jsonAuthorization: Bearer <cognito-id-token>Body:
{ "query": "string", // Optional when keywords[] is non-empty; otherwise required "keywords": ["string"], // Optional additional search terms "categories": ["string"], // Optional category restrictions (e.g. "HomeGarden") "primeOnly": false, // Optional; default false "sortBy": "relevance" // Optional; "relevance" | "price-low-to-high"; default "relevance"}Validation Rules
Section titled “Validation Rules”| Field | Constraint | On Violation |
|---|---|---|
query | Optional when keywords[] is non-empty after filtering; otherwise required. Trimmed; length ≤ 1024 chars (post NFC normalisation). At least one of query or keywords[] must produce non-empty content — categories[] and primeOnly alone do not satisfy Amazon’s minimum-search-term requirement. | INVALID_SEARCH_INPUT |
keywords | Optional; array of strings; at most 20 entries after empty-entry drop; each entry at most 64 chars. | INVALID_SEARCH_INPUT |
categories | Optional; array of strings; at most 5 entries; each entry at most 64 chars. The first entry is used as the Amazon category restrictor (searchIndex); remaining entries are appended to keywords. | INVALID_SEARCH_INPUT |
primeOnly | Optional boolean. | INVALID_SEARCH_INPUT |
sortBy | Optional; must be "relevance" or "price-low-to-high". | INVALID_SEARCH_INPUT |
All strings go through a defensive filter before use: Unicode NFC normalisation, control-character replacement, HTML angle-bracket replacement, and internal-whitespace collapse.
Dispatch Paths
Section titled “Dispatch Paths”After input validation, the route dispatches to one of four internal paths. The caller’s response shape is identical regardless of which path was taken.
| Input Content | Dispatch | Notes |
|---|---|---|
A single bare ASIN or Amazon product URL in query | GetItems shortcut (single ASIN) | Uses the strict extractAsin function; no keyword search overhead |
Multiple ASINs / URLs in query separated by whitespace, commas, or semicolons — every token resolves to an ASIN | GetItems batch (up to BATCH_ASIN_MAX = 10 ASINs) | Enables paste-a-list-of-products lookup; exceeding 10 ASINs returns INVALID_SEARCH_INPUT |
All tokens in query match identifier patterns (UPC-A ^\d{12}$, EAN-13 ^\d{13}$, EAN-8 ^\d{8}$, ISBN-10 ^\d{9}[\dX]$) | SearchItems identifier mode — tokens joined with |, SearchIndex=All, results filtered by exact external-ID match | ”Scan a list of barcodes” import through the same route |
| Anything else (free text, mixed ASIN + keywords, etc.) | SearchItems keyword search | Default path; query + keywords[] joined and sent as SearchItemsRequestContent.keywords |
A short-link (a.co, amzn.to) or non-US-locale Amazon URL in query surfaces UNSUPPORTED_SHORT_LINK or UNSUPPORTED_AMAZON_LOCALE immediately, matching the behaviour of /import.
Silent Query Relaxation
Section titled “Silent Query Relaxation”When the first SearchItems call returns zero items and the ASIN-shortcut or identifier paths were not taken, the route silently retries up to RELAXATION_MAX_RETRIES = 2 additional times, each time dropping one constraint, before returning items: []. The retry ladder is:
- Original call with all constraints.
- Drop
deliveryFlags(Prime restriction) if it was set. - Drop the category restrictor (
searchIndex/browseNodeId) if one was active.
The total budget across all relaxation attempts is RELAXATION_TIME_BUDGET_MS = 1500 ms; the route stops early if this is exceeded. Relaxation is always on and not a contract surface — callers receive items: [] whether or not relaxation ran. Keywords are never mutated.
Response — Success
Section titled “Response — Success”{ "ok": true, "data": { "items": [ /* AmazonImportDto — up to MAX_RESULTS = 10 */ ], "totalResultsHint": 1234 }}items is always an array (never absent); on zero matches it is []. totalResultsHint is an optional convenience copy of Amazon’s “total results” indicator and may be absent on zero-match responses.
The route always returns HTTP 200 on a valid request, including when items is empty. There is no HTTP 206 equivalent for /search.
Each element of items has the same AmazonImportDto shape as /import — see the Output DTO Shape table above.
Response — Errors
Section titled “Response — Errors”The error envelope is identical to /import:
{ "ok": false, "code": "INVALID_SEARCH_INPUT", "message": "The search input is invalid."}Error Code Matrix
Section titled “Error Code Matrix”| Code | HTTP Status | When |
|---|---|---|
AUTHENTICATION_REQUIRED | 401 | Authorization header missing or JWT invalid/expired (emitted by the Next.js handler before the route module is invoked) |
INVALID_REQUEST | 400 | Request body is not valid JSON |
INVALID_SEARCH_INPUT | 400 | Request body is valid JSON but fails search input validation (missing search terms, field too long, wrong type, too many ASINs, etc.) |
UNSUPPORTED_SHORT_LINK | 422 | query contains a short-link URL (a.co, amzn.to) |
UNSUPPORTED_AMAZON_LOCALE | 422 | query contains a non-US Amazon URL |
AMAZON_API_ERROR | 502 | Creators API call failed (network error, throttling, 5xx) — all SDK error types collapse to this code in v1 |
Zero matching items is not an error — the route returns HTTP 200 with items: [].
Creators API Field Mapping
Section titled “Creators API Field Mapping”The route reads the following Creators API resource paths to populate the DTO. Only the v1 minimum set is requested:
itemInfo.titleitemInfo.productInfoitemInfo.externalIdsimages.primary.largeoffersV2.listings.priceoffersV2.listings.isBuyBoxWinnerFull mapping table:
| DTO field | Creators API source path |
|---|---|
name | itemInfo.title.displayValue |
image.url | images.primary.large.url |
image.width | images.primary.large.width |
image.height | images.primary.large.height |
price.amount | offersV2.listings[0].price.money.amount |
price.currency | offersV2.listings[0].price.money.currency |
price.displayAmount | offersV2.listings[0].price.money.displayAmount |
unitCount | itemInfo.productInfo.unitCount.displayValue |
unit | itemInfo.productInfo.size.displayValue |
upc | itemInfo.externalIds.upcs.displayValues[0] |
asin | asin (top-level) |
productUrl | detailPageURL (top-level) — verbatim pass-through |
Configuration Touchpoints
Section titled “Configuration Touchpoints”Environment Variables
Section titled “Environment Variables”Four server-side environment variables are required. They are aggregated through src/lib/env.ts. All have mock-value fallbacks in mock mode.
| Variable | Description | Source |
|---|---|---|
AMAZON_CREATORS_CREDENTIAL_ID | OAuth2 client ID | 1Password vault Arda-*OAM → Amplify env var |
AMAZON_CREATORS_CREDENTIAL_SECRET | OAuth2 client secret | 1Password vault Arda-*OAM → Amplify env var |
AMAZON_CREATORS_CREDENTIAL_VERSION | Credential version string (e.g. "3.1") | 1Password vault Arda-*OAM → Amplify env var |
AMAZON_ASSOCIATE_TAG | Amazon Associates partner tag | 1Password vault Arda-*OAM → Amplify env var |
These variables must be present in the Amplify app’s branch environment before either route becomes operational. They are added to the env | grep allowlist in amplify.yml so Amplify writes them to the build’s .env.
MARKETPLACE Constant
Section titled “MARKETPLACE Constant”The US marketplace identifier "www.amazon.com" is a source constant defined in src/lib/shared/amazon/constants.ts. It is not a runtime env var — v1 supports US only.
Amplify Allowlist
Section titled “Amplify Allowlist”amplify.yml includes these four variable names in the env | grep filter:
-e AMAZON_CREATORS_CREDENTIAL_ID -e AMAZON_CREATORS_CREDENTIAL_SECRET -e AMAZON_CREATORS_CREDENTIAL_VERSION -e AMAZON_ASSOCIATE_TAGAffiliate-Tag Rule
Section titled “Affiliate-Tag Rule”When productUrl is non-null, it is Amazon’s detailPageURL passed through unchanged. Amazon embeds Arda’s partnerTag and tracking parameters (linkCode, language, th, psc) into this URL; these must not be stripped, rebuilt, or modified. Verbatim Amazon guidance (API Rates page, 2026-05-07):
“You are using the links provided by Creators API when linking back to Amazon. Do not edit any of the URL parameters.”
When productUrl is null, Amazon’s response did not include detailPageURL for this ASIN. Neither route synthesises a fallback URL.
The buildAffiliateUrl() utility in src/lib/shared/amazon/affiliate-url.ts constructs a minimal URL from an ASIN for cases where no Creators API response is available (e.g. PDEV-429 cart-link work) — it is not used to produce productUrl in v1.
Source Layout
Section titled “Source Layout”src/ lib/ shared/amazon/ types.ts # AmazonImportDto + AmazonImportErrorCode types constants.ts # MARKETPLACE constant asin.ts # extractAsin() (strict) + extractAsinLenient() affiliate-url.ts # buildAffiliateUrl() — fallback builder (not used for productUrl) server/ lib/amazon/ creators-client.ts # fetchCreatorsApiItems(), fetchCreatorsApiSearch() — SDK wrappers asin-multi-token.ts # classifyMultiTokenInput() — multi-ASIN tokenizer category-resolver.ts # resolveCategory() — static SearchIndex lookup identifier-mode.ts # classifyIdentifierTokens(), filterByExternalIds() item-mapper.ts # mapItemToDto() — SDK Item → AmazonImportDto search-constants.ts # MAX_RESULTS, BATCH_ASIN_MAX, RELAXATION_* etc. search-filter.ts # filterSearchInput() — defensive input normalisation search-relaxation.ts # runWithRelaxation() — silent zero-result retry search-request-builder.ts # buildSearchRequest() — BFF input → SDK request shape routes/amazon/ import.ts # importFromAmazon() — orchestration + DTO mapping search.ts # searchAmazon() — orchestration + dispatch app/ api/amazon/ import/ route.ts # Next.js Route Handler for /api/amazon/import search/ route.ts # Next.js Route Handler for /api/amazon/searchConstraints Not Surfaced in the DTO
Section titled “Constraints Not Surfaced in the DTO”- No in-process response caching. Each request triggers a fresh Creators API call. The IP License permits caching non-image content for up to 24 hours; v1 opts for no cache to keep the implementation simple.
- Image URL is not re-hosted.
image.urlis Amazon’s CDN-hosted URL passed through unchanged. The IP License prohibits storing or caching images. Downstream callers must respect the 24-hour caching ceiling on stored URLs. - No pagination.
/searchalways returns the first page, capped atMAX_RESULTS = 10. The request/response contract is designed to be extended with optional pagination fields in a non-breaking way. - Eligibility risk. Creators API access is conditional on generating qualified referring sales every 30 days. See the Amazon Creators API Onboarding runbook for monitoring guidance.
Copyright: © Arda Systems 2025-2026, All rights reserved