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
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.
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
GET /oauth/authorize— authorization-code flow with PKCE. Tenants sign in and consent.POST /oauth/token— exchange codes, refresh tokens, or mint machine tokens viaclient_credentials.POST /oauth/revoke— revoke a refresh token when a user disconnects (RFC 7009).- Discovery:
/.well-known/oauth-authorization-server.
Send the access token as Authorization: Bearer <token> on every call.
Scopes
| Scope | Grants |
|---|---|
account:read | Account and merchant metadata. |
retail:read | The Amazon SP-API surfaces: on-demand reports, pricing, and the operation catalog (orders, inventory, catalog, finances, Data Kiosk). |
ads:read | The 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:read | Brand Analytics data (rides the retail surface today). |
sql:query | Caller-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:read | Brand context docs: the manifest and individual doc content per brand. |
timeline:read | The brand timeline: events, stakes, and the read-time Amazon Ads change projection. |
intelligence:read | MixShift 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:write | A 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:write | A 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:write | Retire 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:write | Append 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:write | Audited 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:write | Audited 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
| Endpoint | Scope | What it does |
|---|---|---|
GET /api/amazon/merchants | retail:read | Merchants you can act for (one row per account + marketplace). |
POST /api/amazon/reports + poll/document | retail:read | On-demand SP-API reports; documents return as short-lived presigned URLs. |
/api/amazon/pricing/* | retail:read | Featured Offer Expected Price + Competitive Summary batches; see Pricing batches for the sync size limits. |
GET /api/amazon/spapi/operationsPOST /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/profilesGET /api/amazon/ads/operationsPOST /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.
| Operation | What it returns |
|---|---|
promotions.search_promotions | Promotions 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_promotion | Full detail for one promotionId: schedule, budget, benefit, purchase requirements, fees, and issues. |
promotions.get_selection | The 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.
| Operation | Amazon batch size | Most items in one sync call |
|---|---|---|
| Featured Offer Expected Price | 40 SKUs | 200 |
| Competitive Summary | 20 ASINs | 100 |
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:
- Reporting v3 (
reporting.create_report). Amazon answers an identical request that is already in flight with425, and the message names the originalreportId: poll that report instead of creating another. Resending the identical body once is therefore the way to recover the id. We measured this once, with the first report still pending; whether it still holds after the first report has completed is not established, so a200with a new id cannot be told apart from a first create that never landed. - Reporting v1 (
reporting_v1.create_report). Amazon does not deduplicate. An identical resend created a second report, each holding a concurrency slot (also measured once), and the catalog has no operation that lists reports, so a lost one cannot be looked up. Resend at most once: if the first did land, it is an orphan that holds a slot until it finishes. A later429may be that orphan, so wait for in-flight reports to finish rather than resending again.
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.
| Parameter | Type | What it is |
|---|---|---|
sellerId | string | Amazon'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. |
legacySellerId | positive integer | The 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. |
marketplace | string | Optional 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.
| Operation | Ad type | Creative body | Landing page |
|---|---|---|---|
sb.create_ad_groups | n/a | { campaignId, name, state }. No bid: bids live on the keywords and targets created inside the ad group. | n/a |
sb.create_product_collection_ads | PRODUCT_COLLECTION | { asins, brandName, brandLogoAssetID, brandLogoCrop, headline, customImages } | Store |
sb.create_brand_video_ads | BRAND_VIDEO | { asins, videoAssetIds, brandName, brandLogoAssetID, brandLogoCrop, headline } | Store |
sb.create_video_ads | VIDEO | { asins, videoAssetIds } only. No brand fields: sending them is the most common validation failure on this type. | Product detail page |
sb.create_store_spotlight_ads | STORE_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.
| Operation | Placements | Audience cohorts |
|---|---|---|
sp.update_campaign_bid_adjustments | dynamicBidding.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_adjustments | bidding.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)
| Endpoint | Transport | Use with |
|---|---|---|
POST /mcp | Streamable HTTP | Cursor, Codex CLI and IDE, claude.ai, and any current MCP client. |
GET /mcp/sse | HTTP + 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.
| Service | Prefix | What it answers |
|---|---|---|
| Attribution | INS- | 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. |
| Forecasting | FCT- | 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. |
| Request | What it answers |
|---|---|
INS-OPS-BRIDGE-01 | Why ordered sales moved between two periods, item by item: traffic, conversion, price, and mix, bridged to the total. |
INS-ADS-BRIDGE-01 | Why advertising results moved, at campaign, ad group, keyword or target, search term, or ASIN grain. |
INS-DUO-01 | Both bridges on the same period, joined: TACOS, attributed share, paid pressure, and ad AOV against ASP. |
INS-LOSTSALES-01 | Sales lost to stock-outs, sellable inventory, and weeks of cover. |
INS-MONTHLY-01 | The 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-01 | Is 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-01 | The 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-01 | The 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. |
| Endpoint | What it does |
|---|---|
GET /api/intelligence/ids | The 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.
| Endpoint | What 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/batch | Writes, 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/scopes | A 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:
- Three kinds. A month row, keyed
fb:override:<scopeId>:<YYYY-MM-01>, an account setting, keyedfb:account:<scopeId>, and a sub-brand definition, keyedfb:scope_def:<scopeId>. AscopeIdreadssrc:<database>:<merchant>, with:scope:<key>added for a sub-brand. Any other kind, in a read or a write, answers403 insufficient_scope; those belong to MixShift's own app. - Your own database and a connected merchant. One batch names exactly one database.
A scope in another database answers
403 scope_not_yours, and a merchant that is not connected to your account answers403 merchant_not_connected. - You are the actor. The row, the history and the change log carry the name on
your sign-in.
on_behalf_ofis refused with400, and so is a session that carries no name: sign in again. - One copy of the state per database. If it already lives under another login that
your request cannot write, nothing is written and the answer is
409 state_home_elsewhere. Retrying will not help; ask MixShift to check the database.
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, ...).
| Endpoint | Scope | What it does |
|---|---|---|
GET /v1/me | account:read | Who this token is: tenant id, person label, granted scopes. |
GET /v1/accounts | account:read | Every 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-summaryGET /v1/ads/account-pacingGET /v1/ads/campaign-performanceGET /v1/ads/search-termsGET /v1/ads/asin-performance | ads:read | Windowed ads performance: account rollups, MTD pacing, and per-campaign / search-term / ASIN aggregates with cursor pagination. |
GET /v1/retail/salesGET /v1/retail/traffic | retail:read | Daily retail series (sales, orders, units, sessions, page views) with window totals. |
GET /v1/retail/inventoryGET /v1/retail/inventory-history | retail:read | FBA 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-performance | brand_analytics:read | Search 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
| Endpoint | Scope | What it does |
|---|---|---|
GET /v1/timeline | timeline:read | The 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/event | timeline:read + timeline:write | Append 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>/corroborate | timeline:read + timeline:write | Corroborate a stake: append a corroboration and refresh the stake status and close date in one step. |
GET /v1/context/docs | context:read | The 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/doc | context:read | One 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.
| Field | Required | What it takes |
|---|---|---|
brand_slug | yes | The brand's slug as the listing shows it: lowercase letters, digits, and hyphens, up to 64 characters. |
action | yes | retire or restore. |
reason_code | no | client_left, duplicate, superseded, or other. |
note | no | Up to 280 characters, one line of plain text. |
on_behalf_of | no | Up 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.
| Key | What it holds |
|---|---|
state | active or retired. |
changed_at | When the current state began. |
changed_by | Who the change was recorded as: a work email, an svc: label, or a client id. Print it as given. |
surface | Where the change came from: plugin, mcp, api, partner:<id>, or mixshift-app. |
reason_code | The 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.
| Endpoint | Scope | What it does |
|---|---|---|
POST /api/query | full read set + sql:query | Run 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/tables | full read set + sql:query | List the tables in your database. |
GET /api/table/<name> | full read set + sql:query | Columns 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.
| Endpoint | Scope | What it does |
|---|---|---|
POST /api/named-query | full read set | Run 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/ids | full read set | The 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)
- Write operations default to a dry run: the call validates, snapshots current state, logs an audit row, and returns a preview without touching Amazon.
- Only an explicit
"dryRun": falsecommits. Show your user the preview first. - Commits are capped at 200 items per call and audit-logged with the pre-write snapshot.
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.
- Preview first. Send
"mode": "preview"with achangeSetof up to 10 SKUs. The service snapshots the current values of exactly the attributes you are changing, runs Amazon's real validation without mutating anything (mutatedAmazon: false), and returns Amazon's issues, achangeSetId, and achangeSetHashthat expires in 24 hours. - Commit against the approval, not against content.
"mode": "commit"takes only thechangeSetIdandapprovedChangeSetHash. The service replays the stored, previewed payload, so a commit cannot apply anything your user did not see. A mismatched, expired, or already-used approval is refused. - Drift fails closed. If a value you are replacing changed on Amazon between the
preview and the commit, that SKU is refused as
STALErather than overwritten. Re-preview to pick up the new baseline. - Only content attributes, whole-attribute only. Three classes are writable: text (item name, Item Highlights, bullets, description, keywords), product data (ingredients, item form), and images (main plus the eight other image slots). Identifiers, brand, compliance and safety attributes, variations, price, quantity, fulfillment, and deletes are all refused.
- Per-SKU results. A multi-SKU change set is reviewed as one set and reported per SKU, so a partial outcome is visible instead of averaged away.
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.
- Rate limit: 240 requests/minute per credential (HTTP 429 +
Retry-Afterwhen exceeded). - Access tokens are short-lived: 12 hours for a service credential's
client_credentialsmint and for a partner API session (the authorization-code token a partner app holds on a tenant's behalf), 24 hours for interactive sign-in. Refresh viaPOST /oauth/tokenbefore expiry. - Amazon-side throttles surface as
kind: "throttled"; back off and retry.
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 get | What it means | Do |
|---|---|---|
400 bad_request | Amazon 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_report | Amazon 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_required | The merchant's Amazon authorization was lost. | Re-connect the account, then retry. |
422 merchant_inactive | The 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_authorized | Amazon 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 throttled | Rate limited, ours or Amazon's. | Back off and retry. Honour Retry-After. |
502 upstream_unavailable /
host_unreachable | Amazon'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 unknown | Genuinely 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:
- the operation id (e.g.
fulfillment_inbound.get_shipments), - the
amazon_error_codeandmessagefrom the envelope, - the parameters you sent, minus anything you consider sensitive.
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