The short version#
A sale must be in a currency the SKU is priced in for the country you declare, or in one the Content Owner has explicitly authored for that SKU. Your agreement's Commercial Schedule then narrows that further: it lists the currencies you may sell in, and a currency outside that list is outside the grant even when the SKU is priced in it.
Your statement is denominated in your agreement's payment currency, not in the currency of each sale. Conversion happens once, at a dated reference rate, and the statement names the rate, its source and its fixing time for every currency it converted.
Which currency you may declare#
Send integrationOrderContext.currencyCode on every order. Three checks run against it, in this order, and each returns a different error because each has a different fix.
| Check | Error when it fails | What to do |
|---|---|---|
| The country you declared is one you have declared you sell into | FOOTPRINT_COUNTRY_NOT_DECLARED | Declare the country. The error body carries a fixItUrl |
| The currency is priced for that country, or authored on the SKU | CURRENCY_NOT_PRICED_FOR_COUNTRY | Sell in the priced currency, or ask the Content Owner to author yours |
| The Commercial Schedule permits the currency | CURRENCY_NOT_PERMITTED_BY_TERM | Amend the agreement. See "Changing what you may sell" below |
CURRENCY_MISMATCH is a different failure and keeps its own meaning: the currency you sent disagrees with the snapshot a checkout hold froze. Re-read the hold, or create a new one.
Call GET /api/seller/v1/whoami to see all three boundaries before you hit one. data.footprint is what you have declared, data.permittedCurrenciesByPublisher is what each Content Owner's Commercial Schedule permits, and data.geoPolicyByPublisher is their geo-mismatch policy together with any per-SKU tightening. That last one has no other route to you: a Content Owner sets it alone, because it can only ever refuse.
Primary and secondary currencies#
A SKU has one currency per country: the one its price list resolves to. That is the primary currency for that country, and a sale in it is checked against the price directly, the way it always has been.
A Content Owner may additionally author a secondary currency on a SKU. That permits a sale in it; it does not create a price in it. There is nothing to compare your expectedUnitPriceCents against, so instead the sale is checked against a floor: the SKU's price in the primary currency, converted at the reference rate for the sale date.
A sale below that floor is refused with SRP_MISMATCH_FOR_CURRENCY on POST /orders. The error body carries the floor, so you can re-price without guessing. POST /orders/preview quotes the primary currency and checks your footprint and the Schedule for it; it does not quote a secondary currency, so for one of those the floor is what the commit tells you.
The floor exists because a secondary currency would otherwise be a way to sell below the agreed SRP by choosing a currency that happened to be cheap that week. It is truncated to the minor unit rather than rounded up, so the platform never asks for more than the SRP actually converts to: a price converting to 13.7977 floors at 13.79, not 13.80.
Ordinary sales in the primary currency involve no exchange rate at all.
Named currency lists#
A Commercial Schedule may state its permitted currencies as a named list instead of a rule. Where it does, whoami reports currencyMode: "custom" and a currencyDetail object beside it:
{
"currencyMode": "custom",
"currencyDetail": { "EUR": ["PL", "CZ"], "USD": "territory" }
}
Read it as: euros in Poland and Czechia only, dollars anywhere in the granted territory. A currency the list does not name is refused with CURRENCY_NOT_PERMITTED_BY_TERM however the SKU is priced, including a country's own currency. A currency it names in other countries than yours is refused with the same code and details.reason: "currency_not_permitted_for_country", and the body carries the countries that would have been accepted.
Everything else still applies underneath. The currency must be priced or authored for the country, the sale must meet the floor if it is a secondary currency, and the country must be in your declared footprint. A named list narrows; it never widens.
The reference rate#
Rates are dated daily fixings, not live quotes. The source is the European Central Bank, or, where the ECB publishes no rate for a currency, the licensed reference provider recorded on the Platform and named with its fixing time on each statement. That sentence is in your agreement, in the FX Convention definition.
Two conventions exist, and which one applies to you is stated in your Commercial Schedule:
- Period end converts at the last fixing on or before the end of the reporting period.
- Period average converts at the mean of the business day fixings across the period.
Where no direct pair is published, the rate is derived through the euro at eight decimals. Rounding to the currency's minor unit happens once, at the end.
The rate on your statement is not the rate you saw at preview. POST /orders/preview returns an indicative block so you can show a customer an approximate figure; it is labelled indicative because the binding rate is the period's fixing, which does not exist yet when you preview. Do not reconcile against it.
Sales channels#
Your Commercial Schedule lists the channels you may sell through, and integrationOrderContext.salesChannel declares which one each order came through. Accepted values are web, mobile_app, partner_embed, and marketplace:<slug>.
Under the default own_storefront term, the first three are permitted and any marketplace:<slug> is refused with SALES_CHANNEL_NOT_PERMITTED. Omitting the field entirely gives SALES_CHANNEL_REQUIRED.
A Schedule may instead admit named marketplaces. Where it does, whoami reports permittedChannels: "own_storefront_plus_named_marketplaces" and a marketplaceSlugs array beside it. Declare one of those as marketplace:<slug>, spelled exactly as the array spells it. Your own three channels stay permitted, and any marketplace the agreement does not name is still refused. Naming a marketplace does not change who the seller of record is: you remain responsible for a sale made there as if it were made on your own storefront.
This follows the Key Delivery article: the grant is to sell to an end user through your own storefront. Reselling, sub-distributing, or listing a Key on a third-party marketplace is outside it, and an operator of a marketplace may hold the agreement only in respect of its own first-party storefront.
Changing what you may sell#
Currencies, channels and territories are contract terms, frozen into the agreement when it is activated. Narrowing or widening one on a live contract is a countersigned amendment through the normal amendment flow. There is no settings screen that changes them, deliberately: a value that decides what a sale settles at should not be editable by one party alone.
Your footprint, the countries and currencies you sell in, is different. It is your own operational fact, you edit it yourself, and it can only narrow what a contract already grants. It never widens one.
Reading and writing your footprint#
GET /api/seller/v1/me/footprint returns the countries and currencies you have declared. PUT replaces them.
It is a replace, not a merge: send the complete set both times. A merge would leave you unable to say "I no longer sell here", and would make a retry mean something different from the first call. Both lists must name at least one entry: an empty footprint would refuse every order, so the API refuses to store one, as the console always has.
{ "countries": ["NL", "DE", "BE"], "currencies": ["EUR"] }
Both halves are checked on every order, and they are checked first, because
they are the only boundaries you can fix without talking to a Content Owner. A
country you have not declared is refused with FOOTPRINT_COUNTRY_NOT_DECLARED,
and a sale currency you have not declared with
FOOTPRINT_CURRENCY_NOT_DECLARED. Declaring a currency does not price it: the
Schedule still has to permit it and the title still has to carry a price for
the country, so your declaration is the first of three checks rather than the
only one.
Both need seller:account:manage, which is opt-in at key creation and is the one scope legacy keys do not hold. Your footprint decides what your sales settle against, so a key issued before scoping existed does not silently gain the ability to change it.
GET /whoami reports the same footprint under data.footprint, so an integration can check it without the extra scope.
See also#
- Commercial model & settlement for how the share is calculated
- Finance and settlement for the statement APIs
- Pricing view and BMA authority for where a price comes from
- Error playbook for every code above
- How your share is protected for the rate cap and the evidence behind it