Markets

Markets on revenexx — locales, currencies and tax classes as sub-resources, the storefront bootstrap, the readiness diagnosis, and clone, backfill and make-default.

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

FieldMeaning
codeStable 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, labelsDisplay name, and localized names.
currencyThe market's base currency.
ship_from_countryThe country the market ships from, upper-case ISO 3166-1 alpha-2 (DE). See Ship-from country.
ship_from_country_reviewtrue when the platform's own backfill guessed ship_from_country. Set by the platform only.
statusactive or inactive.
is_defaultA tenant has one default market — what every call naming none falls back to.
positionSort 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 market code is a country code, and is then taken from it (de → DE). Otherwise the create is refused with 400 ship_from_country_required. A code such as eu or b2b is 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 400 invalid_country. It is not uppercased for you, so send DE, not de.
  • On update (PUT /v1/markets/{id}) it can be changed but cannot be cleared. Changing it clears ship_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, use POST /v1/markets/{id}/confirm-ship-from below.
  • 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 with 400 ship_from_country_required.
  • Markets stored before the field existed may answer null until the platform's own backfill has filled them in. You can set the country yourself with PUT /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.

Request
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=DE returns the markets that ship from Germany.
  • ?ship_from_country_review=true returns the markets whose country is a backfill guess that no write has confirmed yet.

Do not create a market — clone one

Request
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.

Request
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-resourcePath
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

Locale
{ "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.

Request
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

Currency
{ "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}.

Nothing converts. A price list in another currency simply does not price the item. Registering a currency here does not create prices in it.

Tax classes

Tax class
{ "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.

Request
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, and DELETE /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, and DELETE /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, and DELETE /v1/markets/{market_id}/tax_classes/{id} removes it. DELETE answers 409 while 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_checked in the answer is true only when all three answered, so false means 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 with X-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:

Request
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

Request
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:

KeyIs
marketThe market row.
locales, currencies, tax_classesAll three collections in position order, capped at 200 each.
default_localeWhich 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_policyThe key a client writes a translation under, and the order it reads — resolved, not named.
pricingWhether a stored price in this market is net or gross.
readinessWhether any of it is trustworthy.
This route resolves the market by id only. Unlike readiness, clone and backfill, a market code here is a 400, not a lookup.

locales being empty is a real answer — read default_locale before assuming a language.

locale_policy
{
  "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, so de-DE, de-AT and de-CH are three texts. language writes the bare language, one de every German-speaking market reads.
  • locale_fallback — what a read tries once the exact key holds nothing: language (default) the bare language, default_locale the market's default locale, none nothing.

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

Request
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 ready false: 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

Request
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

Request
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

Request
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.

  • 409 default_market: the tenant's default market cannot be deleted. Make another market the default first with POST /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.

Request
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

Request
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

Request
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

Was this page helpful?