Back to Arky

Customers

Anonymous and verified customers, sessions, sign-in codes and email changes, suppressions, and notes

A customer is one person on one store’s website. Every visitor is a customer: a browser’s first write (a cart, a chat, a form or a tracked action) creates an anonymous customer, and it becomes verified only when a sign-in code proves an email. Storefront sessions are a visitor’s browser session or an email-authenticated one. Platform accounts are a separate operator identity.

import type { EpochMilliseconds } from "arky-sdk";

interface Customer {
	id: string;
	store_id: string;
	type:
		| { type: "anonymous" }
		| {
				type: "verified";
				email: string;
				addresses: CustomerAddress[];
				default_shipping_address_id: string | null;
				default_billing_address_id: string | null;
		  };
	profile: Block[];
	blocks: Block[];
	created_at: EpochMilliseconds;
	updated_at: EpochMilliseconds;
}
  • An email is proof, not profile data. An email someone typed but never proved is never stored on a customer: it stays on the order, form submission or conversation where it was typed. Nobody can write an email onto a customer or sign one in, staff and the API included; only an accepted code does. A verified email is unique in the store.
  • Only a verified customer has an address book, of up to 100 addresses; each default points into it.
  • profile holds details the signed-in customer reads and edits; blocks holds details only staff and the API see, such as imported data.
  • An anonymous customer can browse, keep a cart, check out products and bookings as a guest, chat, send forms and be tracked. Only a verified customer can hold a customer group member, own a saved card, hold a company membership, be named in catalog access or a promotion, buy a group line, use a signed-in-only promotion or accept an offer.

Storefront Customer Flow

POST /v1/storefront/customer/identify
SDK: arky.customer.identify()
POST /v1/storefront/customer/request-code
SDK: arky.customer.requestCode()
POST /v1/storefront/customer/verify
SDK: arky.customer.verify()
POST /v1/storefront/customer/me/change-email
SDK: arky.customer.changeEmail()
POST /v1/storefront/customer/refresh
SDK: arky.customer.refresh()
POST /v1/storefront/customer/logout
SDK: arky.customer.logout()
GET /v1/storefront/customer/me
SDK: arky.customer.getMe()
PATCH /v1/storefront/customer/me
SDK: arky.customer.updateMe()
POST /v1/storefront/customer/me/unsubscribe
SDK: arky.customer.unsubscribe()
POST /v1/storefront/customer/me/resubscribe
SDK: arky.customer.resubscribe()
import { initialize } from "arky-sdk/storefront";
import { createOrderToken } from "arky-sdk/utils";

const arky = initialize("arky_pk_...", {
	locale: "en",
	createVisitorIdentity: () => ({
		customer_id: crypto.randomUUID(),
		session_id: crypto.randomUUID(),
	}),
	createSessionId: () => crypto.randomUUID(),
	createId: () => crypto.randomUUID(),
	createOrderToken,
});

const requested = await arky.customer.requestCode({
	email: "ana@example.com",
	language: "en",
});

await arky.customer.verify({ code: "123456" });

const me = await arky.customer.getMe();
console.log(me.customer.type, me.email_unsubscribed);
  • identify starts a visitor session with a new anonymous customer, under ids the storefront picks. The SDK calls it the first time a stateful call needs a session and reuses that session afterwards. A visitor session lasts until 365 days after it was last used.
  • requestCode sends a code with the template the store selected for sign_in_code. The code email’s id comes from the app’s createId, or the id you pass. A retry after a lost answer for the same address and language reuses it, so one email goes out; a request after an answer, such as “send another code”, gets a new one. language must be one of the store’s languages. A request is refused at once while the store has no sign_in_code email selected, the template has no text in that language, the sending address isn’t ready or the store’s email is paused. A code lasts ten minutes; an address gets at most one new code a minute, and after five wrong tries every try for that address is refused until the code expires. Once a code is accepted, its wrong tries stop counting. With a bot check on the key, send the provider’s token as bot_check_token.
  • verify checks the code and makes a new email-authenticated session under an id from the app’s createSessionId. If no verified customer has the email, the anonymous customer becomes verified and keeps its id. If another customer already has it, everything the anonymous customer owns (its carts, orders, conversations, form submissions, actions) moves to that customer and the session continues as them. Either way, company invites for the email become active.
  • refresh swaps the refresh token for a new signed-in session, under the session_id the app picks, with new access and refresh tokens. The SDK refreshes on its own with an id from createSessionId; when a signed-in session can’t be refreshed, it clears it and reports the sign-out.
  • updateMe carries expected_updated_at and changes the signed-in customer’s own profile, saved addresses (the whole list) and default addresses. It never changes the email.

