Back to Arky

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.

POST /v1/storefront/{storeId}/account/identify
SDK: arky.identify()
POST /v1/storefront/{storeId}/account/verify
SDK: arky.verify()
GET /v1/storefront/{storeId}/account/me
SDK: arky.me()
POST /v1/storefront/{storeId}/account/logout
SDK: arky.logout()

Admin Contact Endpoints

Create Contact (Admin)

POST /v1/stores/{storeId}/contacts
SDK: 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

GET /v1/stores/{storeId}/contacts/{id}
SDK: sdk.crm.contact.get()

Find Contacts

GET /v1/stores/{storeId}/contacts
SDK: 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

PUT /v1/stores/{storeId}/contacts/{id}
SDK: 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

POST /v1/stores/{storeId}/contacts/{id}/merge
SDK: 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

DELETE /v1/stores/{storeId}/contacts/{id}/sessions/{tokenId}
SDK: sdk.crm.contact.revokeToken()

Revokes a single active session for a contact.

Revoke All Session Tokens

DELETE /v1/stores/{storeId}/contacts/{id}/sessions
SDK: 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), or paid (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.

Note

Contact lists collect and organize subscribers. Campaigns send broadcasts and outreach email to contact-list members.

Create Contact List

POST /v1/stores/{storeId}/contact-lists
SDK: 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

GET /v1/stores/{storeId}/contact-lists/{id}
SDK: sdk.crm.contactList.get()

Look up a contact list by ID.

Find Contact Lists

GET /v1/stores/{storeId}/contact-lists
SDK: 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

PUT /v1/stores/{storeId}/contact-lists/{id}
SDK: 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

POST /v1/storefront/{storeId}/contact-lists/{id}/subscribe
SDK: 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 pending until 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

GET /v1/storefront/{storeId}/contact-lists/{id}/access
SDK: 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.

POST /v1/storefront/{storeId}/contact-lists/manage
SDK: arky.crm.contactList.manage()
POST /v1/storefront/{storeId}/contact-lists/unsubscribe
SDK: 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

GET /v1/stores/{storeId}/contact-lists/{id}/contacts
SDK: sdk.crm.contactList.findMembers()

List Contacts in a Contact List (admin only).

Add Contact

POST /v1/stores/{storeId}/contact-lists/{id}/contacts/{contactId}
SDK: sdk.crm.contactList.addMember()

Add a Contact to a Contact List by contact ID (admin only). Skips if already present.

Remove Contact

DELETE /v1/stores/{storeId}/contact-lists/{id}/contacts/{contactId}
SDK: sdk.crm.contactList.removeMember()

Import Contacts Into Contact List

POST /v1/stores/{storeId}/contact-lists/{id}/contacts/import
SDK: 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

POST /v1/stores/{storeId}/campaigns
SDK: sdk.crm.campaign.create()

Import Campaign Enrollments

POST /v1/stores/{storeId}/campaigns/{id}/enrollments/import
SDK: sdk.crm.campaign.importEnrollments()

Import from one contact list, multiple contact lists, individual contacts, or manual emails.

Launch Campaign

POST /v1/stores/{storeId}/campaigns/{id}/launch
SDK: 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

POST /v1/stores/{storeId}/campaigns/{id}/duplicate
SDK: 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();
Tip

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.