Back to Arky

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;
}
POST /v1/stores/{store_id}/markets
SDK: sdk.store.market.create()
GET /v1/stores/{store_id}/markets
SDK: sdk.store.market.find()
GET /v1/stores/{store_id}/markets/by-key/{key}
SDK: sdk.store.market.getByKey()
PUT /v1/stores/{store_id}/markets/{id}
SDK: sdk.store.market.update()
DELETE /v1/stores/{store_id}/markets/{id}?expected_updated_at=
SDK: 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],
});
  • key and currency never change. currency is 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_mode says 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_ids lists 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

POST /v1/stores/{store_id}/sales-channels
SDK: sdk.store.salesChannel.create()
GET /v1/stores/{store_id}/sales-channels
SDK: sdk.store.salesChannel.find()
GET /v1/stores/{store_id}/sales-channels/by-key/{key}
SDK: sdk.store.salesChannel.getByKey()
PUT /v1/stores/{store_id}/sales-channels/{id}
SDK: sdk.store.salesChannel.update()
DELETE /v1/stores/{store_id}/sales-channels/{id}?expected_updated_at=
SDK: 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

POST /v1/stores/{store_id}/api-keys
SDK: sdk.store.apiKey.create()
GET /v1/stores/{store_id}/api-keys
SDK: sdk.store.apiKey.find()
GET /v1/stores/{store_id}/api-keys/{id}
SDK: sdk.store.apiKey.get()
POST /v1/stores/{store_id}/api-keys/{id}/revoke
SDK: sdk.store.apiKey.revoke()
DELETE /v1/stores/{store_id}/api-keys/{id}?expected_updated_at=
SDK: 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_id never changes; a channel may have several keys, such as one per site, app or staging copy. Its bot_check (off, turnstile or h_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 with createAdmin({ 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:

HeaderSDK optionMeaning
X-Arky-Publishable-Keyfirst argumentthe publishable key: the store and its one sales channel
X-Arky-Marketmarketthe market key; implied when the key’s channel lists exactly one active market
X-Arky-Localelocalethe 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

POST /v1/stores/{store_id}/zones
SDK: sdk.store.zone.create()
GET /v1/stores/{store_id}/zones
SDK: sdk.store.zone.find()
PUT /v1/stores/{store_id}/zones/{id}
SDK: sdk.store.zone.update()
DELETE /v1/stores/{store_id}/zones/{id}?expected_updated_at=
SDK: 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

POST /v1/stores/{store_id}/tax-categories
SDK: sdk.store.taxCategory.create()
GET /v1/stores/{store_id}/tax-categories
SDK: sdk.store.taxCategory.find()
PUT /v1/stores/{store_id}/tax-categories/{id}
SDK: sdk.store.taxCategory.update()
DELETE /v1/stores/{store_id}/tax-categories/{id}?expected_updated_at=
SDK: 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_taxable or not_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.
Note

Payment options themselves (Stripe, Monri, cash on delivery, manual) are on the E-shop page.