Epinium API
Access: Settings → API Keys
Epinium exposes your data through a REST API so you can read it from your own tools: a custom dashboard, an analysis script or an AI assistant. It is the same information you see in the app, with the same figures.
The API reads your data and writes to Amazon and to your stores: it queries catalogue, campaigns and metrics, and what is inside your Shopify and WooCommerce stores; it creates and edits Sponsored Products campaigns and edits Sponsored Brands and Display ones; it creates and edits portfolios; it creates, edits, clones and runs workflows; it creates, resolves and applies tasks; and it creates, edits and deletes products and content in your connected Shopify and WooCommerce stores. Your Amazon catalogue only changes when an approved task is applied; your stores' catalogue changes directly, with no task. Each area has its own permission, and you grant the write ones when you create the key.
Looking to use it from an AI assistant?
You do not need to write any code. The Epinium MCP connects this same data to Claude and other compatible assistants.
Generate a token
- Go to Settings → API Keys
- Click Create API key and give it a name that reminds you what it is for
- Tick the permissions it needs
- Copy the key and store it somewhere safe
- Use it in the
Authorizationheader of every request
The key is shown only once
The full value appears a single time when you create it. If you lose it there is no way to recover it: you have to revoke it and create another.
Permissions
Every token carries the permissions you tick when creating it. Grant only the ones your integration needs.
| Permission | Gives access to |
|---|---|
catalog:read | Products, brands, countries and clusters, with their metrics: sales, Amazon fees and margin, returns, stock and, for Vendor, reviews |
campaigns:read | Campaigns, portfolios, ad groups, targets, product ads and search terms |
workflows:read | Your workflows, their configuration and which campaigns they reach |
connections:read | Your account's connections: Amazon, Shopify and WooCommerce |
tasks:read | Epinium tasks and their items |
skills:read | Epinium's catalog of validated marketing playbooks |
platform:read | What is inside your Shopify and WooCommerce stores: products, stock, prices and the rest of what each store exposes, read live from its own API. It includes the record of the changes made in them (/v1/platform-changes, 1 credit per row returned) |
catalog:write | Create, edit and delete keyword clusters: their name, their positive and negative keywords and the products they group. Nothing reaches Amazon |
campaigns:write | Create and change campaigns, portfolios, ad groups, targets and product ads on Amazon, and configure what the optimizer chases on each one |
platform:write | Create, edit and delete products and content in your connected Shopify and WooCommerce stores, directly and with no review first, through the same route used to query them (POST /v1/platform-request): a mutation in Shopify, or a non-GET method in WooCommerce, with the Idempotency-Key header. It also needs platform:read (the Edit level on the API keys screen grants both). Every change is recorded for 12 months and costs the same as a query: 5 credits per call plus 1 per KB returned |
tasks:write | Create tasks, approve or reject their suggestions, close them and apply what was approved |
workflows:write | Create, copy, edit, switch on, pause, delete and restore workflows, and run or simulate them |
Writing is for the Business and Master plans. On Free and Guru tokens read: you cannot create a key or authorise an assistant with write permissions, and if you move down to one of those plans, the ones you already had keep reading but their writes are refused with a 403.
A token reaches all the connections in your account. If you are an agency, it reaches the connections each client shared with you when accepting your invitation.
If you are an agency: pick the account on every call
An agency token reaches several accounts, so every request has to say which one it operates on. Use the Epinium-Account: <account id> header, or the account_id parameter if you are calling through the MCP.
/v1/me and /v1/accounts are the exception: they answer without picking an account, and they are exactly the ones that tell you which accounts you reach and what they are called.
Once the account is picked, you see all of it: its products and campaigns are scoped to the connections it shared with you, and its account-level resources — workflows, tasks and clusters, which hang off no connection — are scoped to the account. Anything outside your reach is not filtered out silently: an account_id you were not invited to returns an explicit error, not an empty list.
What endpoints it covers
Every resource has a list endpoint, and most also let you retrieve a single item by its id. The metrics resources only have a list: an aggregated row over a date range has no identifier of its own.
Catalog — catalog:read
| Endpoint | What it returns |
|---|---|
/v1/products | Your unified products |
/v1/amazon-seller-products | The Seller Central view of each product, and the category of its rank with expand=salesRanks |
/v1/amazon-vendor-products | The Vendor Central view, the category of the rank with expand=salesRanks and the reviews with expand=customerFeedback |
/v1/amazon-advertising-products | The Advertising view |
/v1/seller-product-metrics | Sales, sessions, Buy Box, rank, Amazon fees, margin, returns, stock, B2B and days of coverage per product |
/v1/vendor-product-metrics | Vendor sales per product, with manufacturing and sourcing, returns, sell-in with Amazon, inventory and days of coverage |
/v1/product-brands | Your brands |
/v1/countries | Countries and marketplaces |
/v1/clusters | Your keyword clusters; with catalog:write you can also create and edit them |
Advertising — campaigns:read
| Endpoint | What it returns |
|---|---|
/v1/campaigns | Your Amazon Advertising campaigns |
/v1/campaign-metrics | Spend, sales, ACOS and ROAS per campaign |
/v1/portfolios | Your portfolios and the budget cap each one puts on its campaigns |
/v1/adgroups | The ad groups of each campaign |
/v1/adgroup-metrics | Performance per ad group |
/v1/targets | Keywords and product targets |
/v1/target-metrics | Performance per target, with its keyword and match type |
/v1/product-ads | The link between an ad group and the product it advertises |
/v1/product-ad-metrics | Performance per advertised product, with its ASIN |
/v1/search-terms | The actual shopper queries |
/v1/search-term-metrics | Performance per query, with the text and the target that matched it |
/v1/campaigns/{id}/optimization-history | What the automation changed on a campaign, with the net effect |
/v1/advertising-changes | Every change Epinium has pushed to Amazon, whatever set it off |
Automation — workflows:read
| Endpoint | What it returns |
|---|---|
/v1/workflows | Your workflows, with their schedule and whether they are active |
/v1/workflows/{id} | The full configuration of one: its diagram and every variable with its effective value |
/v1/workflows/{id}/campaigns | Which campaigns it actually reaches, and why each one is in |
/v1/workflows/{id}/performance | The performance of those campaigns, with their target next to it so you can judge it |
A workflow can be scheduled every night and still touch no campaign at all: /campaigns is what surfaces that, and when the set comes back empty it names the reason.
Connections, tasks and skills
| Endpoint | Permission | What it returns |
|---|---|---|
/v1/connections | connections:read | Your connections: Amazon, Shopify and WooCommerce |
POST /v1/platform-request | platform:read, and platform:write to write | A query to one of your stores or, with platform:write, a change to it: GraphQL on Shopify, REST on WooCommerce |
/v1/platform-request/docs/{platform} | platform:read | That platform's API version, its reference and the fields your store has right now |
/v1/tasks | tasks:read | Epinium tasks |
/v1/task-items | tasks:read | The items of each task |
/v1/skills | skills:read | Epinium's catalog of marketing playbooks |
The skills catalog is content written by Epinium, not data from your account: it is the same for every client. Which parts of each playbook you receive depends on your plan.
Queries to your stores return no personal data: names, emails, phones and addresses are refused before the request reaches the store. They are charged by the size of the response, and so are changes. These two routes are not in the browsable reference: the usual way to use them is through the Epinium MCP.
There is also /v1/me, which tells you which permissions and which accounts your token reaches. It is the first useful call to check that the key works.
Three rules when reading the data
These three explain almost every question about interpreting the response:
- Monetary amounts are text, not numbers.
"cost": "8.22"arrives as a string on purpose, so it does not lose precision when converted to a binary decimal. - Ratios come as fractions, not percentages. An ACOS of 90.83% arrives as
0.908287. Multiply by 100 to display it. nullis not0.nullmeans Amazon did not report that figure;0means an actual zero. Anacosofnullis a campaign that spent without selling, not free advertising. Andnew_to_brand_salesarrives asnullon Sponsored Products because Amazon does not measure it for that format.
Profitability, stock and reviews of your products
Product metrics go beyond sales: they also tell you how much Amazon keeps, how much stock is left and what buyers think. Four things before drawing conclusions:
- Seller fees and margin exclude VAT, while
salesincludes it where it applies: do not subtract one from the other.margin_before_adsis what is left after paying Amazon, before advertising and product cost: for the margin after ads, subtract the spend from/v1/product-ad-metrics. Amazon books storage on a single day of the month and refunds on the refund date, so read the margin by month, not by day. days_of_coverageis how many days the stock lasts at the pace of the window: the stock of the last day divided by the average daily units of the range you ask for. It isnullwith any granularity other thantotal, when the product sold nothing or, when grouping, ifgroup_bydoes not include the country. Withorder_by=days_of_coverage&order=ascyou get first the products about to run out.- Vendor also has the sell-in: the units you confirmed to Amazon, those in open purchase orders, the fill rate and the lead time, plus returns split by manufacturing and sourcing and the health of the inventory.
- The category of the rank and the reviews are requested on the product, not on the metrics.
expand=salesRankstells you which category each Best Seller Rank position belongs to, and on Vendorexpand=customerFeedbackreturns the positive and negative review topics and the return reasons of the category. It is this week's snapshot, with the six-month trend Amazon calculates, not a history.
Account totals, by period and by cluster
To know how much the account sold in September you do not need to page through the catalog and add it up: the seven metrics resources aggregate on their own.
group_byadds up across entities.accountgives the account total;countryandconnectionbreak it down by country or by connection, and they combine by repeating the parameter (group_by[]=connection&group_by[]=country).accountdoes not combine with anything. Rows are still split by currency, since amounts are never converted, and, in advertising, by ad type (SP,SB,SD).granularitysplits the range into calendar periods. Besidestotalanddailyit acceptsweekly,monthly,quarterlyandyearly, and each row carries indatethe start of its period: the Monday of the week, or the 1st of the month, quarter or year. The periods at either end are partial: from January 15 to March 10 withmonthlyreturns January from the 15th to the 31st, all of February, and March from the 1st to the 10th.clusternarrows it down to a product family. On Seller, Vendor and product ad metrics,cluster=<id>keeps only the products in that cluster; with several ids, their union. It combines with the above:group_by=account&granularity=monthly&cluster=<id>is the monthly series for that family.group_by=clustergives one row per cluster. On the same three metrics,group_by=clusterreturns the figures of each of your clusters in a single call, with its id inclusterand its name incluster_name. A product in several clusters counts in full in each of them, so the rows do not add up to the account total, and products in no cluster do not appear. It combines withcountryandconnection, and with theclusterfilter to choose which clusters come back; if your clusters add up to more than 2,000 product-cluster memberships, the API asks you to narrow them down with that filter.
In grouped rows, the fields that identify an entity (product, asin, campaign…) arrive as null, and so does whatever cannot be added up: the rank, the price, the channel and the Vendor rates. Seller stock is only added up if you group by country: with FBA's pan-European inventory, Amazon reports the same stock in every marketplace, and adding it up across countries would count the same units several times. That is why, with group_by=account or connection, stock and days_of_coverage arrive as null.
A cluster groups catalog products, not loose ASINs: the same ASIN on another connection is left out unless its product is in the cluster too. And rows Amazon could not match to a catalog product do not enter the filter: around 5% of Vendor sales and 3% of product ad spend.
How much it saves in credits is explained in Credits.
What the automation changed on a campaign
/v1/campaigns/{id}/optimization-history answers what the workflow changed on a campaign, not what it decided. The difference matters: in a decision log the same target can show up with a raise and a cut on the same night, and there you cannot see that they cancel each other out.
So every row comes aggregated to the entity's net effect: the bid from before the first change of the period against the bid after the last one, plus how many times it moved along the way. A target raised from €0.18 to €0.45 and cut back to €0.18 that same night comes back with a net of zero and two moves — which is the useful reading, and exactly the one a flat list hides.
Three things bound what it returns:
- Real changes only. Simulations are not exposed here, under any parameter: they are bids that never reached Amazon.
- Only what was applied, not what was proposed. The optimizer computes a bid that a ceiling can then cut; only what was sent is reported.
- A 90-day window at most. That is how long these records are kept. With no dates, it returns the last 90 days.
Changes that are not bids — harvested keywords, negatives, placement adjustments, quarantined or blocked targets — are counted separately in other_changes, because they have no previous bid to compare against.
Everything Epinium has changed on Amazon
/v1/advertising-changes answers a different question from the previous one: not why the automation decided something, but what was touched and whether Amazon accepted it. And not only what the automation does: also what a person did from the app, an internal process, or this very API.
The rule is one line: if it was sent to Amazon, there is a row. Three consequences follow, and they are worth having clear:
- What Amazon rejected leaves a row too, with
result: "failed"and the Amazon message inerror. It is the first place to look when a change did not take effect. - What was never attempted leaves no row: a budget below the country minimum, or a bid that already was the one being asked for.
- Configuration changes inside Epinium stay out, because they never reach Amazon.
Every row says where it came from. source tells the app (front), the optimizer (workflow), an internal process (system) and this channel (public-api, mcp) apart, and actor identifies the person or the specific credential — when someone made the change from the app, their email is there. If it came from a workflow, correlation_id is that run's identifier, which is the key for cross-referencing this response with optimization-history.
You can filter by campaign, adgroup, entity, entity_type, source, result, field and correlation_id, and narrow the window with start_date and end_date (YYYY-MM-DD, both inclusive). Records are kept for twelve months.
Writing to Amazon
With the campaigns:write permission the API stops being read-only: it creates campaigns, ad groups, targets and product ads, and moves their states, budgets and bids. What you write really goes to Amazon and leaves its row in /v1/advertising-changes.
Five things worth knowing before your first call:
- Everything is born paused. A campaign or an ad group is created
PAUSEDunless you ask forENABLEDexplicitly, because anything enabled starts spending as soon as Amazon accepts it. - Archiving has its own verb:
DELETE /v1/<resource>/{id}. It is the only irreversible operation in this API: on Amazon, archiving cannot be undone. That is why it is not astatevalue you could set by accident next to a bid change, but a separate call with a separate verb. If you only want to stop the spend, usestate: "PAUSED", which is reversible. - Every write requires an
Idempotency-Keyheader. Reuse the same value if you retry the same intent: without it, a network timeout becomes two campaigns. - A batch is not all-or-nothing.
POST /v1/<resource>/batchtakes up to 1000 entities and answers with one row per entity, in the order you sent them.appliedis what reached Amazon, and it is the only thing billed. A partial result is the normal outcome, not an error: read it instead of retrying the whole batch, because a retry would duplicate what already went in. - An entity counts as applied only if everything you asked for went through. If a
PATCHchanges state and budget and Amazon accepts the first and rejects the second, that entity comes backfailedwith the reason — calling it applied is how you end up believing a bid moved when it did not.
| Route | What it does |
|---|---|
POST /v1/campaigns | Create a campaign |
POST /v1/campaigns/full | Campaign + ad group + targets + product ads in one call |
PATCH /v1/campaigns/{id} · POST /v1/campaigns/batch | State, daily budget, name and portfolio (the portfolio, Sponsored Products only) |
POST /v1/adgroups · PATCH /v1/adgroups/{id} · POST /v1/adgroups/batch | Create; state, default bid and name |
POST /v1/targets · PATCH /v1/targets/{id} · POST /v1/targets/batch | Create targets (the types depend on the ad group's ad product); state and bid |
POST /v1/product-ads · PATCH /v1/product-ads/{id} · POST /v1/product-ads/batch | Advertise products; state |
POST /v1/portfolios · PATCH /v1/portfolios/{id} | Create a portfolio; name, state and budget cap |
DELETE /v1/campaigns/{id} · DELETE /v1/adgroups/{id} · DELETE /v1/targets/{id} · DELETE /v1/product-ads/{id} | Archive - irreversible |
What cannot be changed, and why: what defines a target — its keyword, its ASIN, its match type — is not editable on Amazon, so to change it you create another and archive the previous one. A product ad only takes state: it is the link between a product and an ad group, with no bid or name of its own. And entities managed by the optimizer are refused, because its 03:00 UTC run would write over you.
A portfolio's cap limits what all its campaigns spend together. MONTHLY_RECURRING resets every month, DATE_RANGE applies between startDate and endDate and NO_CAP removes it; the currency is the marketplace one and you do not send it. In a PATCH, budget is replaced whole: send the full cap you want, not just the amount. To move a Sponsored Products campaign into or out of a portfolio, change its portfolioId with PATCH /v1/campaigns/{id}.
Two status codes worth telling apart. An entity that does not exist, or belongs to another account, answers 404 — never 403, which would confirm it exists. A rejection from Amazon answers 200 with result: "failed" and the reason: the call did arrive, and what failed was the change.
All three ad products can be written, but not in the same way:
- Sponsored Brands and Sponsored Display are changed and archived with the same routes as Sponsored Products: state, budget, name and bids of campaigns, ad groups, targets and product ads. What you cannot do yet is create them:
POST /v1/campaigns,/v1/campaigns/full,/v1/adgroupsand/v1/product-adsare Sponsored Products only. - Sponsored Brands ad groups have no default bid. A
defaultBidchange on one is rejected with a message that says so. Sponsored Display ad groups do have one. - Targets and negative targets can be created in SB and SD ad groups, with
POST /v1/targets. The acceptedtargetTypedepends on the ad group's ad product (table below). Ad-group level only: SB and SD have no campaign-level negative targets. - Epinium checks the marketplace bid and budget limits only for Sponsored Products, before sending. For Brands and Display it applies no limits of its own: Amazon validates, and its reason comes back in the response, per entity with
result: "failed"or, when creating targets, infailure_details, wherereferenceidentifies the target (keyword, ASIN, SKU or the audience, category or location id).
| Ad product | Accepted targetType |
|---|---|
| Sponsored Products | KEYWORD, PRODUCT, PRODUCT_CATEGORY |
| Sponsored Brands | Those of Sponsored Products plus THEME (themeMatchType KEYWORDS_RELATED_TO_YOUR_BRAND or KEYWORDS_RELATED_TO_YOUR_LANDING_PAGES) |
| Sponsored Display | KEYWORD; PRODUCT by ASIN or by sku, with productMatchType PRODUCT_EXACT or PRODUCT_SIMILAR; PRODUCT_CATEGORY with refinements (brand, price, rating, age range, Prime eligibility); THEME (INTERESTED_AUDIENCE); PRODUCT_AUDIENCE (ASIN + event PURCHASE or VIEW + lookback of 7, 14, 30, 60, 90, 180 or 365 days); AUDIENCE (audienceId), CONTENT_CATEGORY (contentCategoryId) and LOCATION (locationId) |
The ids for AUDIENCE, CONTENT_CATEGORY and LOCATION come from the Amazon Ads console: the API does not list them yet.
Optimization config
With the same campaigns:write permission, besides writing to Amazon you can read and change the configuration Epinium's optimizer uses: what it chases on each campaign. This is a different surface from the one above, and worth not confusing with it.
It never reaches Amazon. It is Epinium's own configuration, consumed by the optimizer on its nightly run, not a fact about the Amazon account. That is why two rules from "Writing to Amazon" do not apply here: no Idempotency-Key header is needed - there is no round trip to Amazon a retry could duplicate - and the change leaves no row in /v1/advertising-changes, which only records what was sent to Amazon.
It is read with expand, not by default. GET /v1/campaigns and GET /v1/campaigns/{id} only return optimizationConfig if you ask for ?expand[]=optimizationConfig. It is opt-in because very few campaigns have this configuration saved: without the expand, the column would come back null almost every time.
What you can set with PATCH /v1/campaigns/{id}/optimization-config, in business terms:
- The objective: a target ACOS or ROAS, or a volume objective - impressions or orders - with an ACOS ceiling alongside it.
- The bid limits: the floor and ceiling it cannot cross, the placement multiplier cap, and the minimum spend before negativizing a target.
- The monthly budget the optimizer chases - not the campaign's Amazon daily budget, which is changed with
PATCH /v1/campaigns/{id}and is a different field with the same name. - The campaign's harvesting overrides: its discovery level and whether it allows targeting competitor ASINs.
- Which workflows optimize it.
That last point is the one that really matters: assigning workflows is what puts the campaign under optimization, not any other field in this block. linkedWorkflowDefinitionIds force-includes it - the workflow processes it every night even if its own filter would not have picked it - and it is how a workflow moves real bids on Amazon; confirm it with the person before writing it, exactly as you would turning on an automation. A workflow with auto-selection can also pick it up on its own, and workflowsOptOut: true is what declines that automatic selection. The only thing that reliably takes it out is excludedWorkflowDefinitionIds: it always wins, whatever else says otherwise.
aiEnabled is not the optimizer's switch
It is the account holder's consent flag, and it travels to Amazon as the epinium:ai_enabled tag. Setting it to false does not take the campaign out of optimization: that is what excludedWorkflowDefinitionIds is for, or removing it from linkedWorkflowDefinitionIds.
optimizeDate and optimizingSince are read-only - the optimizer itself stamps them - and writing either one gets you a 400.
POST /v1/campaigns/optimization-config/batch applies the same patch to up to 1000 campaigns, and it is not all-or-nothing: it answers with one row per campaign, in the order you sent them, and each one is validated against its own stored state - a ceiling that is mandatory for one objective can be optional for another - so one row can fail without affecting the rest.
Two status codes worth telling apart, just as when writing to Amazon: a validation rejection - an objective out of range, a workflow that is not yours - answers 200 with result: "failed" and the reason in reason. A campaign that does not exist or belongs to another account answers 404 on the single PATCH; in the batch, that same campaign instead comes back as a failed row with reason: "not_found", precisely so one bad row cannot sink the other 999.
| Route | What it does |
|---|---|
PATCH /v1/campaigns/{id}/optimization-config | Change a campaign's optimization config |
POST /v1/campaigns/optimization-config/batch | The same config on up to 1000 campaigns, one row per result |
Writing tasks
With the tasks:write permission the API creates tasks and decides on their suggestions, just as you do in Processes → Tasks. It is the piece that lets an assistant or an integration propose changes and a person review them before they reach Amazon.
Four things worth knowing before your first call:
- Approving does not apply. There are two steps: approve or reject each suggestion, and then
POST /v1/tasks/{id}/apply. At that point campaign changes — daily budget, state and name — go to Amazon straight away, and the response says which ones went in (applied) and which ones it rejected (failed). Product changes are queued (enqueued) and applied in the background: to find out whether they went in, read the suggestions again and look atappliedAtandapplyError. - A suggestion that nothing knows how to apply is a note. Today, product suggestions and campaign suggestions that carry a
fieldPathare applied; those without one, and those for ad groups, targets, product ads and search terms, stay as notes. Approving them records the decision and changes nothing, and the response says so withautoApplySkipped: true. It is not an error. When creating, on the other hand, a campaign suggestion with a field other thandailyBudget,stateorname, or with a value that cannot be applied, is rejected with a400: saving it would promise a change nobody is going to make. - Suggestions come with the task. Up to 50 in
itemswhen you create it, each pointing to an entity in your account; they cannot be added later. An entity that does not exist or belongs to another account rejects the whole task, without creating anything. - Resolving a task is final. Its pending suggestions are not resolved with it: they stay frozen and can no longer be approved. Decide the ones that matter to you first and close the task afterwards. Resolving it does not resume any workflow either.
| Route | What it does |
|---|---|
POST /v1/tasks | Create a task, with its suggestions |
PATCH /v1/tasks/{id} | Title, description, priority and expiry date |
POST /v1/tasks/{id}/resolve | Resolve or discard it |
PATCH /v1/task-items/{id} · POST /v1/task-items/batch | Approve or reject suggestions, optionally with your own value in appliedValue |
POST /v1/tasks/{id}/apply | Apply what was approved |
As with Amazon, every write requires Idempotency-Key, and the suggestions batch is not all-or-nothing: it answers with one row per suggestion and applied counts the ones that were recorded. Creating, editing or resolving a task costs a flat charge per call; approving or rejecting is billed per recorded suggestion. Applying is not billed separately: each suggestion was already billed when it was approved.
Writing keyword clusters
With the catalog:write permission the API creates and edits your keyword clusters, the same ones you manage in Segmentation → Clusters: a named group of products with the positive keywords they should rank or advertise for and the negative keywords to keep away from. It is the way for an assistant that has found a keyword gap to leave it saved where Epinium's SEO prompts, the optimizer's keyword discovery and the cluster filter of the product charts will pick it up.
Four things worth knowing before your first call:
- Nothing reaches Amazon. A cluster lives in Epinium. That is why these writes leave no row in
/v1/advertising-changes, and why your AI client will not ask you to confirm them. - Editing is by deltas, not by replacement.
PATCH /v1/clusters/{id}takesaddPositiveKeywords,removePositiveKeywords,addNegativeKeywords,removeNegativeKeywords,addProducts,removeProductsandname; send only what changes. Adding a keyword that is already there updates its language, score and purpose, matched by text; removing one that is not there does nothing. A cluster holds up to 100 positive and 50 negative keywords, counted after the change, and a keyword cannot be positive and negative at once. - Keywords follow the app's rules. Lowercased, up to 80 characters, Amazon's character set, no leading
-or.and no trailing-,+or.; duplicates are merged.languageis a code likees-ESoren-GB,epiniumScoregoes from 1 to 5 andpurposeisSEO,PPCor both (both by default). - A protected cluster is refused with
409. Protecting a cluster from the app is a person's decision, and this API cannot undo it: unprotect it in Segmentation → Clusters first. Theprotectedfield in the response tells you before you try.
| Route | What it does |
|---|---|
POST /v1/clusters | Create a cluster, with its keywords and products if you have them |
PATCH /v1/clusters/{id} | Add or remove keywords and products, or rename it |
DELETE /v1/clusters/{id} | Delete it, with no way back |
Every product you link must belong to your account — for an agency, to the connections the account shared with you — and be a catalog product (type seller or vendor in /v1/products; advertising products are refused), or the whole call is refused with a 400 naming the offending ids, and nothing is written. The cluster is owned by the person behind the credential, so it shows up in the app's table under their name. Deleting is final - there is no bin - and is refused with a 409 while the cluster is protected or a smart campaign uses it as its base cluster; the error names those smart campaigns, which have to be changed or deleted first. Each write costs a flat charge per call, and the plan's cluster quota applies here as it does in the app.
Limits
- Metrics require a date range.
start_dateandend_dateare mandatory on every metrics resource. - The range has a maximum, and it depends on the granularity. With
granularity=daily, 93 days; withtotalor with the calendar periods (weekly,monthly,quarterly,yearly), 366. On targets and search terms, the heaviest resources, those drop to 31 and 93. Asking for more returns an error naming the exact limit. Both dates are included in the range. - Lists paginate in two different ways. Catalog and structure resources use a cursor (
starting_after); metrics resources uselimitandoffset. In both caseshas_moretells you whether another page follows. - There is a request limit per token. If you exceed it you get a
429and simply need to retry more slowly. - There is also a limit on concurrent requests. A
429can mean "too many at once", not just "too many per minute"; a503means the server is momentarily at capacity. Both carryRetry-Afterand both are transient: retry with fewer requests in parallel. - A single response cannot expand more than 5,000 objects.
expandover lists — a cluster's products, a connection's countries — multiplies at every level. Going over returns a400naming how many objects it would have returned: lowerlimit, drop anexpand, or fetch that data from its own endpoint.
Credits
Calls to this API consume credits from your account. What each resource costs is visible in the app, under Settings → Credits.
Every response tells you what you spent:
| Header | What it is |
|---|---|
X-Epinium-Credits-Cost | What this call cost |
X-Epinium-Credits-Remaining | What you have left. It is approximate: if you have other things consuming at the same time, it can drift |
X-Epinium-Credits-Breakdown | The breakdown, per resource and unit price: product=100x1,country=100x0 |
X-Epinium-Credits-Max | The most it could have cost, computed before running it |
Three things worth knowing:
- You are only charged for what is delivered. A call that fails costs nothing, and an
expandthat returns no data — because your token lacks that scope, or the record belongs to another account — costs nothing either. - In metrics, the date range costs too, not just the rows. Asking for a year with
limit=1is not cheap: on top of every row returned you pay for every day of the range, because that is what has to be read to answer, with a 3-day minimum per call. To spend less, shorten the range. - For a total,
group_byinstead of paging. An account total is only a few rows, one per currency, whereas paging through the catalog to add it up pays for every product on every page. On a real account, a month's total sales cost 157 credits withgroup_by=accountand about 17,600 by paging.
If your credits do not cover it you get a 402 before the call runs: you will never be left with half a response. And it does not just say no, it says what to change and to what value:
{
"error": {
"type": "invalid_request_error",
"code": "insufficient_credits",
"message": "insufficient credits: needs up to 366 (campaignMetrics=1x1,campaignMetrics:window=365x1), 120 available. Shorten the date range to 119 days or less.",
"param": "end_date"
}
}param is the same remedy in machine-readable form: limit, end_date or include_total. When it is absent, no parameter change fits the request and you need to top up.
Parameters and responses of each endpoint
The table above says which resources exist. To see the input parameters and the exact shape of each response, there are two routes:
- Browsable reference — every endpoint with its parameters, their types and an example response. Generated from the API itself, so it cannot fall out of date. In English.
- OpenAPI schema — the raw OpenAPI 3.0 document, to import into Postman, Insomnia or a client generator. No token needed.