Markets and Channels
Markets, sales channels, publishable and secret keys, zones, and tax categories with their rules
Selling is shaped by five records:
- a market is a selling region with one currency and one way of writing prices, with its payment options in order;
- a sales channel is a route buyers come through, such as the web shop or a B2B portal, and lists the markets it sells in;
- an API key is a website or app’s public publishable key, which belongs to exactly one sales channel, or a secret key for an integration, with its own permissions;
- a zone is a list of places one market sells in; shipping rates and routing name it;
- a tax category is how something is taxed, with rules for the places it covers.
Creating a store creates its first market, its first sales channel listing that market, the
market’s catalog public-{market key}, and a publishable key on that channel. A new store
has no zone, tax category or rule: Arky never guesses a tax rate, so a store’s first taxed sale
needs a tax category with a rule for the buyer’s place.
Markets
import type { EpochMilliseconds } from "arky-sdk";
interface Market {
id: string;
store_id: string;
key: string;
currency: Currency;
tax_mode: "exclusive" | "inclusive";
payment_option_ids: string[];
status: { type: "active" } | { type: "deleting" };
created_at: EpochMilliseconds;
updated_at: EpochMilliseconds;
}/v1/stores/{store_id}/markets sdk.store.market.create() /v1/stores/{store_id}/markets sdk.store.market.find() /v1/stores/{store_id}/markets/by-key/{key} sdk.store.market.getByKey() /v1/stores/{store_id}/markets/{id} sdk.store.market.update() /v1/stores/{store_id}/markets/{id}?expected_updated_at= sdk.store.market.delete() const market = await sdk.store.market.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "hr",
currency: "eur",
tax_mode: "inclusive",
});
await sdk.store.market.update({
store_id: storeId,
id: market.id,
expected_updated_at: market.updated_at,
payment_option_ids: [cardOption.id, bankTransferOption.id],
});
keyandcurrencynever change.currencyis any currency Stripe can charge, with its own decimal places: two for most, zero for JPY, KRW, CLP or VND, three for KWD, BHD, JOD, OMR or TND. Amounts are always integers in the currency’s minor units.tax_modesays whether prices already contain tax or tax is added on top; it is not a rate.- Creating a market creates no catalog: create its catalogs and their access rows yourself (only the store’s first market gets one at store creation).
payment_option_idslists the market’s payment options in the order buyers see them: at most 20 and one card provider. The first card option is the one checkout suggests.- Deleting is refused while a sales channel lists the market, routing names it, or one of its catalogs or zones is in use; then its catalogs and zones go with it.
Sales Channels
/v1/stores/{store_id}/sales-channels sdk.store.salesChannel.create() /v1/stores/{store_id}/sales-channels sdk.store.salesChannel.find() /v1/stores/{store_id}/sales-channels/by-key/{key} sdk.store.salesChannel.getByKey() /v1/stores/{store_id}/sales-channels/{id} sdk.store.salesChannel.update() /v1/stores/{store_id}/sales-channels/{id}?expected_updated_at= sdk.store.salesChannel.delete() const b2b = await sdk.store.salesChannel.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "b2b-portal",
market_ids: [market.id],
status: { type: "active" },
});
A channel is { id, store_id, key, market_ids, status, created_at, updated_at }. Its key is its
label. archived stops new carts and keeps history. There is no default channel: every cart has
one, because the channel decides prices, catalog access and routing. A storefront cart sells
through its key’s channel; an Admin cart names its channel. A channel can’t be deleted while
routing, a catalog access row, a shipping rate, a publishable key or a live cart names it, nor
archived while routing names it.
API Keys
/v1/stores/{store_id}/api-keys sdk.store.apiKey.create() /v1/stores/{store_id}/api-keys sdk.store.apiKey.find() /v1/stores/{store_id}/api-keys/{id} sdk.store.apiKey.get() /v1/stores/{store_id}/api-keys/{id}/revoke sdk.store.apiKey.revoke() /v1/stores/{store_id}/api-keys/{id}?expected_updated_at= sdk.store.apiKey.delete() const portal = await sdk.store.apiKey.create({
store_id: storeId,
id: crypto.randomUUID(),
name: "B2B portal",
type: { type: "publishable", sales_channel_id: b2b.id, bot_check: { type: "off" } },
});
const importer = await sdk.store.apiKey.create({
store_id: storeId,
id: crypto.randomUUID(),
name: "Product import",
type: { type: "secret", permissions: [{ type: "catalog" }], expires_at: null },
});
if (importer.type === "created") console.log(importer.value);
A key is { id, store_id, name, type, status, created_at, updated_at }, with status active or
revoked with its date. Store Admins create, list, revoke and delete them.
- A publishable key (
arky_pk_...) is for websites and apps. Arky makes its token and shows it again on every read. It ships in the site’s code, so it is public: it names the store and its one sales channel, grants only storefront access, and signs nobody in.sales_channel_idnever changes; a channel may have several keys, such as one per site, app or staging copy. Itsbot_check(off,turnstileorh_captcha, with the provider’s secret) makes storefront form submissions and customer code requests carry the provider’s token. - A secret key (
arky_sk_...) is for integrations and scripts. It acts with its own permissions, which follow the role rules, keeps working when its creator leaves the store, and may expire. Its value is shown once, in the create answer; Arky keeps only a hash and a hint. It can’t do what needs a recent sign-in, such as closing the store. History records its creator with the key that acted. Use it withcreateAdmin({ apiUrl, secretKey }). - A retried create with the same id and request returns the stored key, without the secret value;
anything else under that id is refused with
409. - Revoking takes effect at once and keeps the key listed as revoked; a revoked key can then be deleted. To rotate a key, create a new one, deploy it, then revoke the old one.
The Storefront Context Headers
Every storefront request sends the public key, and the rest of its context in headers. The SDK sets
them from initialize options or setContext, or for one request from its { locale, market }
request options:
| Header | SDK option | Meaning |
|---|---|---|
X-Arky-Publishable-Key | first argument | the publishable key: the store and its one sales channel |
X-Arky-Market | market | the market key; implied when the key’s channel lists exactly one active market |
X-Arky-Locale | locale | the language of texts and slugs; must be one of the store’s languages |
import { initialize } from "arky-sdk/storefront";
import { createOrderToken } from "arky-sdk/utils";
const arky = initialize("arky_pk_...", {
locale: "hr",
market: "hr",
createVisitorIdentity: () => ({
customer_id: crypto.randomUUID(),
session_id: crypto.randomUUID(),
}),
createSessionId: () => crypto.randomUUID(),
createId: () => crypto.randomUUID(),
createOrderToken,
});
There is no channel header: a storefront request always sells through its key’s channel, and a
storefront cart can’t change its channel. Cart calls refuse a per-call market, because a cart keeps
the market it was filled in. A language outside the store’s languages is refused with
400; Arky never falls back to another language.
Zones
/v1/stores/{store_id}/zones sdk.store.zone.create() /v1/stores/{store_id}/zones sdk.store.zone.find() /v1/stores/{store_id}/zones/{id} sdk.store.zone.update() /v1/stores/{store_id}/zones/{id}?expected_updated_at= sdk.store.zone.delete() const croatia = await sdk.store.zone.create({
store_id: storeId,
id: crypto.randomUUID(),
market_id: market.id,
key: "hr-mainland",
areas: [{ type: "country", country: "HR" }],
});
A zone belongs to one market and lists at least one area: country, state, postal_code or
postal_code_prefix. When an address matches several zones, the more specific one wins: a postal
code beats a prefix, a longer prefix a shorter one, a prefix a state, and a state a country. Two
zones of one market can’t cover the same place at the same level. A zone can’t be deleted while
anything names it, such as a shipping rate or a routing condition.
Tax Categories
/v1/stores/{store_id}/tax-categories sdk.store.taxCategory.create() /v1/stores/{store_id}/tax-categories sdk.store.taxCategory.find() /v1/stores/{store_id}/tax-categories/{id} sdk.store.taxCategory.update() /v1/stores/{store_id}/tax-categories/{id}?expected_updated_at= sdk.store.taxCategory.delete() const standard = await sdk.store.taxCategory.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "standard",
rules: [
{
id: crypto.randomUUID(),
areas: [{ type: "country", country: "HR" }],
treatment: {
type: "rates",
components: [
{ id: crypto.randomUUID(), title: "PDV", code: "HR-PDV-25", calculation: { type: "percentage", rate: { numerator: 25, denominator: 100 }, compound: false } },
],
},
starts_at: null,
ends_at: null,
},
],
});
- The rules live inside the category and are written with it, up to 200. Each rule lists the areas it covers, like a zone’s, with an optional validity window; tax places are independent of shipping zones and markets. For a place the most specific matching area wins, and two rules of one category can’t cover the same area at the same level in overlapping windows.
- A treatment is
rates(one or more components, each a percentage, which may be compound, or a fixed amount per unit in a named currency),zero_rated,exempt,not_taxableornot_collecting, each of the last four with a reason. - There is no catch-all. Every sellable names a tax category, and a category with no rule for
the place can’t be sold there: the quote and checkout are refused with
409 TAX.NO_RULE. A buyer whose tax is reverse charge or exempt needs no rule. - A category can’t be deleted while anything names it, such as a variant, booking service, shipping method, customer group entitlement, member revision or order.
Payment options themselves (Stripe, Monri, cash on delivery, manual) are on the E-shop page.