Changing The Email

On a signed-in session, requestCode starts an email change instead of a sign-in, and the code goes to the new address:

await arky.customer.requestCode({
	email: "ana.new@example.com",
	language: "en",
});

const me = await arky.customer.changeEmail({ code: "654321" });
  • The request is refused while another verified customer holds the new address, and while the store has no email_changed email selected.
  • changeEmail proves the code. The session stays signed in, the email becomes the new verified address, and the customer’s other signed-in sessions end. Nothing merges, but company invites for the new address become active.
  • The old address gets the store’s email_changed email in the language of the code request.
  • A refresh keeps a pending change: it moves to the new session with its wrong tries, and the code still completes it there.

Unsubscribing

unsubscribe records the store’s unsubscribe for the signed-in customer’s verified address, so broadcasts stop reaching it; resubscribe lifts it. email_unsubscribed on getMe says whether one is in place. The storefront can’t lift a staff block, a bounce or a complaint.

Important

The arky_pk_... publishable key identifies the store and is safe in browser code. Visitor, access and refresh tokens are returned only when they are issued; the SDK keeps them in its own storage. Never put a secret key in storefront code.

Admin Customers

GET /v1/stores/{store_id}/customers
SDK: sdk.customers.find()
POST /v1/stores/{store_id}/customers
SDK: sdk.customers.create()
GET /v1/stores/{store_id}/customers/{id}
SDK: sdk.customers.get()
PATCH /v1/stores/{store_id}/customers/{id}
SDK: sdk.customers.update()
POST /v1/stores/{store_id}/customers/{id}/erase
SDK: sdk.customers.erase()
POST /v1/stores/{store_id}/customers/{id}/merge
SDK: sdk.customers.merge()
POST /v1/stores/{store_id}/customers/import/preview
SDK: sdk.customers.previewImport()
POST /v1/stores/{store_id}/customers/import
SDK: sdk.customers.import()
const customer = await sdk.customers.create({
	store_id: storeId,
	id: crypto.randomUUID(),
	profile: [{ id: crypto.randomUUID(), key: "first_name", type: "text", value: "Ana" }],
	blocks: [{ id: crypto.randomUUID(), key: "phone", type: "text", value: "+49 30 000000" }],
});
  • Staff and the API create anonymous customers, for a phone or walk-in order or an import. An import puts the old details, such as email, name and phone, into profile or blocks; an email there is plain data that signs nobody in and never receives broadcasts.
  • find lists verified customers and anonymous customers with at least one order (shown by their latest order’s contact), with their has_cart, has_customer_action and has_order facts, and filters by type, text and those facts.
  • update changes profile, blocks and a verified customer’s addresses and defaults; an omitted field stays as it is.
  • merge({ store_id, id, target_customer_id, expected_updated_at }) moves the customer named by id into a verified target, for example one person with two emails: its orders, carts, conversations, form submissions, actions, experiment assignments, returns, notes, profile and blocks (the target’s value wins on the same key), its customer group members, saved cards, company memberships and addresses. It is refused while both hold a member that isn’t cancelled of the same group or offering, or a membership of the same company. Every session of the merged-away customer ends, and webhooks get one customer.merged naming both.
  • erase({ store_id, id, expected_updated_at }) leaves the customer anonymous: its email, address book, profile and blocks are cleared and every session ends. It deletes the customer’s open carts, conversations, form submissions, actions, experiment assignments, promotion use counts, saved cards, company memberships and notes. Orders and cancelled members keep the bare customer, and email suppressions stay. Erasing is refused while the customer holds a member that isn’t cancelled, or a company’s last manager role while others are active.

Sessions

GET /v1/stores/{store_id}/customers/{customer_id}/sessions
SDK: sdk.customers.findSessions()
POST /v1/stores/{store_id}/customers/{customer_id}/sessions/{session_id}/revoke
SDK: sdk.customers.revokeSession()
POST /v1/stores/{store_id}/customers/{customer_id}/sessions/revoke
SDK: sdk.customers.revokeAllSessions()

