Builder Platform · Partner API

Build against your MixShift data and your connected Amazon accounts, behind one OAuth 2.0 gateway. early access

What is in the Builder Platform

Five surfaces, one credential. Everything below is included with your MixShift subscription.

Warehouse

Read-only SQL against your own tenant database, plus the contract-stable /v1 data endpoints for accounts, ads performance, retail, and Brand Analytics.

Live Amazon operations

Cataloged SP-API and Ads API calls on the authorization your accounts already granted MixShift, including audited writes. No developer application with Amazon.

Brand memory

The brand context your team maintains and the change log of what happened and why: launches, promotions, stockouts, and price tests, with corroboration.

Intelligence

MixShift Intelligence: finished reads served as data. Two services in one catalog: attribution (why sales and ad results moved, lost sales, the monthly read) and forecasting (whether a merchant has the history to be forecast at all).

MCP server

The same gateway as an MCP server, so Claude, ChatGPT, Cursor, Codex, and any other MCP client can work against your data in plain language.

New here, or want the plain-language version first? Builder Platform in the knowledge base walks through each surface without the reference detail.

Getting access

New to MixShift? Everything below assumes a MixShift account with activated data behind it. If you do not have one yet, create your account, then connect your Amazon accounts and activate ads + retail data (getting started guide). Most accounts are fully populated within 24-48 hours of activation; large catalogs can take longer. Then come back here to set up credentials.

Then pick your path, depending on whose data your code touches.

Guide: Get access

most customers

Building on your own MixShift account

You do not need to register an application. A tenant admin creates a service credential at /admin ("Create with raw secret"): you get a client_id and client_secret on the spot (the secret is shown once, at creation, so store it in your secret manager as you mint it). Exchange it for a bearer token with the client_credentials grant at POST /oauth/token and start calling.

Credentials are read-only by default (ads:write is grantable at mint time), revocable and rotatable at any time, and built for unattended use: schedulers, sync pipelines, CI, and multi-user apps that serve your own team.

Setting up the MixShift Claude plugin for unattended runs rather than your own code? Use the admin console's primary setup code flow instead: a one-time code the plugin exchanges itself, so no human ever handles the secret.

partner apps

Building a product that other MixShift tenants sign into

Then you need a registered partner application so each tenant can authorize you on a consent screen. Email support@mixshift.io with your app name, redirect URIs, and the scopes you need; you receive a client_id (and a secret for confidential clients) and use the authorization-code flow with PKCE below.

Coming from legacy direct MySQL access (IP allowlist + database credentials)? The service credential replaces that setup. The same SQL runs through POST /api/query, so nothing about your workflow changes except the connection. You drop the IP dependency (any host works, and no allowlist updates when your network changes), your server never holds a database password, and compromise recovery is fast: revoke the credential and any stolen access token expires within 12 hours, the current service-credential token TTL. Partner API sessions run the same 12-hour TTL.

OAuth 2.0

Send the access token as Authorization: Bearer <token> on every call.

Scopes

