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.
profileholds details the signed-in customer reads and edits;blocksholds 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
/v1/storefront/customer/identify arky.customer.identify() /v1/storefront/customer/request-code arky.customer.requestCode() /v1/storefront/customer/verify arky.customer.verify() /v1/storefront/customer/me/change-email arky.customer.changeEmail() /v1/storefront/customer/refresh arky.customer.refresh() /v1/storefront/customer/logout arky.customer.logout() /v1/storefront/customer/me arky.customer.getMe() /v1/storefront/customer/me arky.customer.updateMe() /v1/storefront/customer/me/unsubscribe arky.customer.unsubscribe() /v1/storefront/customer/me/resubscribe 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);
identifystarts 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.requestCodesends a code with the template the store selected forsign_in_code. The code email’s id comes from the app’screateId, or theidyou 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.languagemust be one of the store’s languages. A request is refused at once while the store has nosign_in_codeemail 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 asbot_check_token.verifychecks the code and makes a new email-authenticated session under an id from the app’screateSessionId. 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.refreshswaps the refresh token for a new signed-in session, under thesession_idthe app picks, with new access and refresh tokens. The SDK refreshes on its own with an id fromcreateSessionId; when a signed-in session can’t be refreshed, it clears it and reports the sign-out.updateMecarriesexpected_updated_atand changes the signed-in customer’s ownprofile, 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_changedemail selected. changeEmailproves 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_changedemail in the language of the code request. - A
refreshkeeps 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.
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
/v1/stores/{store_id}/customers sdk.customers.find() /v1/stores/{store_id}/customers sdk.customers.create() /v1/stores/{store_id}/customers/{id} sdk.customers.get() /v1/stores/{store_id}/customers/{id} sdk.customers.update() /v1/stores/{store_id}/customers/{id}/erase sdk.customers.erase() /v1/stores/{store_id}/customers/{id}/merge sdk.customers.merge() /v1/stores/{store_id}/customers/import/preview sdk.customers.previewImport() /v1/stores/{store_id}/customers/import 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
profileorblocks; an email there is plain data that signs nobody in and never receives broadcasts. findlists verified customers and anonymous customers with at least one order (shown by their latest order’s contact), with theirhas_cart,has_customer_actionandhas_orderfacts, and filters bytype, text and those facts.updatechangesprofile,blocksand 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 byidinto a verified target, for example one person with two emails: its orders, carts, conversations, form submissions, actions, experiment assignments, returns, notes,profileandblocks(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 onecustomer.mergednaming both.erase({ store_id, id, expected_updated_at })leaves the customer anonymous: its email, address book,profileandblocksare 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
/v1/stores/{store_id}/customers/{customer_id}/sessions sdk.customers.findSessions() /v1/stores/{store_id}/customers/{customer_id}/sessions/{session_id}/revoke sdk.customers.revokeSession() /v1/stores/{store_id}/customers/{customer_id}/sessions/revoke 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).
/v1/stores/{store_id}/email-suppressions sdk.customers.emailSuppression.find() /v1/stores/{store_id}/email-suppressions/{id} sdk.customers.emailSuppression.get() /v1/stores/{store_id}/email-suppressions/block sdk.customers.emailSuppression.block() /v1/stores/{store_id}/email-suppressions/record-unsubscribe sdk.customers.emailSuppression.recordUnsubscribe() /v1/stores/{store_id}/email-suppressions/{id}/unblock sdk.customers.emailSuppression.unblock() /v1/stores/{store_id}/email-suppressions/{id}/record-resubscribe 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_sentwith the reasonrecipient_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_atthey 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
/v1/stores/{store_id}/customers/{customer_id}/notes sdk.customers.notes.find() /v1/stores/{store_id}/customers/{customer_id}/notes sdk.customers.notes.create() /v1/stores/{store_id}/customers/{customer_id}/notes/{id} sdk.customers.notes.update() /v1/stores/{store_id}/customers/{customer_id}/notes/{id}?expected_updated_at= 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.