A session’s type is visitor (with its pending code request, if any) or email_authenticated (with its pending email change, if any), and its status is active, superseded or revoked. Admin reads never include tokens, hashes or codes.

Email Suppressions

One “don’t email this address” entry per store, address and type: unsubscribe (the broadcast unsubscribe link, the storefront or a team member), block (a team member blocked the address), hard_bounce and complaint (from the store’s Resend reports).

GET /v1/stores/{store_id}/email-suppressions
SDK: sdk.customers.emailSuppression.find()
GET /v1/stores/{store_id}/email-suppressions/{id}
SDK: sdk.customers.emailSuppression.get()
POST /v1/stores/{store_id}/email-suppressions/block
SDK: sdk.customers.emailSuppression.block()
POST /v1/stores/{store_id}/email-suppressions/record-unsubscribe
SDK: sdk.customers.emailSuppression.recordUnsubscribe()
POST /v1/stores/{store_id}/email-suppressions/{id}/unblock
SDK: sdk.customers.emailSuppression.unblock()
POST /v1/stores/{store_id}/email-suppressions/{id}/record-resubscribe
SDK: sdk.customers.emailSuppression.recordResubscribe()
interface EmailSuppression {
	id: string;
	store_id: string;
	email: string;
	type: "unsubscribe" | "block" | "hard_bounce" | "complaint";
	status:
		| {
				type: "active";
				cause:
					| { type: "storefront"; customer_session_id: string }
					| { type: "unsubscribe_link"; broadcast_id: string }
					| { type: "account"; actor: AccountActor; note: string }
					| { type: "report" };
		  }
		| {
				type: "released";
				by: { type: "account"; actor: AccountActor; note: string } | { type: "storefront"; customer_session_id: string };
		  };
	created_at: EpochMilliseconds;
	updated_at: EpochMilliseconds;
}
  • A block, a hard bounce or a complaint stops every store email to the address except sign-in codes; an unsubscribe stops broadcasts only, without ending any membership. A new entry changes no waiting email by itself: Arky checks the address right before each send, so a waiting email to it ends not_sent with the reason recipient_suppressed.
  • A team member blocks or records an unsubscribe with an id the app picks and a note; unblock and record-resubscribe carry the note and the expected_updated_at they read. Releasing a hard bounce or complaint also removes the address from the store’s Resend suppression list.
  • Opening a broadcast’s unsubscribe link only shows its confirmation; the one-click post records the unsubscribe, so a link scanner unsubscribes nobody.
  • A signed-in customer lifts their own unsubscribe with resubscribe, or by signing up again for a free group they already hold.

Customer Groups

A verified customer, a company or a company location becomes a member of a customer group by buying it or when the team assigns them. Membership gives member prices, catalogs, deliveries or newsletters; it is never a list staff keep by hand.

const memberships = await sdk.eshop.customerGroupMember.find({ store_id: storeId, customer_id: customer.id });

Staff Notes

GET /v1/stores/{store_id}/customers/{customer_id}/notes
SDK: sdk.customers.notes.find()
POST /v1/stores/{store_id}/customers/{customer_id}/notes
SDK: sdk.customers.notes.create()
PUT /v1/stores/{store_id}/customers/{customer_id}/notes/{id}
SDK: sdk.customers.notes.update()
DELETE /v1/stores/{store_id}/customers/{customer_id}/notes/{id}?expected_updated_at=
SDK: sdk.customers.notes.delete()
const note = await sdk.customers.notes.create({
	store_id: storeId,
	customer_id: customer.id,
	id: crypto.randomUUID(),
	body: "Prefers delivery after 14:00.",
});

A Note is { id, store_id, target, body, actor, created_at, updated_at }, where target names a customer, a company, an order, a form submission or a conversation. A note holds at most 2,000 characters and a record at most 500 notes. Customers never see notes. Only the author edits a note; the author or an admin deletes it, with the expected_updated_at they read.

Timeline And Events

Each customer’s timeline is their customer actions: the storefront’s own action keys, and the moments Arky records itself (cart items added and removed, orders placed, forms submitted, support conversations started, handed to the team and resolved, and customer group members activated, cancelled and switched).

Webhooks: customer.verified (when an email is first proven), customer.updated, customer.merged and customer.erased. A new anonymous customer posts nothing.