E-shop
Products, bookings, catalogs and prices, carts and checkout, orders, payments, returns, rentals, inventory, and companies
Commerce in Arky runs through one path: a buyer shops through a catalog, fills a cart,
and checkout turns it into one order with its payment. Product variants, booking offerings and
customer groups are all sellables in that path. Products, booking services, booking resources and
customer groups have a key and blocks, never a separate name or title: the name a buyer sees is a
block.
Every create on this page takes the id the app picks; every change carries the
expected_updated_at it read. A refusal names its reason in a code such as
409 PRICE.OVERLAP; only 409 GENERAL.CONFLICT means “changed since you read it, reload”. See
Core Concepts and Errors.
Products And Variants
import type { EpochMilliseconds } from "arky-sdk";
interface Product {
id: string;
store_id: string;
key: string;
slugs: Record<string, string>;
blocks: Block[];
options: { id: string; key: string; label: LocalizedText; values: { key: string; label: LocalizedText }[] }[];
categories: CategoryEntry[];
status: { type: "active" } | { type: "draft" } | { type: "archived" } | { type: "deleting" };
created_at: EpochMilliseconds;
updated_at: EpochMilliseconds;
}
interface ProductVariant {
id: string;
store_id: string;
product_id: string;
sku: string | null;
blocks: Block[];
options: { option_id: string; value_key: string }[];
options_valid: boolean;
fulfillment:
| { type: "none" }
| {
type: "physical";
shipping_profile_id: string;
inventory_requirements: { inventory_item_id: string; quantity: number }[];
backorder: { type: "disallow" } | { type: "allow" };
}
| { type: "digital"; asset_ids: string[] };
tax_category_id: string;
status: { type: "active" } | { type: "draft" } | { type: "archived" } | { type: "deleting" };
created_at: EpochMilliseconds;
updated_at: EpochMilliseconds;
}/v1/stores/{store_id}/products sdk.eshop.product.create() /v1/stores/{store_id}/product-variants sdk.eshop.productVariant.create() /v1/stores/{store_id}/digital-assets sdk.eshop.digitalAsset.upload() /v1/stores/{store_id}/digital-assets/{id}/archive sdk.eshop.digitalAsset.archive() const product = await sdk.eshop.product.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "linen-shirt",
slugs: { en: "linen-shirt" },
blocks: [{ id: crypto.randomUUID(), key: "name", type: "localized_text", value: { en: "Linen shirt" } }],
options: [
{
id: crypto.randomUUID(),
key: "size",
label: { en: "Size" },
values: [
{ key: "m", label: { en: "M" } },
{ key: "l", label: { en: "L" } },
],
},
],
categories: [],
});
const variant = await sdk.eshop.productVariant.create({
store_id: storeId,
id: crypto.randomUUID(),
product_id: product.id,
sku: "SHIRT-M",
blocks: [],
options: [{ option_id: product.options[0].id, value_key: "m" }],
fulfillment: {
type: "physical",
shipping_profile_id: standardProfile.id,
inventory_requirements: [{ inventory_item_id: shirtItem.id, quantity: 1 }],
backorder: { type: "disallow" },
},
tax_category_id: standardTax.id,
});
- Options belong to the product. It lists them in display order, each with a label and its
values; option keys and value keys never change. Every variant that isn’t archived names one value
of each option, and no two name the same values (
409 PRODUCT_VARIANT.OPTIONS_TAKEN); a product without options has at most one such variant. A variant’s own pictures and texts are itsblocks. - Option edits are always allowed. A variant that no longer fits, because a value was removed or
another variant now names the same values, reads
options_valid: false, is left out of storefront reads and sales, and its next save must fix its choices. - The product key is unique in the store (
409 PRODUCT.KEY_TAKEN) and its slugs per store and language (409 PRODUCT.SLUG_TAKEN). - A variant is physical (shipped from stock, with a shipping profile), digital (files the buyer downloads from their library) or neither (a service or membership sold as a product). Every variant names a tax category. Checkout freezes each option’s key with the chosen value’s label in the order’s language.
- A digital variant lists 1 to 100 digital assets: private files with their size and
sha256, uploaded with an id the app picks. An asset is archived rather than deleted; archiving is refused while a variant still lists it, and past buyers keep their files. - Deleting a product is refused while anything but its own variants, catalog items, prices and carts
points at it, such as an order line; archive a sold product instead. A deleted product and its
variants read
deletinguntil cleanup has removed them. A variant on its own is refused while anything but its prices and carts points at it.
Storefront reads name the catalog and return the price the buyer would pay:
/v1/storefront/catalogs arky.eshop.catalog.find() /v1/storefront/products arky.eshop.product.find() /v1/storefront/products/{id_or_slug} arky.eshop.product.get() /v1/storefront/products/by-key/{key} arky.eshop.product.getByKey() const { items: [catalog] } = await arky.eshop.catalog.find();
const { items } = await arky.eshop.product.find({ catalog_id: catalog.id, limit: 20 });
const variants = await arky.eshop.productVariant.find({ catalog_id: catalog.id, product_id: items[0].id });
const shirt = await arky.eshop.product.get({ slug: "linen-shirt", catalog_id: catalog.id });
catalog.findlists the open catalogs of the scope’s market in which the buyer has any level; which one a page uses is up to the storefront. Every product read names itscatalog_id; Arky never picks or merges catalogs.getreads a canonical UUID as the id and anything else as a slug in the request’s language; a miss is404and is never retried as a key.getByKeyreads the key. Link pages by slug or id, never by key.
Bookings
A booking service is what is booked (a haircut, a fitting) and sets what every booking of it is: its segments, buffers, slot interval, booking window, how long before the start a customer may still change it, its reminders and its tax category. A booking resource is who or what does it (a stylist, a room), with its timezone, capacity and working hours. A booking offering says one resource provides one service, when and where.
/v1/stores/{store_id}/booking-services sdk.eshop.bookingService.create() /v1/stores/{store_id}/booking-resources sdk.eshop.bookingResource.create() /v1/stores/{store_id}/booking-offerings sdk.eshop.bookingOffering.create() /v1/storefront/booking-services/availability arky.eshop.bookingService.getAvailability() const haircut = await sdk.eshop.bookingService.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "haircut",
slugs: { en: "haircut" },
blocks: [],
categories: [],
segments: [{ type: "service", minutes: 45 }],
buffer_before_minutes: 0,
buffer_after_minutes: 15,
slot_interval_minutes: 15,
booking_window: { opens_before_start_minutes: null, closes_before_start_minutes: 120 },
customer_changes: { type: "until_before_start", minutes: 1440 },
reminder_offsets_minutes: [1440],
tax_category_id: servicesTax.id,
});
const stylist = await sdk.eshop.bookingResource.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "lena",
slugs: { en: "lena" },
blocks: [],
categories: [],
timezone: "Europe/Berlin",
capacity: 1,
hours: {
weekly_availability: {
monday: [{ from_minute: 540, to_minute: 1020 }],
tuesday: [{ from_minute: 540, to_minute: 1020 }],
},
date_overrides: { "2026-12-24": [] },
},
});
await sdk.eshop.bookingOffering.create({
store_id: storeId,
id: crypto.randomUUID(),
booking_service_id: haircut.id,
booking_resource_id: stylist.id,
hours: { type: "resource" },
venue: { type: "store_location", store_location_id: salon.id },
});
- Hours are minutes of the day in the resource’s timezone;
date_overridesreplaces a day’s hours by its calendar day (YYYY-MM-DD), and an empty list closes it, for every offering of the resource. An offering books inside its resource’s hours (resource) or only where its own hours overlap them (within). - Buffers hold the resource before and after the appointment but aren’t part of it. Edits to segments, buffers or hours affect only later bookings.
- The venue always names a store location: where the customer comes (
store_location), or where the service is supplied from (not_fixed). Booking tax uses that location’s address. - A service’s key is unique in the store (
409 BOOKING_SERVICE.KEY_TAKEN), and so are its slugs (409 BOOKING_SERVICE.SLUG_TAKEN); a resource’s key is unique too (409 BOOKING_RESOURCE.KEY_TAKEN), and one offering links a service and a resource (409 BOOKING_OFFERING.PAIR_TAKENfor a second). - A resource’s capacity can’t be cut below the bookings that still hold it
(
409 BOOKING_RESOURCE.CAPACITY_IN_USE), and its timezone can’t change while a booking still to end, or any booking on today’s local date, holds it (409 BOOKING_RESOURCE.TIMEZONE_IN_USE). Bookings that have ended never block a change. Until a day the resource keeps in its earlier timezone has passed, a slot whose time reaches that day shows no spots and can’t be sold (ORDER.BOOKING_TIMEZONE_CHANGED). - Availability answers each resource’s days with slots and the spots left, for at most 60 days per request. A booking is a booking line on the cart and on the order; there is no separate booking record.
reminder_offsets_minutesschedules thebooking_reminderemail before the booking. A reminder goes only while its booking is confirmed and upcoming, and only for the start it was planned for, so reminders follow a moved booking.
See Scheduled Services for the storefront flow.
Catalogs, Access And Prices
A catalog is a price book for one market, such as public-hr (the first catalog a store gets is
public- and its first market’s key), “Trade partners HR” or “Christmas 2026”. Catalog items
list what is in it and in which order, catalog access says who may use it, in which channels,
and whether they browse, see prices or buy, and prices say what each sellable costs there.
/v1/stores/{store_id}/catalogs sdk.eshop.catalog.create() /v1/stores/{store_id}/catalogs/{id}/copy sdk.eshop.catalog.copy() /v1/stores/{store_id}/catalog-items sdk.eshop.catalogItem.create() /v1/stores/{store_id}/catalog-accesses sdk.eshop.catalogAccess.create() /v1/stores/{store_id}/prices sdk.eshop.price.create() /v1/stores/{store_id}/prices/batch sdk.eshop.price.batch() const partners = await sdk.eshop.catalog.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "trade-partners-hr",
market_id: croatiaMarket.id,
schedule: { type: "always" },
status: { type: "active" },
});
await sdk.eshop.catalogAccess.create({
store_id: storeId,
id: crypto.randomUUID(),
catalog_id: partners.id,
audience: { type: "all_companies" },
channels: { type: "only", sales_channel_ids: [b2bChannel.id] },
level: { type: "buy" },
});
await sdk.eshop.price.create({
store_id: storeId,
id: crypto.randomUUID(),
catalog_id: partners.id,
sellable: { type: "product_variant", variant_id: variant.id },
amount: 2100,
compare_at: null,
min_quantity: 24,
max_quantity: null,
schedule: { type: "always" },
status: { type: "active" },
});
- A sellable is a
product_variant, abooking_offeringor acustomer_group, named by its own id only. A catalog item lists aproduct, abooking_service, acustomer_groupor acustomer_group_offering, always with a position; a new item goes to the end, and a price is created only for something the catalog lists (409 PRICE.NOT_LISTED). - A price is
always, orscheduledwith a start and an optional end; a scheduled tier in force beats thealwaysone. Quantity tiers come frommin_quantityandmax_quantity:alwaystiers of one sellable can’t overlap in quantity, nor can scheduled tiers whose times overlap (409 PRICE.OVERLAP). Deleting a price is refused while an order line, a member’s terms or an offer names it; archive it instead. - Audiences are
everyone, acustomer_group, acustomer_group_offering(members of any of its groups), acustomer,all_companies, acompanyor acompany_location; channels arealloronlysome; levels arebrowse,see_pricesandbuy. A buyer’s level is the highest of the rows that match them. - A catalog has its own schedule; outside it no cart can use it. The catalog key
public-…means nothing special. copyadds another catalog’s listings, after this catalog’s own in the source’s order, and their prices, when both markets share currency and tax mode. It skips what is already there, prices of items the source no longer lists and of variants that no longer fit their options, and never copies access rows, so it can run again and creates nothing new.
Carts And Checkout
/v1/storefront/carts arky.eshop.cart(scope).create() /v1/storefront/carts/{id}/product-items arky.eshop.cart(scope).addProduct() /v1/storefront/carts/{id}/quote arky.eshop.cart(scope).quote() /v1/storefront/carts/accept arky.eshop.cart(scope).checkout() 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,
});
const cart = arky.eshop.cart({ buyer: { type: "customer" }, catalog_id: catalog.id });
await cart.ensure();
await cart.addProduct({
variant_id: variant.id,
quantity: 2,
purchase: { type: "catalog" },
});
await cart.selectShippingMethod({ shipping_method_id: courier.id });
const quote = await cart.quote();
const accepted = await cart.checkout({
contact_email: "ana@example.com",
payment: {
type: "payment_option",
payment_option_id: quote.suggested_payment_option_id!,
return_url: window.location.href,
save_payment_method: false,
payment_method_terms: null,
},
});
- A storefront cart sells through its publishable key’s sales channel and names its catalog; there is no fallback catalog.
- Every line carries its own id, from the app’s
createIdunless it passes one. Lines areproduct,bookingandcustomer_group; a product line names only its variant. A product line’spurchaseiscatalog, or one of the customer group routes to member prices:existing_purchase_access(an access grant the buyer’s order gave),same_cart_purchase_access(a group joined in this cart) orassigned_purchase_access(a member the team assigned, with its entitlement). - The quote is what checkout would create: lines with snapshots, deliveries with their
shipping offers, promotions, taxes and totals, the payment options and a
presentation_digest. It is in the client’s language.readyis false while something blocks checkout;blockerssays what. A shipping rate that needs a weight Arky doesn’t know is left out of a delivery’soffersand listed in itsunavailablewith the codeSHIPPING.WEIGHT_UNKNOWN, and a selected one becomes ashipping_rate_unavailableblocker; the other rates stay. A booking line that can’t be sold readsavailability: { type: "unavailable", code }, with abooking_unavailableblocker carrying the same code (Scheduled Services). - Checkout carries the
order_idandorder_token(the SDK takes them from the app’screateIdandcreateOrderTokenunless you pass both), the cart’sexpected_updated_at, the quote’spresentation_digest, the contact email, and how the order is paid:free(exactly when the total is zero), apayment_option, oron_accountfor a company or company location whose purchasing policy is on account. To save a card for later, setsave_payment_methodand send the exactsaved_card_termswording the buyer saw inpayment_method_terms. The order’s language is the client’s language (X-Arky-Locale), as for the quote. A changed quote is refused with409 COMMERCE.PRESENTATION_CHANGEDand the new quote, which the SDK raises asCartPresentationChangedError. - The answer is
{ type: "placed", order_id, number, payment_id, payment_action }, or{ type: "already_member", customer_group_member_id }when the cart is a free sign-up for a group the buyer already belongs to: no order is made. The payment action says what the buyer does next: nothing, or a Stripe embedded checkout or Monri card form to mount.mountCheckoutActioninarky-sdk/storefrontmounts it. - Retries: without
order_idandorder_token, checkout keeps the request across reloads before sending it, the same checkout again recovers the kept one, and a different checkout is refused untilrecoverCheckoutsettles it. With both given, useretainCheckoutbefore posting andrecoverCheckouton the next page load. The sameorder_idnever makes a second order. A definite refusal (any4xxexcept408and429) drops the kept request, so the buyer can change the cart and check out again.eshop.checkoutFlow({ buyer, catalog_id })runs the whole checkout page on these rules (Checkout). - A cart is
active,abandoned,converted(with its order),superseded,mergedorexpired. The browser keeps the cart’s recovery token, which the SDK sends asX-Arky-Cart-Token. A cart holds at most 100 lines of each kind and 100 deliveries, and a cart too large to store is refused with413 CART.TOO_LARGE.
Company carts and offers. A cart’s buyer is the customer themself, a company, a
company_location, or a company_location_selection: a company cart whose location isn’t picked
yet, which takes no lines until one is. Company buyers carry an optional purchase order number, and
buying for a company or location needs the place_orders company permission there. Staff can
review a company or location cart and send it as an offer in a language, with an optional expiry and
a note for the customer; an offer locks the cart, holds only lines priced from its catalog (no
purchase-access goods and no upgrade), and a changed offer is a new cart that supersedes it. The
customer signs in with the email code to open and order it before it expires
(409 CART.OFFER_EXPIRED after); an expired offer sends no reminder and stays locked until staff
withdraw or revise it. Offers send the offer_sent and offer_reminder emails, whose wording reads
the note at cart.offer.status.note and the expiry at cart.offer.status.expires_at.
Orders
interface Order {
id: string;
blocks: Block[];
number: string;
invoices: OrderInvoice[];
store_id: string;
source:
| { type: "cart"; cart_id: string; placed_by: Actor }
| { type: "renewal"; order_customer_group_line_item_id: string; recovery: RenewalRecovery | null };
customer_id: string;
contact: { email: string | null; first_name: string | null; last_name: string | null; phone: string | null };
buyer: OrderBuyer;
market_id: string;
sales_channel_id: string;
tax_mode: "exclusive" | "inclusive";
collection:
| { type: "free" }
| { type: "payment_option"; payment_option_id: string }
| { type: "customer_group_member" }
| { type: "on_account"; payment_option_id: string; terms: PaymentTerms; due_at: EpochMilliseconds; approval: OnAccountApproval };
line_items: OrderLineItem[];
delivery_groups: OrderDeliveryGroup[];
currency: Currency;
promotions: OrderPromotion[];
billing_address: PostalAddress | null;
language: string;
created_at: EpochMilliseconds;
updated_at: EpochMilliseconds;
status: "pending" | "confirmed" | "partially_cancelled" | "cancelled";
totals: OrderTotals;
}/v1/stores/{store_id}/orders sdk.eshop.order.find() /v1/stores/{store_id}/orders/{id} sdk.eshop.order.get() /v1/stores/{store_id}/orders/{id}/cancel sdk.eshop.order.cancel() /v1/stores/{store_id}/orders/{id}/product-items/{line_item_id}/cancel sdk.eshop.order.cancelProductItem() /v1/stores/{store_id}/orders/{id}/booking-items/{line_item_id}/cancel sdk.eshop.order.cancelBookingItem() /v1/stores/{store_id}/orders/{id}/booking-items/{line_item_id}/reschedule sdk.eshop.order.rescheduleBookingItem() /v1/stores/{store_id}/orders/{id}/booking-items/{line_item_id}/attend sdk.eshop.order.attendBookingItem() /v1/stores/{store_id}/orders/{id}/booking-items/{line_item_id}/no-show sdk.eshop.order.markBookingItemNoShow() /v1/stores/{store_id}/orders/{id}/resend-receipt sdk.eshop.order.resendReceipt() /v1/stores/{store_id}/orders/{id}/credits sdk.eshop.order.credit.create() - Lines are
product,booking,customer_group,rental_useandpurchase_access, each with its frozen snapshot, money and tax. A line ispending(a card checkout waiting 30 minutes for its payment),confirmedorcancelledwith a reason:store_cancelled,customer_cancelled,payment_failedorexpired. The order’s status is computed from its lines. - Arky gives each order a
numberat placement, unique in its store, which never changes. An order holds at most 500 lines and 100 deliveries. - Lines start
confirmedwhen nothing must be paid first: a free order, an on-account order, and cash on delivery or a manual option, whose payment stays open until the money is recorded. Staff can’t confirm a card order’s waiting lines by hand. - Booking lines are the bookings: staff mark attendance (
attendBookingItem) or a no-show once the booking has started (409 ORDER.BOOKING_NOT_STARTEDbefore), move it to another start of the same offering before it starts (rescheduleBookingItem, at most 20 moves, emailingbooking_confirmedagain unlessnotify_customeris false), or cancel it until it ends. Customers move or cancel their own booking on the storefront only as the service’scustomer_changesallows.sdk.eshop.order.findBookingItemslists bookings across orders bypending,confirmed,attended,no_showorcancelled. - Cancelling a confirmed product or booking line names the app-picked id of the credit it creates; a credit is money owed back to the buyer, refunded through the payment. Cancelling every line of an order paid by cash or a manual option closes its open payment.
- Staff credits (
credit.create, under a credit id the app picks, with itstargetsandreason) lower what the buyer owes; the same id with othertargetsor anotherreasonis409 GENERAL.CONFLICT. A credit that leaves nothing owed settles the order like a payment would, so goods heldawaiting_paymentcan leave. - Staff with
ordersmay correct the order’s contact (a normalized email) and, while nothing of a delivery has left, its address; they also edit the order’sblocksand attach up to 20 invoice or credit note links from the store’s accounting software. - Resend receipt sends the current
order_receivedwording again to the order’s contact email, as a new outgoing email under the id the app picks, and answers with that email. When that email is queued to go, the order gets a new link and the old link stops working; a resend recordednot_sent, for example to a blocked address, leaves the old link working. It is refused while the store has noorder_receivedemail selected (409 STORE_EMAILS.ORDER_RECEIVED_EMPTY). - Customers read their own orders, deliveries and payments on the storefront through
arky.eshop.order.
Payments
Payment options are how a market’s buyers pay: stripe (cards, after connecting the store’s
Stripe account and its webhook), monri, cash_on_delivery and manual (such as a bank transfer,
with blocks telling the buyer where to pay). A market lists its options in order. A Stripe or Monri
option holds the store’s own saved_card_terms, per language: the wording a person accepts when a
card is saved under it.
/v1/stores/{store_id}/payment-options sdk.store.paymentOption.create() /v1/stores/{store_id}/payment-options/stripe sdk.store.paymentOption.stripe.connect() /v1/stores/{store_id}/payments sdk.eshop.payment.find() /v1/stores/{store_id}/payments/{id}/refunds sdk.eshop.payment.createRefund() /v1/stores/{store_id}/payments/{id}/refunds/{refund_id}/receipts sdk.eshop.payment.recordRefundReceipt() /v1/stores/{store_id}/payments/{id}/collections sdk.eshop.payment.recordCollection() /v1/stores/{store_id}/payments/{id}/collections/{collection_id}/void sdk.eshop.payment.voidCollection() /v1/stores/{store_id}/payments/{id}/resolve-hold sdk.eshop.payment.resolveHold() /v1/stores/{store_id}/payments/{id}/resolve-charge sdk.eshop.payment.resolveCharge() /v1/stores/{store_id}/payments/{id}/refunds/{refund_id}/resolve sdk.eshop.payment.resolveRefund() /v1/stores/{store_id}/incoming-webhooks sdk.notification.incomingWebhook.find() interface Payment {
id: string;
store_id: string;
order_id: string;
payment_option_id: string;
money: Money;
type:
| ({ type: "stripe_checkout" } & StripeCheckoutPayment)
| ({ type: "stripe_card" } & StripeCardPayment)
| ({ type: "monri_checkout" } & MonriCheckoutPayment)
| ({ type: "monri_card" } & MonriCardPayment)
| ({ type: "cash_on_delivery" } & RecordedPayment)
| ({ type: "manual" } & ManualPayment);
holds: { id: string; message: string; opened_at: EpochMilliseconds; status: { type: "open" } | { type: "resolved"; by: AccountActor; note: string; at: EpochMilliseconds } }[];
status: "pending" | "open" | "requires_action" | "paid" | "expired" | "cancelled" | "declined" | "failed" | "unknown" | "closing";
status_revision: string;
created_at: EpochMilliseconds;
updated_at: EpochMilliseconds;
}
- Who may do what: staff with
ordersread payments and manage saved cards; onlypaymentsmoves money or records money moving (refunds, collections and their voids, settling unknown results, resolving holds). A pickup clerk withfulfillmentreach at the location records the cash of a cash-on-delivery handover there. - One record per payment. Its refunds, disputes and recorded money are entries inside it, and refunds never exceed what was paid. Refunds and recorded collections have their own commands on the payment.
- Cash and manual payments count as paid once their active recorded money reaches what is due. Cancelling the whole order closes an open one. A collection recorded by mistake is voided with a reason: it stays in the history, stops counting, moves no money and sends no email; a wrong amount is fixed by voiding it and recording the right one.
- Cash and manual refunds are one payout: a requested refund is recorded
sentwith the reference of the payout, or cancelled; money that comes back makes itreturned, and paying it again is a new refund. - Holds open when facts disagree, such as money arriving after a payment ended, and stay open until a person resolves them by id with a note; they stay in the history.
- Unknown Monri results: when Monri’s answer to a card charge or a refund never arrived, staff
check Monri and record what happened with
resolveChargeorresolveRefund:{ type: "made", transaction_id }or{ type: "not_made" }, with the payment’sexpected_updated_at. The charge or the refund then readsconfirmed_madeorconfirmed_not_made. - Saved cards are payment methods:
requestSetup,startSetup,completeSetup. A card belongs to itsowner, a customer, a company or a company location, and pays only for that buyer. Saving one records thelanguageand the exacttermsthe buyer accepted, which must be the option’s currentsaved_card_termsin that language (409 PAYMENT_METHOD.TERMS_CHANGEDorTERMS_UNAVAILABLEotherwise). A card can’t be revoked while a customer group member that isn’t cancelled renews with it (409 PAYMENT_METHOD.IN_USE, naming the member). - Incoming webhooks are the store’s inbox for posts from its Stripe and Monri options and its own
Resend account, unique per provider event id:
waiting(new, retried, orunmatcheduntil its record appears),processing,applied,ignored, or inreviewuntil staff withpaymentsresolve it withapplyorignore. - A refund that succeeds sends the
order_refundedemail, once per refund, when its request said so. Webhooks postrefund.created,refund.succeededandrefund.failed, each with{ order, payment_id, refund_id };refund.succeededalso carriesnotify_customer.
Returns And Rentals
/v1/stores/{store_id}/returns sdk.eshop.return.create() /v1/stores/{store_id}/returns/{id}/execute sdk.eshop.return.execute() /v1/stores/{store_id}/returns/{id}/credit sdk.eshop.return.credit() /v1/stores/{store_id}/rentals sdk.eshop.rental.find() /v1/stores/{store_id}/rentals/{id}/commands sdk.eshop.rental.execute() A return is an order return or a rental return. A customer’s return starts requested; staff
approve it, opening it at a decided location, or decline it with a reason, and each choice carries
notify_customer to say whether the customer gets the return_approved or return_declined email.
A return staff create names the location that receives it (409 RETURN.DESTINATION_REQUIRED
without one), so creating one needs orders and fulfillment at every location. Staff move a return with one command and the expected_updated_at they read; a
receipt, disposal or missing record that closes it posts its own webhook, then return.closed. A
return never changes money by itself: once units of an order return are received, staff credit them
on the order with credit, under a credit id the app picks and the return’s expected_updated_at,
each received unit once, and refund the payment separately. Customers start returns from the storefront with
arky.eshop.return.create, using getOrderOptions and findRentalOptions to see what can go back. A
rental is one rented item of a customer group member, with
the terms in force; staff with orders request a replacement unit for it.
Inventory
/v1/stores/{store_id}/inventory-items sdk.eshop.inventoryItem.create() /v1/stores/{store_id}/inventory-levels sdk.eshop.inventoryLevel.find() /v1/stores/{store_id}/inventory-movements sdk.eshop.inventoryMovement.record() /v1/stores/{store_id}/inventory-units sdk.eshop.inventoryUnit.receive() An inventory item is a stocked thing, such as one product model; variants name the items they
need. It is tracked by quantity, individual (one unit record per piece) or untracked (no
counted stock). An inventory level is its stock at one location (on_hand, reserved,
unavailable and the available rest), changed by stock actions that each carry an action_id the
app picks. Movements are the stock log with
typed reasons; a dispatched row names the fulfillment that took the goods out. Inventory units
track one serialized item through receiving, allocation, moves, returns and write-offs.
Companies
/v1/stores/{store_id}/companies sdk.companies.create() /v1/stores/{store_id}/company-locations sdk.companies.location.create() /v1/stores/{store_id}/companies/{id}/purchasing sdk.companies.setPurchasing() /v1/stores/{store_id}/company-locations/{id}/purchasing sdk.companies.location.setPurchasing() /v1/stores/{store_id}/companies/{id}/tax/registrations sdk.companies.submitTaxRegistration() /v1/stores/{store_id}/companies/{id}/tax/registrations/review sdk.companies.reviewTaxRegistration() /v1/stores/{store_id}/company-roles sdk.companies.role.create() /v1/stores/{store_id}/company-memberships sdk.companies.membership.create() await sdk.companies.location.setPurchasing({
store_id: storeId,
id: downtown.id,
expected_updated_at: downtown.updated_at,
purchasing: {
payment: { type: "on_account", terms: { type: "net_days", days: 30 }, billing_address: downtownBilling },
allowed_payment_option_ids: [bankTransfer.id],
purchase_order_number_required: true,
},
});
await sdk.companies.membership.create({
store_id: storeId,
id: crypto.randomUUID(),
company_id: northwind.id,
email: "buyer@northwind.example",
language: "en",
grants: [{ scope: { type: "company_location", company_location_id: downtown.id }, role_ids: [buyerRole.id] }],
});
- A company is a business buyer with a name, a contact email and blocks. A company and each of
its locations have their own purchasing policy:
standard_checkout, oron_accountwith payment terms such as net 30 and a billing address; the payment options they may use (an empty list allows none); and whether a purchase order number is required. A location never inherits its company’s policy. Only staff set purchasing, tax, blocks and the location a company location is served from; a new company or location pays at checkout with no payment option allowed until staff list some. - A company and each location keep their own tax registrations, which staff submit and then verify or reject. Orders carry the buyer’s verified registrations.
- Memberships add a person by email, with grants, each for the whole company (which covers all
its locations, now and later) or for one location, with company roles for that scope. A verified
customer with that email is active at once; anyone else is
invitedand becomes active the moment they verify that email by signing in, with nothing to accept. Adding someone sends the store’scompany_inviteemail in the language named. Permissions areplace_orders,join_customer_groups,access_digital_products,view_own_orders,view_company_orders,view_own_customer_groups,view_company_customer_groups,manage_company,manage_addresses,manage_members,manage_company_customer_groups,manage_payment_methodsandadmin. - A member with
manage_companyedits the company’s name and contact email. A member withmanage_addressesedits a location’s name, shipping address and billing address; for the whole company it also edits the company’s billing address and creates and removes its locations. - While other members are active, one active membership must keep
manage_members(oradmin) for the whole company: removing, disabling or narrowing it, or editing one of its roles, is refused with409 COMPANY_MEMBERSHIP.LAST_MANAGERuntil another member holds it. Adding someone and an invite turning active are never refused by this rule, so a company’s buyers can be active before its first manager signs in. - Members read their companies, locations and grants with
arky.companieson the storefront.
Customer groups are on Customer Groups; promotions on Promotions; shipping and fulfillment on Shipping and Fulfillment.