Markets
A market is a distinct business context inside a tenant: a country, a region, or a storefront segment such as B2C versus B2B. It has a base currency, the locales it renders in, the currencies it trades in, and its own tax classes.
Base path: https://api.revenexx.com/v1/markets.
The row
| Field | Meaning |
|---|---|
code | Stable identifier. POST /v1/markets/{id}/clone, /backfill, /make-default, /confirm-ship-from and GET /v1/markets/{id}/readiness accept it in place of the uuid. Every other route takes the uuid, and a code there is 400. |
name, labels | Display name, and localized names. |
currency | The market's base currency. |
ship_from_country | The country the market ships from, upper-case ISO 3166-1 alpha-2 (DE). See Ship-from country. |
ship_from_country_review | true when the platform's own backfill guessed ship_from_country. Set by the platform only. |
status | active or inactive. |
is_default | A tenant has one default market — what every call naming none falls back to. |
position | Sort order. |
Ship-from country
ship_from_country is the country the market ships from, in upper-case ISO 3166-1 alpha-2 (DE, not de). Documents print an address for this country when they carry no sender address of their own, so it decides whether an address counts as abroad. The platform passes it to every app next to the market code.
- On create (
POST /v1/markets) it may be omitted only when the marketcodeis a country code, and is then taken from it (de→DE). Otherwise the create is refused with400ship_from_country_required. A code such aseuorb2bis not a country code, so send the country yourself:{"code":"eu","name":"Europe","ship_from_country":"DE"}. - A value that is not an assigned country code is refused with
400invalid_country. It is not uppercased for you, so sendDE, notde. - On update (
PUT /v1/markets/{id}) it can be changed but cannot be cleared. Changing it clearsship_from_country_review; sending back the stored country does not, because an edit form posts the stored value back. To confirm a guessed country as it stands, usePOST /v1/markets/{id}/confirm-ship-frombelow. - On clone (
POST /v1/markets/{id}/clone) it is the country the new market ships from. It defaults to the country the new code names when that is a country code, else to the source market's. A clone that ends up with none is refused with400ship_from_country_required. - Markets stored before the field existed may answer
nulluntil the platform's own backfill has filled them in. You can set the country yourself withPUT /v1/markets/{id}.
ship_from_country_review is true when the platform's own backfill (a platform job, not a call you make) could not take the country from the code and filled in the tenant's first market country as a guess. Only that backfill sets it: a body may send back false or the stored value, and any other value is refused with 400 server_owned_field on POST /v1/markets and PUT /v1/markets/{id}.
POST /v1/markets/{id}/confirm-ship-from clears ship_from_country_review when the guessed country is right as it stands. It takes a market uuid or market code, and the country it confirms is the one the market stores, so the change is not in the request. Send {}. A market without the flag is returned unchanged and nothing is written. An unknown market returns 404.
curl -X POST "https://api.revenexx.com/v1/markets/de/confirm-ship-from" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
Filters on GET /v1/markets (exact match):
?ship_from_country=DEreturns the markets that ship from Germany.?ship_from_country_review=truereturns the markets whose country is a backfill guess that no write has confirmed yet.
Do not create a market — clone one
curl -X POST https://api.revenexx.com/v1/markets \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"code":"at","name":"Austria","currency":"EUR"}'
POST /v1/markets needs a code and a name, and it leaves a row that cannot serve anybody: no locale to render in, no currency registered to price in, no rate to tax with.
A tenant's first market starts active unless the body names a status. It becomes the default market unless the body sends is_default: false or creates it inactive; is_default: true together with status: inactive is 409 default_market_inactive. It still has no locale, currency or tax class of its own. Every later market starts inactive unless the body names a status, so it can be given its locales, currencies and tax classes before it sells. A market created inactive cannot be the default.
curl -X POST "https://api.revenexx.com/v1/markets/de/clone" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"code":"at","name":"Austria"}'
Clone takes the source market's locales, traded currencies and tax classes with it in a single call, so what comes back is a market that can trade. The path id is the source. It resolves by uuid or by market code. A clone also takes an optional ship_from_country, which defaults as described in Ship-from country.
The three sub-resources
Each is a full CRUD collection under a market, and each carries a code, an is_default flag and a position.
| Sub-resource | Path |
|---|---|
| Locales | /v1/markets/{market_id}/locales |
| Currencies | /v1/markets/{market_id}/currencies |
| Tax classes | /v1/markets/{market_id}/tax_classes |
market_id is filled from the route path on write and never read out of the body. Deleting the market cascades to all three.
Locales
{ "code": "de-AT", "language": "de", "country": "AT", "is_default": true, "position": 0 }
code is language-COUNTRY and unique per market. It is the locale a storefront asks for, not necessarily the key a translation is stored under: a tenant that keeps text per region stores under de-AT, a tenant that keeps text per language stores one de for every German-speaking market. The locale_policy answers which. language and country are stored separately so a client can group markets by language without parsing.
country here is a spelling of the language, not a shipping destination — a market may register de-AT and ship nowhere near Austria.
is_default is the locale a storefront renders when the request asks for none. At most one per market; where none carries the flag, the lowest position wins.
status is preparing or active. A new locale is active unless you send preparing, and any other value is a 400 invalid_locale_status. A storefront offers active locales only, and a preparing locale is never the one default_locale renders in. Add a language as preparing while its search index is still being built, then switch it to active with a PUT. Filter the list with ?status=. The storefront bootstrap carries each locale's status too.
Create one with POST /v1/markets/{market_id}/locales. code is the only required field. language and country are derived from it when omitted, and a value that disagrees with the code is 400 locale_code_mismatch. A malformed code is 400 invalid_locale_code, and a code already taken in this market is 409 (code_taken). The answer is 201 with the stored locale.
curl -X POST "https://api.revenexx.com/v1/markets/{market_id}/locales" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"code":"de-AT","status":"preparing"}'
GET /v1/markets/{market_id}/locales lists them, 50 at a time (limit up to 200, offset, order). Every column is an exact-match filter, such as ?code=de-AT or ?is_default=true. The owning market comes from the path, so market_id in the query is ignored.
Currencies
{ "code": "CHF", "is_default": false, "position": 10 }
One entry per currency this market trades in, as opposed to the single base currency on the market row. is_default is what is offered first to a buyer stating no preference, and it should be the market's base currency — readiness reports it when it is not.
active (boolean, default true) takes a currency out of the choices for new records; records that already carry it keep it. GET /v1/markets/{market_id}/currencies filters on it (?active=true lists the currencies on offer). The default currency cannot be deactivated (409 default_must_stay_active), and neither can the market's base currency (currency on the market row; 409 booking_currency, naming the market). Flagging an inactive currency or tax class as the default is 409 inactive_cannot_be_default. Moving the market's currency onto a currency the market registered and deactivated is 409 currency_inactive on PUT /v1/markets/{id}.
Tax classes
{ "code": "reduced", "name": "Reduced rate", "rate": 7, "is_default": false,
"labels": { "de": "Ermäßigter Satz" } }
code is the rate bucket a product or a shipping method is assigned to — standard, reduced, zero — and other apps resolve against it by code. A product's tax_class column and a shipping method's tax class are both plain text naming one of these.
rate is in percent, 0–100. 20 means 20 %, not 0.2. Whether a stored price already contains it is a separate question, answered per price list.
is_default is applied to a line naming none. A market's first tax class becomes its default when the body names no is_default and the class is active (not active: false). labels is a flat {locale: label} map for storefronts and invoices, keyed by the write key of the tenant's locale policy (de in a tenant that keeps text per language, de-DE per region).
active (boolean, default true) takes a class out of the choices for new records. A product or shipping method that already names it keeps it, and it keeps its rate. GET /v1/markets/{market_id}/tax_classes filters on it (?active=true lists the classes on offer). The default class cannot be deactivated: 409 default_must_stay_active. Make another class the default first.
Create one with POST /v1/markets/{market_id}/tax_classes. code and name are required, rate and position default to 0, and the market comes from the path whatever the body says. The answer is 201 with the stored class. A code already used in this market is 409 (code_taken). active defaults to true. When the body says nothing about is_default, the market's first class becomes the default, unless it is sent with active: false.
curl -X POST "https://api.revenexx.com/v1/markets/{market_id}/tax_classes" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"code":"reduced","name":"Reduced rate","rate":7}'
One locale, currency or tax class
Each of the three also has a route for one row, by its id. A row that belongs to another market answers 404 on read, update and delete, and is never returned, changed or removed.
- Locales:
GET /v1/markets/{market_id}/locales/{id}reads one locale,PUT /v1/markets/{market_id}/locales/{id}updates it, andDELETE /v1/markets/{market_id}/locales/{id}removes it. - Currencies:
GET /v1/markets/{market_id}/currencies/{id}reads one currency,PUT /v1/markets/{market_id}/currencies/{id}updates it, andDELETE /v1/markets/{market_id}/currencies/{id}removes it. - Tax classes:
GET /v1/markets/{market_id}/tax_classes/{id}reads one tax class,PUT /v1/markets/{market_id}/tax_classes/{id}updates it, andDELETE /v1/markets/{market_id}/tax_classes/{id}removes it.DELETEanswers409while anything still points at the tax class by its code: a shipping method, a product or variant, or a payment method, or the market's shipping or payment fee fallback setting. The check is asked of the shipping, products and payments apps, in the class's own market, and it fails open: an app that is not installed or does not answer is not counted as in use.usage_checkedin the answer istrueonly when all three answered, sofalsemeans the row went without that guarantee for at least one of them. The delete cannot be undone: ask the usage route of each app for the same code withX-Revenexx-Market, and point anything that still names it at another class, or create the class again with the same code. Each app's answer is in Shipping rates and Payment methods, and Products.
PUT changes only the fields you send; the rest keep their value. An empty body is 400. A PUT to a locale, currency or tax class by id takes the same fields as its create, and its code must stay unique within the market (409 code_taken). A currency's code is upper-case ISO 4217 (400 invalid_currency, not uppercased for you). Setting is_default moves the flag from the sibling that held it.
Switching a preparing locale to active is one PUT:
curl -X PUT "https://api.revenexx.com/v1/markets/{market_id}/locales/{id}" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"status":"active"}'
The storefront bootstrap
curl "https://api.revenexx.com/v1/markets/{id}/context" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
One call instead of five, and — more to the point — one place the resolution rules live rather than a slightly different copy in every storefront. It answers:
| Key | Is |
|---|---|
market | The market row. |
locales, currencies, tax_classes | All three collections in position order, capped at 200 each. |
default_locale | Which locale to render in, and source naming where that answer came from — a locale flagged default, the first by position, or a tenant fallback. |
locale_policy | The key a client writes a translation under, and the order it reads — resolved, not named. |
pricing | Whether a stored price in this market is net or gross. |
readiness | Whether any of it is trustworthy. |
400, not a lookup.locales being empty is a real answer — read default_locale before assuming a language.
{
"granularity": "regional",
"fallback": "language",
"locales": [
{ "code": "de-AT", "language": "de", "write": "de-AT", "read": ["de-AT", "de"] }
]
}
write and read are resolved from two tenant settings:
locale_granularity—regional(default) writes the full code, sode-DE,de-ATandde-CHare three texts.languagewrites the bare language, onedeevery German-speaking market reads.locale_fallback— what a read tries once the exact key holds nothing:language(default) the bare language,default_localethe market's default locale,nonenothing.
A read always starts at the exact code, so a stored de-AT text wins even in a tenant that writes de. Write under write, read along read, and do not derive either from the setting names. Changing a setting moves no stored text (as of 2026-10). The full table of combinations, and what the platform rejects, is on Languages and locale codes.
For a surface that stands in no market at all — a Cockpit editing a tenant-level default rather than one market's value — GET /v1/markets/locale-policy answers the union of every market's locales, each already resolved to the key it is written under, and each naming the markets that asked for it. A tenant-level value has to be readable by every market, and an editor listing six inputs without saying who needs them invites translations nobody will read.
Readiness is a diagnosis, not a yes/no
curl "https://api.revenexx.com/v1/markets/at/readiness" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
Every check runs on every call and comes back with its own severity.
- Blocking turns
readyfalse: no currency registered means nothing to price in; no tax class means nothing to tax with. - Warning leaves the market serviceable: no locale of its own, while the tenant declares a fallback that covers for it.
Each warning is also listed in warning_reasons, with the reason behind it: locales_missing (no locale of its own, so the tenant fallback applies), locales (its locales have no single default), currencies, tax_classes, or ship_from_country. fallback_locale on the report is the tenant's fallback as it applies to this market, and null when it is empty, which is what makes a missing locale blocking.
Resolves by uuid or market code.
Repairing a market
curl -X POST "https://api.revenexx.com/v1/markets/at/backfill" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"source":"de"}'
Repairs the market in the path out of a source market that is already right. The two are compared by code, collection by collection, and only the codes the target does not already carry are added — so a locale, currency or tax class it already holds is left exactly as the merchant left it, rate included, and is never overwritten.
Idempotent: running it twice adds nothing the second time. Both ids resolve by uuid or code.
Moving the default flag
curl -X POST "https://api.revenexx.com/v1/markets/at/make-default" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
One call promotes the market in the path and demotes whoever held the flag, in the same operation. Doing it from a client as promote-then-demote leaves two defaults when the second write does not land, and none when the first does.
It writes once per row that was actually wrong and touches nothing else. It answers the market plus the codes it demoted; repeating the call writes nothing. Accepts an id or a code.
Deleting a market
curl -X DELETE "https://api.revenexx.com/v1/markets/{id}" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
DELETE /v1/markets/{id} takes the market's uuid. This route does not resolve a market code, so a non-uuid id is 400. The market's locales, currencies and tax classes are deleted with it, so it is never refused for having children. No other app is asked first, and there is no trash: the delete cannot be undone. The answer is 200 with deleted: true and the id.
The tax-class usage check does not run here, so the delete does not report rows in other apps that still name one of the market's tax classes by code: shipping methods and the shipping shipping_tax_class setting, products, and payment methods and the payment payment_fee_tax_class setting. Those keep the old code, which then matches no class in the deleted market. Before you delete, ask the usage route of each app with X-Revenexx-Market set to the market's code, and repoint anything it lists: GET /v1/shipping/tax-classes/{code}/usage, GET /v1/products/tax-classes/{code}/usage and GET /v1/payments/tax-classes/{code}/usage.
409default_market: the tenant's default market cannot be deleted. Make another market the default first withPOST /v1/markets/{id}/make-default.404: no market with that id in this tenant.
Vocabularies
These are the closed value sets this app owns, ready for a select box. GET /v1/markets/vocabularies lists the names with their title and description, without the values. GET /v1/markets/vocabularies/{name} returns one vocabulary with every value in the order the schema lists them. Each value carries its title, description and a badge tone, and default_tone is what a value without one falls back to.
curl "https://api.revenexx.com/v1/markets/vocabularies/market-statuses" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
market-statuses is the only name this app publishes. Any other name is 404. The values are read from the app's own schema, so a select box built from them cannot offer a value that a write would refuse.
Setup checklist
curl "https://api.revenexx.com/v1/markets/setup" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
GET /v1/markets/setup answers the checklist the Cockpit's setup screen shows: every step the app declares, in declared order, each with done and kind, plus done, total and complete for the required steps. A complete checklist does not make a market ready to sell: readiness checks each market. A step is done when what it names exists: at least one market, one locale, one tax class. The locale-policy step is done only when the tenant's settings store locale_granularity; the declared default alone does not count. The route only reads; it stores nothing.
Stored defaults
curl -X POST "https://api.revenexx.com/v1/markets/defaults" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
POST /v1/markets/defaults stores the settings this app needs stored, not just declared, when the tenant has not stored them yet. That is locale_granularity (as of 2026-10), which the search service needs before it publishes. The declared default is written only where nothing is stored, and a value the tenant already stored is never touched, so the call is idempotent: repeating it writes nothing new. The answer says what was written, what was kept, and why nothing was written when nothing was. The app also runs it by itself whenever a market's locales change. The setting itself is described on Settings.
Where to go next
- Channels — the other scoping axis.
- Price lists — what resolves against a currency and a tax class.
- Settings — per-market configuration, and the header's effect on writes.
- Languages and locale codes — the write key and read order in depth.
- Market concept — making your own app's entities market-scoped.