Endpoints

All endpoints are read-only (GET) and live under /api/v1.

Tenant

`GET /me`

Returns the authenticated creator's basic info and current plan.

Contacts

`GET /contacts`

Canonical contact feed for CRM and email-marketing sync. Requires both contacts:read and consents:read. Each row has a stable id/contact_id, nullable user_id, contact fields, first/last seen timestamps, updated_at, archive state, and the current marketing-consent snapshot.

API keys created before these scopes were introduced are not expanded silently. Generate a new key before switching an existing integration to this endpoint.

Query params:

  • limit (default 50, max 200), cursor, created_after, created_before
  • updated_since - ISO 8601 timestamp for incremental sync
  • marketing_consent - exactly granted, withdrawn, or none; never infer consent from lead/customer membership
  • include_archived=true - include archived contacts

The cursor is opaque and ordered by updated_at plus contact id. Save meta.next_cursor and reuse it unchanged.

Example incremental marketing sync:

curl --get "https://zemers.com/api/v1/contacts" \
  --header "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "marketing_consent=granted" \
  --data-urlencode "updated_since=2026-08-30T00:00:00.000Z"

Poll /events with a key that has both events:read and consents:read to receive later consent.withdrawn events and unsubscribe the matching contact_id in your external platform.

Leads

`GET /leads`

List leads (users captured via your storefront/forms) ordered by created_at desc. By default, leads that have converted into customers or are archived are excluded.

Query params:

  • limit (default 50, max 200)
  • cursor - opaque, from previous response meta.next_cursor
  • created_after, created_before - ISO 8601 timestamps
  • include_converted - set true to include leads with converted_at != null (former leads who became paying customers). Useful when you sync the full history into a CRM.
  • include_archived - set true to include contacts the creator soft-archived from their dashboard. Use this when syncing the full history to a CRM that needs to delete or unsubscribe the matching record on its side.
  • updated_since - ISO 8601 timestamp for incremental contact updates
  • marketing_consent - granted, withdrawn, or none

Each lead includes:

  • id, source - acquisition surface (e.g. storefront_login, blog_lead_capture, free_booking, free_signup)
  • created_at, converted_at (null when still a lead), archived_at (null when active)
  • contact_id - stable canonical id; user_id is nullable
  • user - legacy compatibility envelope. It is null for form-only contacts; otherwise it contains id, email, name, first_name, last_name, and phone.
  • marketing_consent - see Marketing consent object below.

`GET /leads/:id`

Retrieve a single lead, including converted_at, archived_at, and marketing_consent. Returns the row even after conversion or archive (so CRM dereferences keep working).

Customers

`GET /customers`

List users with at least one purchase from your store. Archived customers are excluded by default; pass ?include_archived=true to include them.

Each customer keeps its legacy User id and also includes stable contact_id, user_id, updated_at, and the same marketing_consent object as leads. Name fields may be null when not collected at checkout.

`GET /customers/:id`

Retrieve a single customer. Includes:

  • acquisition - source, acquired_at, converted_at if the customer started as a lead (null otherwise).
  • marketing_consent - see below.

Marketing consent object

Returned on every contact, lead, and customer payload. Always present (defaults to { status: "none", ...all-null } for contacts who never interacted with consent).

{
  "marketing_consent": {
    "status": "granted" | "withdrawn" | "none",
    "granted_at": "2026-06-02T10:00:00.000Z" | null,
    "withdrawn_at": "2026-06-15T14:30:00.000Z" | null,
    "source": "auth_modal_storefront" | "...",
    "policy_version": "2026-06-01" | null
  }
}

Status semantics:

  • granted - granted_at set, withdrawn_at null. Only contacts in this state can receive marketing emails.
  • withdrawn - withdrawn_at set. Do not send marketing communications, even if re-syncing later overwrites your CRM-side flag.
  • none - no consent record. Treat the same as withdrawn for marketing purposes.

Source values (which surface captured the latest action):

SourceWhere the consent was given
auth_modal_storefrontSign-in modal on the storefront landing page
auth_modal_blog_leadSign-in modal opened from a blog-post lead-capture button
auth_modal_lessonSign-in modal on a course lesson page
auth_modal_productSign-in modal on a product page
auth_modal_appSign-in modal on a course/blog app presentation page
auth_modal_checkoutSign-in modal on the checkout init page
blog_lead_sectionInline Subscribe widget rendered inside a blog post (no modal hop - signed-in user)
storefront_lead_sectionInline Subscribe widget rendered as a storefront LEAD_CAPTURE item (signed-in user)
free_signup_checkoutFree-product checkout form
paid_checkoutStripe-hosted paid checkout (recorded by the tenant webhook after checkout.session.completed)
account_settingsThe contact toggled the subscription themselves on /client/account
creator_initiatedThe creator (or team admin) unsubscribed the contact on their behalf - typically after an external request (GDPR Art. 21). Distinguishes proactive opt-outs from user-initiated ones for audit purposes.

`policy_version` - version label of the consent wording shown to the contact at the moment they granted consent. Bumped when the wording changes materially. Stored so you can prove which legal text the contact agreed to.

Products

`GET /products`

List your products with active pricing options.

`GET /products/:id`

Retrieve a single product.

Orders

`GET /orders`

List purchases (one-time + recurring) across all your products.

Query params include the standard pagination/date filters plus:

  • status - ACTIVE, CANCELLED, EXPIRED, REFUNDED, FREE_TRIAL, PENDING_CHECKOUT
  • product_id - filter to a specific product

`GET /orders/:id`

Retrieve a single order.

Subscriptions

`GET /subscriptions`

Like /orders but filtered to recurring purchases only. Same query params.

Analytics

`GET /analytics/summary`

Returns aggregated metrics for a period.

Query params:

  • period_start, period_end - ISO 8601 (default: last 30 days)

Response includes:

  • orders_count, paying_orders_count
  • leads_count
  • unique_customers
  • conversion_rate (unique customers / leads, or null)
  • revenue_by_currency - array with currency, gross_revenue_cents, aov_cents per currency

Events

`GET /events`

Unified feed of recent events across your store. Use this to drive webhook-style integrations via polling.

Use the cursor from the previous response and poll every 5–15 minutes. Do not poll more often than once every 5 minutes unless the creator's plan explicitly allows it.

Event types:

  • lead.created - data: lead_id, customer_id, source
  • lead.converted - emitted when a lead becomes a paying customer. occurred_at is the conversion timestamp. data: lead_id, customer_id, source
  • contact.created, contact.updated - canonical contact lifecycle without email or form answers in the event body
  • consent.granted, consent.withdrawn - included only when the key also has consents:read; carries contact_id, nullable user_id, source, and policy version
  • order.completed
  • subscription.cancelled
  • access.granted

Each event has id, type, occurred_at, and a data object specific to the type. Existing customer_id fields retain their User-id meaning; use the additive contact_id for canonical joins.