CRM
Contacts, storefront account sessions, and contact lists
The CRM module manages contact records and contact lists for your store. Contacts are store-scoped. A contact can be linked to orders and contact lists, and can authenticate via email magic codes.
Key Concepts
Contact vs Account
- Account — platform-level auth identity. One person, one Account across all stores.
- Contact — store-level CRM record with one email, verification state, status, and taxonomies. Each store has its own view of a contact.
Taxonomies
Contact classification and searchable custom fields are stored as taxonomies — the same content model used across the platform. There is no direct contact tags field; tags and segments are taxonomy entries.
Email And Channels
Contacts can exist without email. Email, phone, and social identities live on contact channels, and verified email/phone/provider IDs can resolve back to an existing contact.
Storefront Account Flow
Buyer-facing flows use Account language. Under the hood, account sessions attach to contacts.
/v1/storefront/{storeId}/account/identify arky.identify() /v1/storefront/{storeId}/account/verify arky.verify() /v1/storefront/{storeId}/account/me arky.me() /v1/storefront/{storeId}/account/logout arky.logout() Admin Contact Endpoints
Create Contact (Admin)
/v1/stores/{storeId}/contacts sdk.crm.contact.create() Creates a new contact. If a contact with the same email already exists, returns the existing one (idempotent).
Parameters
| Name | Type | Description |
|---|---|---|
email required | string | Primary email for the contact |
taxonomies optional | TaxonomyEntry[] | Taxonomy entries for segments, lifecycle stage, or other custom classification |
Get Contact
/v1/stores/{storeId}/contacts/{id} sdk.crm.contact.get() Find Contacts
/v1/stores/{storeId}/contacts sdk.crm.contact.find() Search and list contacts.
Parameters
| Name | Type | Description |
|---|---|---|
query optional | string | Search across contact email, ID, and indexed taxonomy values |
taxonomy_query optional | TaxonomyQuery[] | Filter by taxonomy entries |
status optional | 'active' | 'archived' | Filter by contact status |
limit optional | number | Items per page |
cursor optional | string | Pagination cursor |
sort_field optional | string | Field to sort by (e.g. createdAt) |
sort_direction optional | string | asc or desc |
Update Contact
/v1/stores/{storeId}/contacts/{id} sdk.crm.contact.update() Parameters
| Name | Type | Description |
|---|---|---|
id required | string | Contact ID |
email optional | string | Replace the contact's email |
taxonomies optional | TaxonomyEntry[] | Replace taxonomy entries |
status optional | 'active' | 'archived' | Contact status |
Merge Contacts
/v1/stores/{storeId}/contacts/{id}/merge sdk.crm.contact.merge() Merges a source contact into the target. The source is permanently deleted (GDPR-compliant). Orders and contact list entries are moved onto the target contact.
Revoke Session Token
/v1/stores/{storeId}/contacts/{id}/sessions/{tokenId} sdk.crm.contact.revokeToken() Revokes a single active session for a contact.
Revoke All Session Tokens
/v1/stores/{storeId}/contacts/{id}/sessions sdk.crm.contact.revokeAllTokens() Revokes every active session for a contact (logout everywhere).
Contact Object
{
"id": "uuid",
"store_id": "uuid",
"email": "[email protected]",
"verified": true,
"status": "active",
"taxonomies": [],
"promo_usage": [],
"auth_tokens": [],
"verification_codes": [],
"created_at": 1234567890,
"updated_at": 1234567890
}
Contact Lists
Contact Lists are contact groups used for subscriptions, content gating, paid access, and outreach audience sourcing. They live under the CRM module as sdk.crm.contactList.*.
Key Concepts
A contact list is a subscriber group with two dimensions:
- Type:
standard(free),confirmation(double opt-in), orpaid(Stripe checkout) - Confirmation: Optional double opt-in via an email template (
type.confirm_template_id)
Use cases include product updates, premium memberships, course access, tiered content, and reusable audience sources for campaigns.
Contact lists collect and organize subscribers. Campaigns send broadcasts and outreach email to contact-list members.
Create Contact List
/v1/stores/{storeId}/contact-lists sdk.crm.contactList.create() Parameters
| Name | Type | Description |
|---|---|---|
key required | string | Unique contact list identifier (alphanumeric, hyphens, underscores) |
name optional | string | Display name |
description optional | string | Internal description |
type optional | ContactListType | standard (default), confirmation, or paid with prices |
type.confirm_template_id optional | string | Email template ID for double opt-in confirmation lists |
Get Contact List
/v1/stores/{storeId}/contact-lists/{id} sdk.crm.contactList.get() Look up a contact list by ID.
Find Contact Lists
/v1/stores/{storeId}/contact-lists sdk.crm.contactList.find() Parameters
| Name | Type | Description |
|---|---|---|
ids optional | string[] | Filter by specific contact list IDs |
status optional | string | Filter by status (active, archived) |
query optional | string | Search in contact list keys and names |
limit optional | number | Items per page (default 50) |
cursor optional | string | Pagination cursor |
Update Contact List
/v1/stores/{storeId}/contact-lists/{id} sdk.crm.contactList.update() The type cannot be changed after creation, but you can update the key, name, description, and status.
Subscribe to Contact List
/v1/storefront/{storeId}/contact-lists/{id}/subscribe sdk.crm.contactList.subscribe() Subscribe a contact to a contact list. Behavior depends on the contact list configuration:
- Standard, no confirmation: Subscription is immediately active
- Standard, with confirmation: Subscription is
pendinguntil the user clicks the confirmation link - Paid: Returns a Stripe checkout URL
Parameters
| Name | Type | Description |
|---|---|---|
id required | string | Contact List ID |
contact_id required | string | Contact ID (obtain via arky.identify or arky.me in storefront flows) |
price_id optional | string | Stripe price ID (required for paid contact lists) |
success_url optional | string | Redirect URL after successful payment |
cancel_url optional | string | Redirect URL if user cancels payment |
confirm_url optional | string | Base URL for the confirmation link |
Check Access
/v1/storefront/{storeId}/contact-lists/{id}/access sdk.crm.contactList.checkAccess() Check if the current contact has access to a contact list.
Manage Or Leave A Contact List
Contact-list emails link to /customer/contact-lists/manage?store_id={storeId}&token={membershipToken}. The page loads sanitized list metadata, membership status, content-access targets, and the current access result without exposing the contact or other members.
/v1/storefront/{storeId}/contact-lists/manage arky.crm.contactList.manage() /v1/storefront/{storeId}/contact-lists/unsubscribe arky.crm.contactList.unsubscribe() const state = await arky.crm.contactList.manage(membershipToken);
if (state.has_access) {
await arky.crm.contactList.unsubscribe(membershipToken);
}
Standard and confirmation memberships cancel immediately. A recurring paid membership schedules provider cancellation and keeps access through the paid period end.
Paid period-end access does not keep subscription email active: cancellation_scheduled memberships retain content entitlement but stop receiving contact-list-backed campaign messages immediately.
Contact-list-backed newsletter_email messages also include the RFC 8058 List-Unsubscribe and List-Unsubscribe-Post headers. The header URL uses the same opaque membership token and accepts only POST; a normal GET cannot cancel membership. The visible template link still opens the management page.
Get Contacts
/v1/stores/{storeId}/contact-lists/{id}/contacts sdk.crm.contactList.findMembers() List Contacts in a Contact List (admin only).
Add Contact
/v1/stores/{storeId}/contact-lists/{id}/contacts/{contactId} sdk.crm.contactList.addMember() Add a Contact to a Contact List by contact ID (admin only). Skips if already present.
Remove Contact
/v1/stores/{storeId}/contact-lists/{id}/contacts/{contactId} sdk.crm.contactList.removeMember() Import Contacts Into Contact List
/v1/stores/{storeId}/contact-lists/{id}/contacts/import sdk.crm.contactList.importContacts() Upserts contacts and adds them to a contact list in one transient operation. The API returns row counts and errors; it does not create a stored import record.
Campaigns
Campaigns are one-off audience email sends or sequences. Create a campaign, choose mailbox sender(s), choose email templates for each step, import enrollments from contact lists, contacts, or manual emails, then launch it.
Campaign enrollments are fixed for that campaign once imported. Replies and sent messages stay attached to that campaign conversation.
Create Campaign
/v1/stores/{storeId}/campaigns sdk.crm.campaign.create() Import Campaign Enrollments
/v1/stores/{storeId}/campaigns/{id}/enrollments/import sdk.crm.campaign.importEnrollments() Import from one contact list, multiple contact lists, individual contacts, or manual emails.
Launch Campaign
/v1/stores/{storeId}/campaigns/{id}/launch sdk.crm.campaign.launch() await sdk.crm.campaign.launch({ id: campaign.id });
Campaign messages keep the template ID, template variables, rendered subject, rendered HTML, rendered text, attachments, and mailbox ID. Editing the template later does not mutate sent campaign messages.
Duplicate Campaign
/v1/stores/{storeId}/campaigns/{id}/duplicate sdk.crm.campaign.duplicate() Duplicate a campaign when you want to reuse the setup for a new one-off send.
const draft = await sdk.crm.campaign.duplicate({
id: campaign.id,
name: 'Weekly Newsletter - July',
copy_enrollments: true
});
Duplicating copies campaign setup and the current enrollment set into a new draft. It does not copy sent messages, replies, or conversation history.
Typical Flows
E-commerce Checkout
import { initialize } from "arky-sdk/storefront";
const arky = initialize({ baseUrl, storeId });
// 1. Attach the shopper's account/contact identity
await arky.identify({ email: shippingAddress.email });
// 2. Checkout the cart; the backend resolves the contact from the session token
await arky.eshop.cart.checkout({
shipping_address: shippingAddress,
clear_after_checkout: true
});
Contact-list Subscription with Double Opt-in
const arky = initialize({ baseUrl, storeId });
const session = await arky.identify({ email: "[email protected]" });
// Subscribe to the contact list (sends confirmation email when configured)
await arky.crm.contactList.subscribe({
id: contactListId,
contact_id: session.contact.id,
confirm_url: 'https://yoursite.com/confirm'
});
Account Login (Magic Code)
const arky = initialize({ baseUrl, storeId });
// 1. Ask for a code
await arky.identify({ email: "[email protected]", verify: true });
// 2. User enters the code from their email
await arky.verify({ code: "123456" });
// 3. The storefront SDK persists the contact session
const account = await arky.me();
If you delete a confirmation email template, any contact list referencing it will automatically lose its double opt-in and fall back to single opt-in.