ScopeGrants
account:readAccount and merchant metadata.
retail:readThe Amazon SP-API surfaces: on-demand reports, pricing, and the operation catalog (orders, inventory, catalog, finances, Data Kiosk).
ads:readThe Amazon Ads API surfaces: profiles, reporting v3 and the unified Reporting v1, exports, entity lists, budget usage, recommendations, DSP reads, and ad-hoc Amazon Marketing Cloud SQL.
brand_analytics:readBrand Analytics data (rides the retail surface today).
sql:queryCaller-authored SQL reach over your tenant database: POST /api/query, GET /api/tables, GET /api/table/<name>, and the equivalent MCP tools. Bundled by default when a service credential is minted with no explicit scope list; request it explicitly alongside the domain reads if you pick a specific scope set instead. See Warehouse SQL.
context:readBrand context docs: the manifest and individual doc content per brand.
timeline:readThe brand timeline: events, stakes, and the read-time Amazon Ads change projection.
intelligence:readMixShift Intelligence: the request catalog, running a request, and fetching an async run (/api/intelligence/* and the intelligence_* MCP tools). Granted to interactive sign-ins by default; tenant admins grant it to a service credential at /admin. See MixShift Intelligence.
corrections:writeA person's own forecast corrections and settings (/api/app-state/forecasting/*, the month rows, account settings and sub-brand definitions a person enters): written under the person's name, into the one copy of the database's state the forecasting app reads. Granted to interactive sign-ins by default; tenant admins may grant it to a service credential. See Entering forecast corrections.
appstate:writeA MixShift app's own state (every kind, including its model runs and sub-brand definitions) on /api/app-state/*. Minted only, for the app's per-database credential; never on an interactive sign-in.
context:writeRetire or restore a brand for your whole team (POST /api/context/lifecycle or the context_set_brand_lifecycle MCP tool); see Retiring and restoring a brand. Grant it together with context:read: every brand context call checks that first, so context:write alone is refused. It also authorizes the doc sync and brand assignment changes the MixShift plugin uses, which are not published builder surfaces. Opt-in per credential, granted at mint time at /admin. Changes are recorded as the signed-in credential, never a value in the request, and nothing is deleted: a retired brand keeps its docs and history, and restore brings it back.
timeline:writeAppend timeline events (stakes, comments, action outcomes) and corroborate stakes. Opt-in per credential, granted at mint time at /admin. Appends are attributed to the signed-in credential, never to a value in the request.
ads:writeAudited Ads mutations: bids, placement and audience bid adjustments, budgets, states, negatives, and campaign, ad group and ad creation. Opt-in per credential: tenant admins can grant it when creating a service credential at /admin; partner applications get it case by case from MixShift. Every write defaults to a dry-run preview and is audit-logged.
listings:writeAudited listing-content updates on existing SKUs: title, Item Highlights, bullets, description, keywords, the catalog-compliance fields Amazon asks for in listing warnings (ingredients, item form), and the nine product image slots. Every change previews against Amazon first and commits only against the exact approved change set. Tenant admins can grant it when creating a service credential at /admin. See Listing content writes.

Raw-SQL warehouse access (POST /api/query) requires the full read set plus sql:query; see Warehouse SQL below.

Signing in from a claude.ai or Claude Desktop connector, or from MixShift added to ChatGPT as a custom app, grants every scope above, the same as signing in to the MixShift plugin: it is your own login reaching your own account. The consent screen says so before you approve, including that the connector or app can make changes to your Ads campaigns, listings, and brand context. A connector or app added before September 27 keeps the access it was first set up with until it is added again; see MCP server. Partner applications and service credentials still hold only the scopes granted to them.

Amazon operations

Live calls against the two Amazon APIs, on the authorization your accounts already granted MixShift. The operation catalogs are self-describing: list them to see every callable operation and its parameters.

Guide: Work with live Amazon operations

EndpointScopeWhat it does
GET /api/amazon/merchantsretail:readMerchants you can act for (one row per account + marketplace).
POST /api/amazon/reports + poll/documentretail:readOn-demand SP-API reports; documents return as short-lived presigned URLs.
/api/amazon/pricing/*retail:readFeatured Offer Expected Price + Competitive Summary batches; see Pricing batches for the sync size limits.
GET /api/amazon/spapi/operations
POST /api/amazon/spapi/call
retail:read
(+ listings:write for listing writes)
Catalog-driven SP-API operations: orders, FBA inventory, FBA inbound shipments, catalog items, fees, finances, product type definitions, seller promotions, Data Kiosk, and the guarded listing-content write. The operations listing documents every call.
GET /api/amazon/ads/profiles
GET /api/amazon/ads/operations
POST /api/amazon/ads/call
ads:read (+ ads:write for writes)Catalog-driven Amazon Ads operations: reporting v3, the unified Reporting v1 (cross-product history, including DSP), exports, entity lists, budget usage, recommendations, DSP reads, ad-hoc Amazon Marketing Cloud SQL, and audited writes including bid adjustments.

Inbound shipments to FBA. The SP-API catalog covers the inbound side of FBA and not only the sellable side, so you can see stock on its way to Amazon rather than just stock that already arrived: shipment status by pipeline stage (working, shipped, in transit, receiving, closed), the line items on a date range of shipments, and expected versus received quantities per SKU on one shipment. That is what lets your own tooling attribute an out-of-stock to receiving latency instead of to demand. These operations are read-only and they run on the authorization your accounts already granted MixShift, so there is nothing extra to request from Amazon.

Two details save you a failed first call. Both of the search-style inbound calls take an explicit query type (the per-shipment item call is keyed by shipment id instead and takes none), and the shipment search additionally needs a status list or a shipment id list, including when you are querying by date range. And treat paging as load-bearing here: the per-shipment item response can return a continuation token next to a short item list, so the first page is not necessarily the shipment's whole manifest. The operations listing carries the required parameters and the paging rules for each call.

Seller promotions (read-only)

Three SP-API catalog operations read a seller's own promotions live from Amazon's Promotions API (version 2025-12-01): deals, coupons, price discounts, and basket-building offers. They need only retail:read and run on the authorization your seller accounts already granted MixShift, so there is nothing extra to request from Amazon. Nothing here creates, edits, or cancels a promotion. Sellers only: a vendor merchant gets 403 (restricted_report), which is the API being seller-only and not a re-authorization problem.

OperationWhat it returns
promotions.search_promotionsPromotions for one marketplace, filtered by status, type (DEAL, COUPON, PRICE_DISCOUNT, BASKET_BUILDING), ASIN or SKU (up to 10 each), or start, end, and update dates. Each summary carries everything except the item list.
promotions.get_promotionFull detail for one promotionId: schedule, budget, benefit, purchase requirements, fees, and issues.
promotions.get_selectionThe promotion's item list, paged: ASIN, SKU, and each item's own discount and budget (set on DEAL and PRICE_DISCOUNT items).

The chain is search, then detail, then items: take promotionId from the search result, and for the item list take both selectionId and revisionId from the promotion's selection object. revisionId is required, and a promotion that is mid-edit has several revisions, so the wrong one returns the wrong item list.

Details that save a failed call. The marketplace is injected and takes exactly one. Date filters must be ISO 8601 with a timezone offset. Search matches only live revisions unless you pass revision as LATEST or ANY. Paging uses pagination.nextToken, passed back as paginationToken with the same other arguments; Amazon can return an empty page mid-stream, so keep paging until pagination is absent, and expect the pages to occasionally sum to less than totalResults. Limit is 1 to 100, default 20. List the operations for the full parameter text.

Pricing batches: sync size and async

Featured Offer Expected Price (keyed by SKU) and Competitive Summary (keyed by ASIN) each have a synchronous endpoint that returns the full response inline, and a /start endpoint that accepts any size, returns a runId, and works in the background. Poll GET /api/amazon/pricing/runs/<id> and fetch the per-item responses from GET /api/amazon/pricing/runs/<id>/result. Inputs are split into Amazon's own batches for you and paced, so you send one list.

OperationAmazon batch sizeMost items in one sync call
Featured Offer Expected Price40 SKUs200
Competitive Summary20 ASINs100

Competitive Summary batches are paced about 35 seconds apart, so 100 ASINs takes around two and a half minutes; a longer sync call would be cut off by the proxy, which is why the sync limit is 100 rather than 200. A sync call over its limit answers 400 too_many_items with max_items and items_count. That is a validation error that can never succeed on retry: send the list to the /start endpoint instead.

Creating a report: one attempt per call

reporting.create_report and reporting_v1.create_report are never retried automatically. Each call makes at most one create request to Amazon, because a server error or a lost response can arrive after Amazon accepted the report, and a second create would duplicate it. When the outcome is uncertain (a network failure, a timeout, a server error, or a success response with no readable body), the error carries "automaticRetry": false and "requestOutcome": "unknown", and no report id came back. Neither a timeout nor a 5xx proves that nothing was created, and what a resend does differs by API:

A 429 is a refusal rather than an uncertain outcome; it carries only "automaticRetry": false and keeps its usual advice (on v1, free a slot by deleting a report or waiting for one to finish).

Which merchant: sellerId and legacySellerId

Every call above acts for one merchant, and there are two ways to name one. Getting this wrong is a common first-call failure, so it is worth thirty seconds up front.

ParameterTypeWhat it is
sellerIdstringAmazon's own merchant token, the A... / ENTITY... string. Selects a merchant on its own only when the token maps to a single row. A seller trading in several marketplaces needs marketplace alongside it.
legacySellerIdpositive integerThe MixShift seller id. The durable unique key for exactly one merchant row (one seller in one marketplace). Takes precedence over sellerId and marketplace. Prefer it whenever you have it.
marketplacestringOptional hint (US, CA, MX, ...) used only to narrow a sellerId that matches more than one row.

Both values come back from GET /api/amazon/merchants, one row per account and marketplace. GET /api/amazon/spapi/operations also returns this contract as a merchantIdentifiers object next to the operation list, so a program never has to hardcode it.

There is no merchantId parameter. If you passed one it was ignored, and the call behaved as if you had named no merchant at all, which succeeds only when your credential can act for exactly one.

The same distinction reaches the warehouse. Seller-scoped tables generally carry both an integer SellerID and a varchar AmazonSellerID, and the indexes are usually built on the integer. On spapi_settlement, for instance, the integer SellerID is indexed together with the posted date, while the Amazon token is indexed with the settlement id instead. Filtering by the token plus a date range leaves the date with no usable index and turns a sub-second query into a table walk. Filter on the integer SellerID unless a table says otherwise.

Creating a complete Sponsored Brands ad

Deployed 2026-08-26. Building a full SB v4 ad from nothing takes an ad group plus one ad-type-specific create, because Amazon exposes a separate v4 endpoint per creative type instead of one shared ad shape. Build in order: sb.create_campaigns, then sb.create_ad_groups, then the create below for the ad type you want.

OperationAd typeCreative bodyLanding page
sb.create_ad_groupsn/a{ campaignId, name, state }. No bid: bids live on the keywords and targets created inside the ad group.n/a
sb.create_product_collection_adsPRODUCT_COLLECTION{ asins, brandName, brandLogoAssetID, brandLogoCrop, headline, customImages }Store
sb.create_brand_video_adsBRAND_VIDEO{ asins, videoAssetIds, brandName, brandLogoAssetID, brandLogoCrop, headline }Store
sb.create_video_adsVIDEO{ asins, videoAssetIds } only. No brand fields: sending them is the most common validation failure on this type.Product detail page
sb.create_store_spotlight_adsSTORE_SPOTLIGHT{ brandName, brandLogoAssetID, brandLogoCrop, headline, subpages }. No top-level asins: the advertised ASINs ride inside subpages[], one entry per Store subpage.Store subpages

Two acceptance rules Amazon enforces that this catalog cannot pre-flight for you. First, every asset id (videoAssetIds, brandLogoAssetID, customImages[].assetId) must already exist in the Creative Asset Library: there is no upload API on this surface, so a new video or logo goes in through the Amazon console first, then assets.list_recent or an existing ad's creative gives you the id. Second, on the three Store-landing types, every advertised ASIN must already appear on that Store page or Amazon rejects the ad; reading an ad already serving in the destination ad group (sb.list_ads) is the best available way to confirm this before a fresh build, since there is no cataloged pre-flight read for it yet.

Dry run does not test any of this. A dry run checks request shape and the per-operation required fields (the array is present, the 200-item cap, and every field the operation declares in requiredItemFields), and never reaches Amazon, so none of the creative or landing-page rules above are validated until you commit. requiredItemFields is served on every operation by GET /api/amazon/ads/operations and is the authoritative list, so you can validate a body before calling rather than learning the rule from a 400. It is what catches the Sponsored Brands trap: the v3-era keyword, target and negative-target UPDATES each need adGroupId alongside the entity's own id, and a negative target is identified by targetId, not negativeTargetId. Amazon validates per item, so commit one ad first and read its error result before sending the rest of a batch. And unless a user explicitly asked for the ad to go live immediately, create with state: "PAUSED": an ENABLED ad inside an enabled ad group and campaign starts serving, and billing, the moment the commit lands.

Placement and audience bid adjustments

Deployed 2026-09-13. sp.update_campaign_bid_adjustments and sb.update_campaign_bid_adjustments set the modifiers that used to be reachable only in the Amazon console: the percentage uplift a campaign bids by placement, and by audience cohort. They are split out of sp.update_campaigns / sb.update_campaigns rather than folded into them, so a modifier change is audited and metered as its own thing instead of arriving as an untyped campaign edit. The modifier object (dynamicBidding on SP, bidding on SB v4) is a required item field for that reason: an item without it is not a modifier change.

OperationPlacementsAudience cohorts
sp.update_campaign_bid_adjustmentsdynamicBidding.placementBidding[]: PLACEMENT_TOP, PLACEMENT_PRODUCT_PAGE, PLACEMENT_REST_OF_SEARCH, and SITE_AMAZON_BUSINESS, which is the Amazon Business (B2B) modifier riding the same array rather than a field of its own.dynamicBidding.shopperCohortBidding[]. strategy is optional here; leave it out to keep the campaign's current bidding strategy.
sb.update_campaign_bid_adjustmentsbidding.bidAdjustmentsByPlacement[]: TOP_OF_SEARCH, HOME, DETAIL_PAGE, OTHER. SB has no Amazon Business modifier.bidding.shopperCohortBidAdjustments[], alongside the bidOptimization setting they sit with.

Send the full set you want in force, and read current state first. Both operations replace the object you send: an entry you leave out does not keep its old value. Read the current modifiers from sp.list_campaigns / sb.list_campaigns (every campaign returns its dynamicBidding or bidding) and re-send the entries you are not changing. On SB the whole bidding object goes back, bidOptimization included, or a modifier edit silently flips the optimization setting with it.

Three more things worth knowing before the first call. percentage is a whole-number bid increase and 0 means no adjustment, so an SP modifier can never lower a bid. Audience ids come from the campaign list for cohorts a campaign already carries; adding a brand new audience needs an id from Amazon's audience discovery surface, which this catalog does not carry yet. And modifiers are not in the MixShift warehouse, so the live campaign list is the only current-state source: do not read them out of SQL. Performance by placement is a report, not a read, and it has its own trap: request it from reporting.create_report with groupBy set to campaignPlacement only, and ask for the placementClassification column explicitly. Adding campaign to groupBy can strip the placement rows entirely. These are ordinary ads:write operations, so they take the dry-run-by-default contract and the 200-item cap like every other write, and the current modifiers are snapshotted to the audit log before the change.

Creative Asset Library: assets.list_recent

Deployed 2026-08-28. POST /assets/search, profile-scoped, reads the newest page of a merchant's Creative Asset Library: asset ids, type, status, per-program eligibility, and file metadata.

Read it for what it actually is. It returns only the newest page (a few dozen rows) rather than the whole library, and despite its name, a filter in the body or query string is silently ignored: you get the identical page back with HTTP 200 whether or not you sent one, so never treat a result as filtered. Pagination is not currently usable: the response carries a token, but no way to spend it has been found. And the library is per-profile, not account-wide, so one merchant returning zero assets does not mean the account has none.

File metadata differs by asset type: a video asset populates resolutionWidth / resolutionHeight and durationInSeconds; an image asset populates width / height and aspectRatio. Use this operation to find the asset id of something just uploaded in the console, and to check which ad programs an asset already qualifies for (specCheckApprovedPrograms). It reads the library; it does not add to it.

Unified Reporting v1: history v3 does not have

Deployed 2026-09-13. Amazon's unified reporting API is cataloged as three operations on the same ads:read credential as everything else here: reporting_v1.create_report, reporting_v1.retrieve_report to poll, and reporting_v1.delete_report. It answers across ad products in one report, DSP included, and it is the reason to reach past reporting v3: v3 gives most report types a 95-day lookback and 31 days per request, while v1 retention runs to 24 months at daily grain and six years at monthly and yearly grain (25 months per report at monthly grain, six years at yearly). If you have been rebuilding long-run history out of the warehouse because the API would not give it to you, this is the surface that does.

It is addressed differently from the rest of the Ads catalog, which is the first thing that trips a caller. It is not profile-scoped: instead of a profile you name up to five distinct accounts in accessRequestedAccounts, as either an advertiserAccountId or a managerAccountId, from accounts.query_advertiser_accounts and accounts.list_manager_accounts. Discover DSP advertisers with accounts.list_manager_accounts. Each linked account carries two ids side by side: pass the numeric dspAdvertiserId as advertiserAccountId. The accountId next to it is a seat id, and Amazon rejects it in either slot with a 401 that reads like a permissions failure. A managerAccountId is accepted too, but in our testing 1 of 6 manager-scoped reports completed against 3 of 3 single-advertiser ones, so address advertisers directly. One report per create call, and one report id per poll.

Two shapes here will hand you a wrong answer rather than an error. First, time is a dimension in v1, not a timeUnit, and month.value and week.value carry no year. Over any window that crosses a year boundary the same month number from two years merges into one row with its metrics summed, silently. Request year.value alongside them, or report one calendar year per request. Second, a completed report arrives as completedReportParts, an array of presigned urls: the partitioned formats split deliberately, so code that reads the first part and stops truncates the report without failing. Iterate every part, and fetch each url with no Authorization header, which is enforced rather than merely advised.

Four more, each of which costs an afternoon the first time. Every call in this family answers HTTP 207 with { error: [], success: [] }, so a refused report arrives inside a 2xx: branch on a non-empty error, never on the status code. A 429 from create is a concurrency cap, not a rate cap, which inverts the usual advice: it means reports are already in flight, and backing off does not clear it. Free a slot with reporting_v1.delete_report or wait for one to reach a terminal status. The spend metric is metric.totalCost: there is no metric.cost, and like metric.sales it needs budgetCurrency.value in the same request (verified on Sponsored Ads; not yet confirmed for DSP). And DSP callers should not expect v1 numbers to tie out against v3: v1's base metrics include halo, so metric.purchases is v3's totalPurchases and metric.sales is v3's totalSales (the promoted-only equivalents are metric.purchasesPromoted / salesPromoted / roasPromoted), and v1 reports conversions on the traffic date where DSP v3 reported them on the conversion date. Field pairing, filterable fields, and the per-grain limits are carried per operation in the catalog itself; list GET /api/amazon/ads/operations for the full text before building against them.

DSP v1 reads

Deployed 2026-08-28. Six operations cover a DSP advertiser's campaigns, ad groups, and creatives, including where a creative is actually live: dsp.list_campaigns, dsp.list_ad_groups, dsp.list_ad_creatives, dsp.list_creative_associations, dsp.list_association_moderations, and dsp.list_eligible_creatives.

Those are structure reads. DSP performance is a report, and which report decides how far back you can see: dsp.create_report is the DSP-native path, and unified Reporting v1 is the one that reaches years rather than months. Check the metric definitions in that section before comparing the two, because v1 redefines the DSP purchase and sales metrics.

Discovery: a brand's DSP advertiser id comes from the advertising account surfaces (accounts.list_manager_accounts, linked accounts of type DSP_ADVERTISING_ACCOUNT), not from a Sponsored Ads profile id. It is the numeric dspAdvertiserId on that linked account, not its accountId, which is the seat id and is rejected. These operations carry detailed pagination and taxonomy notes in the operation catalog itself; list GET /api/amazon/ads/operations for the full text before building against them.

Amazon Marketing Cloud

Eight operations run ad-hoc AMC SQL on the same ads:read credential as everything else on this surface, and five more create and manage audiences. AMC is Amazon's clean room: you submit a SQL workflow against an advertiser's instance and Amazon returns aggregated results as short-lived CSV download urls. Reporting mutates nothing advertiser-facing, so it needs no write scope; audiences do (below).

Discovery runs in order, because a query needs both an instance and the entity id that reaches it: amc.list_accounts (no parameters) returns the AMC entity accounts and their marketplaces; amc.list_instances returns the instances for one entity and marketplace pair, and must be probed one pair at a time because parallel probing trips rate limits; amc.get_instance returns a single instance by id. Then the schema: amc.get_data_source for one table you can already name, or amc.list_data_sources to browse, which pages at 100 and returns every table with its full column list. Finally the run itself: amc.create_workflow_execution, amc.get_workflow_execution to poll, and amc.get_download_urls to fetch.

Audiences are the write side of AMC, and the one place this surface mutates something advertiser-facing, so they need ads:write and follow the dry-run then commit pattern of every other write here. amc.create_audience builds a rule-based audience from SQL over the instance's _for_audiences tables and activates it on one destination account, a DSP advertiser or a sponsored-ads entity; amc.list_audiences and amc.get_audience read status and Amazon's failure reason; amc.update_audience and amc.delete_audience work on FAILED audiences only, because a successful audience is immutable. The window you send is the lookback: Amazon applies it to every audience table before the SQL runs, so the query itself carries no date filter, and the service refuses before Amazon the things Amazon rejects after the run: a trailing semicolon, a refresh outside 0 to 21 days, interval date arithmetic, and a query that reads a paid dataset the instance does not have.

Whether an advertiser has AMC at all depends on their advertising login rather than on their MixShift account, so treat an empty amc.list_accounts as a normal answer and not an error. Some AMC tables are additionally gated behind paid Amazon subscriptions the advertiser holds directly, Amazon Retail Purchases among them, which is the long-window dataset behind lifetime value and cohort work. A schema read reports isPremium, and the instance carries an optionalDatasets array whose entries name each activated dataset and when it switched on. Query one an advertiser is not subscribed to and Amazon rejects it when it compiles the SQL, in a message that does not mention the subscription, so check before you build on it.

Two things worth knowing before the first query, because each returns a confident wrong answer rather than an error. timeWindowType defaults to a single day at Amazon, so a query written to cover months or years would answer for one day. This service therefore REQUIRES the field and refuses an execution that omits it, rather than letting the default decide: any of the three values is accepted, the point is that you chose one. Prefer UTC bounds, since a timezone west of UTC is converted forward and a window ending at local midnight reaches into the next UTC day. And AMC governs what a result may contain, not just what the SQL may say: each column carries a sensitivity, shopper-level identifiers can be joined and counted inside common table expressions but never returned, and rows built from too few distinct shoppers are dropped, so a short result can be a privacy floor rather than an absence of data. Submitting with dryRun validates in seconds instead of minutes. Every operation carries its parameters, its traps, and the dataset notes in the operation catalog itself; list GET /api/amazon/ads/operations for the full text before building against them.

MCP server

The same tool surface is exposed as an MCP server, so AI clients can use the warehouse and the Amazon surfaces directly. Two transports:

Guide: Connect AI tools over MCP (per-client setup for Claude, ChatGPT, Cursor, and Codex)

EndpointTransportUse with
POST /mcpStreamable HTTPCursor, Codex CLI and IDE, claude.ai, and any current MCP client.
GET /mcp/sseHTTP + SSE (legacy)Older MCP clients. Kept for compatibility.

Authenticate with Authorization: Bearer <access_token> on every request. Unattended setups mint from a service credential via client_credentials (see Getting access), while interactive users can manually mint a token at /login?mode=direct. Client setup examples for Cursor and Codex live in the auth setup guide. claude.ai and Claude Desktop connectors register automatically: add a connector with the server URL https://mcp.mixshift.io/mcp and sign in with your MixShift account when the OAuth window opens. For ChatGPT, turn on developer mode in ChatGPT's settings, then add MixShift as a custom app with the server URL https://mcp.mixshift.io/mcp and OAuth, and sign in when prompted. ChatGPT registers itself; there is no token to paste. A connector or app gets the same access as the MixShift plugin, write tools included. A claude.ai or Claude Desktop connector connected before September 27 keeps the read-only access it was approved with: remove the connector and add it again to get the full set. If you added MixShift to ChatGPT before September 27, add it again to get the full access: reconnecting keeps the access it was first set up with.

Tools cover the warehouse (legacy_query, table listing, describe), the Amazon SP-API and Ads surfaces (amazon_*), and the org-brain services: timeline_list, timeline_add_event, timeline_corroborate, context_list_docs, context_get_doc, and context_set_brand_lifecycle; and MixShift Intelligence: intelligence_catalog, intelligence_run, intelligence_poll, and intelligence_get. A credential may connect when it holds the scope for at least one tool, and each tool checks its own scope on every call: the SQL tools need the full read set plus sql:query, the timeline tools need timeline:read (writes also need timeline:write), the context tools need context:read (retiring or restoring a brand also needs context:write), and the intelligence tools need intelligence:read. A tool you lack the scope for returns a clear insufficient-scope result rather than data. Which scopes a token holds depends on how it was issued. A token minted at /login?mode=direct is an interactive sign-in and holds the same scopes as a MixShift plugin sign-in, context and timeline read and write included, and so does a sign-in from a claude.ai or Claude Desktop connector or a ChatGPT custom app (see above if yours was added before September 27). Service credentials and partner applications hold only the scopes granted to them.

MixShift Intelligence

Finished reads served as data, not rows to interpret. MixShift Intelligence is a catalog of named requests; each returns a JSON result your report, agent, or app can quote directly, with the arithmetic checked server-side. Open to every tenant by default and included in the subscription; every run is metered by request id so usage reports roll up per service.

The catalog is organized by service. Each request id carries its service prefix, and ids are append-only: a request never changes meaning, and a new capability is a new id.

ServicePrefixWhat it answers
AttributionINS-Why a number moved, decomposed into contributions that sum exactly to the change, with the caveats the engine raises on its own (dark periods, promotions, surge and matched windows, restatements, end-of-life exclusions). Five requests today, listed below.
ForecastingFCT-What should happen next, at a planned spend, from the merchant's own history. Three requests today: the history flag, the published baseline, and the report-month tracker, both served from the forecast the MixShift forecasting app publishes with its users' corrections applied; the plan solve joins the same catalog, gate, and metering when it ships. A forecast is never the brand's plan: the plan is the brand's own targets and budget; the forecast is the sanity check on the front end of planning and the operator's efficiency measure on the back end.
RequestWhat it answers
INS-OPS-BRIDGE-01Why ordered sales moved between two periods, item by item: traffic, conversion, price, and mix, bridged to the total.
INS-ADS-BRIDGE-01Why advertising results moved, at campaign, ad group, keyword or target, search term, or ASIN grain.
INS-DUO-01Both bridges on the same period, joined: TACOS, attributed share, paid pressure, and ad AOV against ASP.
INS-LOSTSALES-01Sales lost to stock-outs, sellable inventory, and weeks of cover.
INS-MONTHLY-01The monthly read: month over month and year over year, retail and advertising, in one bundle. The substrate of the Monthly Report Max plugin skill.
FCT-READINESS-01Is this merchant forecastable: months of sales and ad spend in the warehouse (one seller row, a brand scope of several rows summed month by month, or a sub-brand of one seller read by its labels), the trainable overlap against the served floors (12 complete months required, 18 recommended), the first month a request would read each verdict if both feeds keep producing, and what would close the gap, including what an Amazon Reporting v1 ad-spend backfill would add. Never fits a model; the history flag onboarding and the health check read first.
FCT-BASELINE-01The forecast baseline as the MixShift forecasting app published it, corrections included: the model, its monthly expectations and 95% ranges, the measured backtest, the readiness it trained on and the corrections it stands on, months keyed YYYY-MM, with the age of the published copy. Revenue or units; a merchant or one of its sub-brands. A units forecast is served as the forecast contract's units document (forecast_bridge.units_document/v1, contract 0.2.0). The answer carries spend_basis, ads_only or ads_plus_dsp, which says whether the spend the forecast stands on includes DSP spend typed into the forecasting app. Where nothing is published for a merchant or one of its sub-brands, a revenue baseline the gateway computes from the warehouse with the same engine the app runs, labelled source: computed, with its backtest and the corrections the gateway holds for the scope; a published copy always wins. Answers available: false with the reason when the forecast was withdrawn, it is below the served floor, or nothing is published and the warehouse history is below the floor.
FCT-TRACK-01The forecast figures for one report month in the monthly report's figure contract: the year to date, the month itself, the months still to come, and what the forecast stands on. For a closed month it serves two expectations, each under its own name: the forecast, made before the month closed, and the projection, from the current model, which has already seen the month. A beat or a miss is stated against the forecast, never the projection. Carries forecast_state (provided_current, stale or not_provided) so a report shows a forecast only when it is current. A units forecast is served as the units document (contract 0.2.0), with the same figure ids. The answer carries spend_basis (ads_only or ads_plus_dsp), and so does every spend or budget figure in it.
EndpointWhat it does
GET /api/intelligence/idsThe deployed catalog: id, version, content revision, service, and a one-line purpose.
POST /api/intelligence/run{ "id", "params" }. Params name the merchant (sellerId plus marketplaceId, or brand) and the period. Add "evidence": true for the What We Know statements. On the retail-backed requests a large account answers account_too_large_use_async; retry with "async": true and poll. Each account may hold two computing requests at a time; a third answers busy.
GET /api/intelligence/runs/<runId>Poll or fetch an async run you started.

Every result stamps the engine and evidence versions it was computed with, and the public GET /health reports the versions currently serving. Results carry a limitations list; read it before quoting figures. The same requests are available as the intelligence_* MCP tools and through the plugin command line (mixshift intelligence). Intelligence is not part of the /v1 partner contract: the result shape is versioned by the engine, not by the /v1 conventions.

Errors follow the { ok:false, kind, friendly } shape: not_enrolled (403) means MixShift switched Intelligence off for this account; bad_params (400); merchant_not_resolved (422); no_data_for_period (404); busy (429) when the service or this account is at its concurrency cap, retry shortly.

Entering forecast corrections (corrections:write)

A person's month-by-month corrections to the forecast (a sales or spend figure they know better than the model, a budget, a status) are stored by MixShift and applied to the forecasts served above. With corrections:write you can read and write them under your own name through these endpoints, and the forecasting app reads the very same rows. Interactive sign-ins hold the scope by default, and a tenant admin may grant it to a service credential at /admin.

EndpointWhat it does
GET /api/app-state/forecasting/doc?key=One document: key, kind, version, doc, updated_at, updated_by. 404 not_found when nothing is stored under the key.
GET /api/app-state/forecasting/docs?prefix=&scope_id=&after=&limit=Documents by key prefix, in key order. The prefix names one kind of one merchant, fb:<kind>:src:<database>:<merchant>, and so also matches that merchant's sub-brands; the optional scope_id keeps only the rows of exactly the scope it names (a merchant's leaves its sub-brands out). limit is 1 to 500 (200 by default); pass the returned next_after as after for the next page.
POST /api/app-state/forecasting/batchWrites, all or nothing.
GET /api/app-state/forecasting/scopes?legacy_seller_id=The sub-brands and item groups of your connected merchants, or of one of them: definitions, each with its scopeId and version.
POST /api/app-state/forecasting/scopesA new sub-brand or item group: { "create": { "legacySellerId", "kind", "name", "itemValues", "campaignValues" } } (or merchantId in place of legacySellerId). MixShift mints its key and answers definition (with its id) and version.
GET /api/app-state/forecasting/revisions?scope_id=&month=&after=The change log of a scope, one row per field that changed: id, scopeId, month, field, before, after, reason, actor, createdAt. month is optional and must be YYYY-MM-01; after is a revision id.

A batch body is { "writes": [ { "key", "expected_version", "doc" } ], "reason" }. reason is optional (one line, up to 500 characters) and lands in the change log. doc is the document to store, or null to delete it (404 not_found when nothing is stored there). Every write names the version it read in expected_version, and 0 means the key must not exist yet. If any key is stale, nothing is written and the answer is 409 conflict with a conflicts list naming every stale key (key, expected, actual). Read again, reapply your change, and resend. A batch holds up to 1,000 writes and 8 MB, one document up to 512 KB, and a key may appear once; over a size limit the answer is 400 too_large. A success returns results, one per write, with version and changed. A save that changes nothing writes nothing and does not advance the version.

What a person may write:

A month row carries scopeId and month (both must match the key), the fields sales, units, adSpend, adBudget and dspSpend, a status of auto, final or draft, and a note. Each money or unit field is { "mode", "value", "sourceObserved" } (adBudget has no sourceObserved), where mode is inherit, replace or suppress (dspSpend takes only the first two) and replace needs a value. A field you leave out of a write is kept as stored, not cleared: say inherit to clear it. MixShift stamps version, updatedAt and actor on the row and ignores any value you send for them. The change log records each change under the field names sales, units, ad_spend, ad_budget, dsp_spend and status.

A sub-brand or item group is a share of one merchant named by labels: { "id", "merchantId", "kind", "name", "itemValues", "campaignValues", "createdAt", "updatedAt" }, where kind is sub_brand or item_group, itemValues are the product labels (the brand on Seller Central items, the custom brand on Vendor Central items, or the item group) and campaignValues the campaign labels it counts, up to 100 a side. A side with no values holds nothing. Create one with POST /api/app-state/forecasting/scopes and forecast it with scopeKey on FCT-READINESS-01 and FCT-BASELINE-01; its month rows and settings live under its own scopeId, never the merchant's. Deleting a definition leaves its month rows; delete those in the same batch to remove its state. A forecast the forecasting app published for the sub-brand is still served until it is withdrawn in the app.

Your calls count as your account's API usage; they are not free the way MixShift's own app traffic is. A request that fails (status 400 or higher) is recorded but not charged. Other errors you can meet: 400 bad_params for a key or body that does not parse, and 503 unavailable when the database's home login could not be checked (retry).

Partner data plane: /v1

Guide: Use the /v1 data endpoints

The /v1 endpoints are the contract-stable partner surface: snake_case params, cursor pagination, explicit currency on money fields, and date windows anchored to the latest fully complete Amazon data day. Domain errors use { "error": { "code", "message" } } with a small stable code vocabulary (account_required, account_not_found, validation_error, rate_limited, ...).

EndpointScopeWhat it does
GET /v1/meaccount:readWho this token is: tenant id, person label, granted scopes.
GET /v1/accountsaccount:readEvery account/brand the tenant exposes, with per-domain auth status (retail, ads) and currency. Pass an account's id as account_id on data endpoints.
GET /v1/ads/account-summary
GET /v1/ads/account-pacing
GET /v1/ads/campaign-performance
GET /v1/ads/search-terms
GET /v1/ads/asin-performance
ads:readWindowed ads performance: account rollups, MTD pacing, and per-campaign / search-term / ASIN aggregates with cursor pagination.
GET /v1/retail/sales
GET /v1/retail/traffic
retail:readDaily retail series (sales, orders, units, sessions, page views) with window totals.
GET /v1/retail/inventory
GET /v1/retail/inventory-history
retail:readFBA stock position: current on-hand units, days of supply and Amazon's health alert per ASIN (a snapshot, so it carries data_through rather than a window), plus a daily on-hand series for trend and stockout work. Filter the snapshot with at_risk_only=true or days_of_supply_lte.
GET /v1/brand-analytics/search-query-performancebrand_analytics:readSearch Query Performance aggregated by query (weekly grain). search_query_volume is the query's total searches counted once per week; the three *_share fields are the seller's share of the query total in percent, computed from counts; metric_definitions in the response defines every field. Domains without data return 200 with an availability block.

The brand memory endpoints are /v1 too and follow the same conventions; they get their own section below.

Brand memory

The data endpoints above tell you what the numbers did. Brand memory is the part of the platform that knows why, and it is reachable with the same credential. Two halves: the brand context your team maintains (what a brand sells, how the account is structured, what normal looks like) and the change log of what actually happened to it, including the advertising changes committed through MixShift.

A stake is the change-log entry that says "this is a thing that changes how the numbers should be read": it carries a category (launch, promotional window, stockout, price test, media spike, brand migration, and more), a time window, and a required plain-language interpretation. Stakes can then be corroborated by someone else, so agreement is earned rather than asserted. This is what lets your own dashboard explain a spend spike instead of just drawing it.

Guide: Work with brand memory

EndpointScopeWhat it does
GET /v1/timelinetimeline:readThe brand timeline: native events plus the read-time Amazon Ads change projection, filterable by brand, family, kind, date window, and the stake facets (category, source, status, affects, tag, interval overlap, future). Cursor pagination via next_cursor.
POST /v1/timeline/eventtimeline:read + timeline:writeAppend one event: a structural stake (category plus interpretation), a comment, or an action outcome. Attributed to your credential. knowledge.* and server-reserved kinds are rejected.
POST /v1/timeline/event/<id>/corroboratetimeline:read + timeline:writeCorroborate a stake: append a corroboration and refresh the stake status and close date in one step.
GET /v1/context/docscontext:readThe brand context manifest: every doc per brand with its type, revision, and content hash. Each brand carries a lifecycle field (active or retired, and when, by whom, and through which surface it last changed), and the response carries retired_count. Retired brands are always listed here, never dropped.
GET /v1/context/doccontext:readOne context doc by brand and type (corpus docs also take a name), with content and revision.

These return the partner envelope like the rest of /v1: a bare success body, or { "error": { "code", "message" } } on failure. timeline:read and context:read come with a read-only credential; timeline:write is opt-in at mint time. Brand context docs are read-only on /v1: your team authors them through the MixShift plugin, and your integration reads them. The one brand context change a builder makes is retiring or restoring a brand, described next. The same capabilities are available as MCP tools (see MCP server).

Retiring and restoring a brand

A brand your team no longer works on can be retired for everyone on the account, and restored later. Retiring deletes nothing: the brand keeps its context docs, their revision history, and its change log, and reports and warehouse data are untouched. What changes is the brand's lifecycle state in the brand context listing. This write is not on /v1: call POST /api/context/lifecycle, or the context_set_brand_lifecycle MCP tool, which takes the same arguments and runs the same checks. Both need context:read and context:write.

FieldRequiredWhat it takes
brand_slugyesThe brand's slug as the listing shows it: lowercase letters, digits, and hyphens, up to 64 characters.
actionyesretire or restore.
reason_codenoclient_left, duplicate, superseded, or other.
notenoUp to 280 characters, one line of plain text.
on_behalf_ofnoUp to 254 characters, one line of plain text: who asked for the change. Recorded as given and never used as the actor.

A success is 200 { ok: true, brand_slug, state, changed, at, event_id? }, where state is "active" or "retired". changed: true means this call made the change, and at and event_id identify its change log event. Repeating the current state is a no-op: changed: false and no second event, so a retry is safe. Errors use the /api envelope (see Errors and limits), with the code in kind: 404 unknown_brand when the account has no brand context for that slug, 400 bad_params when the body breaks a rule above, and 403 insufficient_scope without the scopes. Through the MCP tool, a missing scope comes back in the tool result as error: "insufficient_scope" with the required_scopes listed, not as an HTTP 403. The change is recorded as the credential that made the call, never as a name in the body.

Reading the state. Every brand in the brand context listing carries a lifecycle object, and the listing carries retired_count, the number of retired brands.

KeyWhat it holds
stateactive or retired.
changed_atWhen the current state began.
changed_byWho the change was recorded as: a work email, an svc: label, or a client id. Print it as given.
surfaceWhere the change came from: plugin, mcp, api, partner:<id>, or mixshift-app.
reason_codeThe reason sent with the change, if any.

A brand that has never been retired reads state: "active" with the other four keys null. Which brands a listing includes depends on where you read it. GET /api/context/manifest and the context_list_docs MCP tool take lifecycle: active leaves retired brands out (retired_count still counts them) and all includes them. The default is all, so retired brands stay listed, marked retired, rather than dropped. GET /v1/context/docs always lists every brand and ignores the parameter. An integration that should skip retired brands filters on lifecycle.state.

Each change also lands on the brand timeline, so GET /v1/timeline and timeline_list return it as a structural.brand_retired or structural.brand_restored event, with reason_code, note, on_behalf_of, and surface in its payload. The timeline endpoints cannot append either kind: this route is the only way a brand's state changes.

Warehouse SQL: /api/query

Guide: Query the warehouse (includes migrating off direct MySQL access)

Read your warehouse directly with SQL when the /v1 endpoints do not cover what you need. Your credential resolves server-side to your own tenant database (the standard MixShift schema) over a read-only connection, so a query only ever sees your own data.

EndpointScopeWhat it does
POST /api/queryfull read set + sql:queryRun read-only SQL. Body { sql, params?, queryTimeoutMs? }; params is a positional array (? placeholders) or a named object. Returns { ok: true, rows, rowCount, durationMs } or { ok: false, kind, friendly }.
GET /api/tablesfull read set + sql:queryList the tables in your database.
GET /api/table/<name>full read set + sql:queryColumns for one table (a SHOW COLUMNS result).

Raw SQL spans every data domain, so this surface needs the full read set (account:read, ads:read, retail:read, brand_analytics:read) plus sql:query, the separate grant for caller-authored reach. Both are included by default when a service credential is minted with no explicit scope list; if you request a specific scope set instead, add sql:query explicitly, or the four domain reads alone will not reach this endpoint. Per-call limits: queryTimeoutMs defaults to 60s and caps at 120s; a response caps at 50,000 rows (too_many_rows) and 10 MB (response_too_large), so paginate large extracts with LIMIT / OFFSET or chunk by date. Raw SQL couples you to the warehouse schema; prefer /v1 when you want a versioned contract.

Named queries: /api/named-query

MixShift keeps a catalog of vetted warehouse queries on the server, so a client can ask for a result by id instead of carrying the SQL itself. The query text can be corrected or tuned without you shipping anything, and every result tells you which version answered it. The catalog ids are also published unauthenticated at /.well-known/mixshift-query-pack, so a client can check which ids exist before it holds a token.

EndpointScopeWhat it does
POST /api/named-queryfull read setRun one catalog entry. Body { id, params?, sellerIds?, queryTimeoutMs? }. Returns the same envelope as /api/query plus revision (a content hash of the SQL that ran, so a result stays attributable to exact query text) and applied_params. An id this deployment does not carry returns 404 with kind: "unknown_query".
GET /api/named-query/idsfull read setThe deployed catalog: every entry id with its current revision. Diff this against the ids you depend on before you ship, rather than discovering skew through a failed call.

Check applied_params before you treat a result as filtered. Entry parameter schemas are deliberately tolerant: a parameter the deployed entry does not declare is dropped and the query still runs, which is what lets your integration and this service deploy on separate schedules. The cost of that tolerance is that a filter we do not recognise is ignored silently and you still get ok: true. So every successful response carries applied_params, the sorted names of the parameters this execution actually bound. If a parameter you sent is missing from that list, it was dropped and your rows are unfiltered on that dimension. Names only, never values. Failed responses do not carry it: nothing ran, and the error kind already tells you why.

Sub-brand labels. Some sellers run several distinct brands under a single Amazon account, separated only by a brand label on the retail and ads records. The catalog carries discovery entries (sbd-01 retail, sbd-02 ads, sbd-03 vendor, sbd-04 cross-side match rate) that report the distinct labels per side, how much of the catalog or ad spend carries no label yet, and how well the retail and ads labels agree with each other. The entries behind brand context accept optional retail_brand_label and ads_brand_label filters. Both are optional and both default to no filter, which is exactly why applied_params matters here: it is how you confirm a label filter was honored rather than dropped, and that is the difference between one brand's numbers and the whole account's.

Sub-brand economics. Three more catalog entries rank labels by dollars instead of by count: sbd-05 retail (mws_items.Brand), sbd-06 ads (campaign.Brand), and sbd-07 vendor (vendor_items.CustomBrand). Each returns trailing 365-day revenue or ad spend per label, a trailing 90-day slice of it, and the last date the label sold or spent, one row per label per seller. Every label is included, including ones you may go on to treat as dormant, so totals built from these entries still reconcile against Seller Central, Amazon Ads, or Vendor Central. None take entry-specific params beyond the sellerIds every named-query call already sends.

Write safety model

Two write surfaces, two shapes. Both audit every change and both put a preview in front of it.

Amazon Ads writes (ads:write)

Listing content writes (listings:write)

Listing writes use a stronger two-call contract on listings.patch_listings_item, because the content is customer facing and Amazon applies parts of it asynchronously.

Images apply asynchronously, and that changes how you verify. An image value is a list of { marketplace_id, media_location } (no language_tag) where media_location is an https:// or s3:// URL Amazon can reach. Amazon accepts the patch, then fetches your URL and re-hosts the image itself, so a later read returns an m.media-amazon.com URL and never the URL you sent, and a fetch failure appears as a listing issue minutes later rather than in the patch response. Do not verify an image by comparing your URL to what a read returns, because it will never match. Verify that the slot's URL changed and that the listing's issues stayed clean.

ACCEPTED means Amazon accepted the submission for processing. It does not mean the change is published. Confirm publication by reading the listing (listings.get_listings_item with includedData=summaries,attributes,issues) and the shared catalog record (catalog.get_item). This surface deliberately does not poll Amazon on your behalf: your workflow decides how long to wait and what to do next.

A failed write does not always mean nothing happened. This is the part of the contract most worth building around, because getting it wrong duplicates a customer-facing change. Three failure kinds are ambiguous by nature, since none of them proves Amazon rejected your change before applying it: upstream_unavailable (Amazon's own server failed while processing the request), throttled (shed, but a throttle response can arrive after acceptance), and host_unreachable (no response at all, so the request may still have landed). An unclassified transport failure counts as ambiguous too, because the safe reading of "we do not know" is that it might have applied. Every other kind is a definitive Amazon-side rejection, and there "nothing was applied" is safe to assume. On the ambiguous three, verify before you act and never blindly re-send the mutation, or you can apply the same change twice.

You are not left to work that out alone. MixShift auto-retries a read through an Amazon server error and deliberately does not do the same for a write, because that retry is exactly the move that could double-apply. Instead the affected SKU comes back UNCERTAIN rather than failed, and the service performs one read-back against Amazon, comparing what is live now against both the value you asked for and the value it captured before the write, then reports per attribute what it could confirm. Everything confirmed reads back as ACCEPTED; anything it cannot confirm stays UNCERTAIN with a detail that says so; and a change set where some content did land never terminates as though nothing changed, so a partial outcome stays visible.

For text attributes, an inconclusive read-back leaves the change set resumable, and the safe next step is to re-run the commit with the same changeSetId: it re-reads and reconciles the same attempt instead of sending a second one. Image slots settle in that single pass rather than staying open, because Amazon fetches and re-hosts those asynchronously and a later read of the listing is the only real confirmation available. Either way, the rule for your integration is the same: on an ambiguous outcome, read before you write again.

Not yet available from the bundled mixshift CLI: use the REST endpoint or the amazon_spapi_call MCP tool. CLI support is coming.

Errors and limits

Guide: Limits and guardrails

Failures share one envelope: { "ok": false, "kind": "...", "friendly": "...", "message": "..." }. Branch on kind (insufficient_scope, reauth_required, throttled, merchant_not_found, ...), not HTTP status. friendly is a short summary safe to show a user; message carries the specific reason, so read it when you need to fix something. A rejected listing patch, for example, names the SKU, the attribute, the item index, and what was wrong with it.

Whose problem is it, and should you retry?

The single most useful thing the envelope tells you is whether the failure is yours, ours, or Amazon's. Retrying the wrong class of failure is the most common way to waste an afternoon, so the status code and the kind are chosen to answer that directly.

You getWhat it meansDo
400 bad_requestAmazon rejected your request. Your parameters. Carries Amazon's own status and amazon_error_code (InvalidInput, InvalidParameterValue, ...) plus its full response under responsePayload.Fix the request and send it again. Re-sending it unchanged will fail identically every time, so a blind retry loop never terminates. If our own docs led you here, tell us (below).
403 restricted_reportAmazon refused access. Usually a role or restricted-data grant, sometimes an authorization that predates a newer role.Do not retry. Re-authorize the merchant, or use a different operation.
409 reauth_requiredThe merchant's Amazon authorization was lost.Re-connect the account, then retry.
422 merchant_inactiveThe merchant is real and its authorization is fine, but it is not activated for Amazon Ads in MixShift, so Amazon will not serve its campaign data.Do not retry and do not re-authorize. This needs a person to activate the merchant in the MixShift platform. Until then every call against it fails identically, so skip it rather than running the rest of a change set against it.
403 profile_not_authorizedAmazon says the advertising login this merchant is connected through does not have access to it. The MixShift connection itself is working.Do not retry, and re-authorizing will not change it. Check that the advertising login has access to the advertiser in Amazon Ads, or ask us to re-map it.
429 throttledRate limited, ours or Amazon's.Back off and retry. Honour Retry-After.
502 upstream_unavailable / host_unreachableAmazon's side failed, or we could not reach it. We already retried reads.Retry later. For writes, check whether it applied first: a 5xx does not prove Amazon rejected it before acting.
500 unknownGenuinely unclassified, and now rare. Treat it as ours.Retry once; if it persists, report it.

Before this, an Amazon 400 came back as a 500. If you built retry logic against that, you can simplify it: a 4xx from us now means stop, and a 5xx means it may be worth trying again. merchant_inactive and profile_not_authorized are newer still, and they are a split rather than an addition: until 2026-09-15 an inactive merchant and an unmapped advertiser both arrived as 409 reauth_required, which sent callers into a re-authorize loop that could never succeed. If your integration branches on the status code rather than on kind, these two now land somewhere you may not handle.

Reporting an error (and why it is worth your 30 seconds)

If a call fails and our own documentation told you to make it that way, that is a bug on our side, not yours. The operation catalog is hand-maintained: a required parameter Amazon enforces but does not document, a casing rule, a combination that is only valid together. When our notes are wrong, every caller hits the same wall.

We see that a call failed and which operation. We do not see your parameters. So the fastest fix starts with you sending three things:

Send it to support@mixshift.io. Plugin users can run mixshift feedback instead, which attaches the context automatically. A recurring amazon_error_code on one operation is how a documentation gap gets found and fixed, and it usually turns into a catalog note the next caller never trips over.

Questions: support@mixshift.io · mcp.mixshift.io