Email API

Send us your merchants and what they do. Emails & Flows takes care of campaigns, event-triggered flows, billing-based segments, and revenue attribution. Agent-readable version: /docs/emails-api.md

The one rule: every contact is a shop

A contact must carry both an email and a shopDomain— the shop's .myshopify.com domain. The shop domain is the join key that links the contact to Shopify Partner billing data (paying / on trial / plan / monthly spend), which powers billing segments and revenue attribution. Creating a contact without one is rejected with an explanatory 400.

Authentication

Two keys, created in AppStorePulse → Emails & Flows → Settings. The secret key (sk_live_…) lives on your backend — full ingestion, 600 requests/min. The publishable key (pk_live_…) is safe in the browser — write-only, locked to the origins you list on it, 60 requests/min per visitor.

Trust rule: contacts first createdwith the publishable key can't receive flow emails until confirmed server-side (a secret-key identify or CSV import) — the publishable key is public by design, so it can't be allowed to enroll strangers into your sending. Best practice: identify from your backend, use the browser SDK for events.

Authorization: Bearer sk_live_your_key_here

POST /api/e/identify — create or update a contact

Upserts a contact keyed on email. Safe to call repeatedly: re-identifying refreshes name / shopDomain and merges new attribute keys — and it never re-subscribes someone who unsubscribed, bounced, or complained (opt-outs are sticky, tombstoned across deletion).

curl -X POST https://www.appstorepulse.com/api/e/identify \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "owner@shop.com",
    "shopDomain": "their-shop.myshopify.com",
    "name": "Jane Merchant",
    "attributes": { "plan": "growth", "country": "US" },
    "lists": ["Onboarding"]
  }'
  • email (required).
  • shopDomain (required on create): the .myshopify.com domain. Pasted URLs and mixed case are normalized; anything else is rejected with the expected format. externalId is a legacy alias. Optional when the contact already has one.
  • name: used by merge variables like {{name | there}}.
  • attributes: any key/values — become segment filters and merge variables.
  • lists: list names to add the contact to (created on demand).
  • alreadyInstalled: the contact had already installed your app before you sent them to us (backfill). Flows on app_installed skip these contacts, so a long-time merchant who reinstalls never gets your welcome email.

POST /api/e/track — record an event

Events trigger flows and feed reports. Identify the contact first.

curl -X POST https://www.appstorepulse.com/api/e/track \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "owner@shop.com",
    "name": "app_installed",
    "properties": { "source": "oauth_callback" },
    "idempotencyKey": "install-their-shop.myshopify.com"
  }'

idempotencyKeymakes retries safe: the same key is recorded once, so a network retry can't fire a flow twice. Use one stable key per real-world action (e.g. a database row id) — never a random value per request. Event names starting with $ or __ are reserved.

Lists: POST /api/e/lists/add and /api/e/lists/remove with { "email", "list" }. Adding can trigger list-based flows, once per contact per flow. Removing needs the secret key.

Which events to send

Recommended conventions:

  • app_installed — from your install/OAuth callback (below). Reserved name: contacts marked alreadyInstalled are skipped for flows on this event.
  • app_uninstalled — from your app/uninstalled Shopify webhook.
  • subscription_started / plan_changed / subscription_cancelled — from your app_subscriptions/update Shopify webhook.
  • Your own product events — feature_activated, usage_milestone, anything meaningful in your app.

The install moment

Shopify's Partner events identify shops by domain only — they never include an email. Close the gap in your OAuth/install callback, where the Admin API gives you the shop's email, and your welcome flow fires within a minute of install:

// In your install/OAuth callback, after fetching the shop:
await fetch("https://www.appstorepulse.com/api/e/identify", {
  method: "POST",
  headers: { "Authorization": "Bearer sk_live_xxx", "Content-Type": "application/json" },
  body: JSON.stringify({
    email: shop.email,
    name: shop.shop_owner,
    shopDomain: shop.myshopify_domain,
  }),
});
await fetch("https://www.appstorepulse.com/api/e/track", {
  method: "POST",
  headers: { "Authorization": "Bearer sk_live_xxx", "Content-Type": "application/json" },
  body: JSON.stringify({
    email: shop.email,
    name: "app_installed",
    idempotencyKey: `install-${shop.myshopify_domain}`,
  }),
});

Browser SDK

<script src="https://www.appstorepulse.com/sdk/e.js" data-key="pk_live_xxx"></script>
<script>
  asp.identify({ email: "owner@shop.com", shopDomain: shop, name: "Jane" });
  asp.track("feature_activated", { feature: "bundles" });
  asp.addToList("Beta testers");
</script>

Contact management

Read, opt out, and delete contacts — e.g. to honor a merchant's data-deletion request. Secret key only.

# One contact / paginated list
GET /api/e/subscribers?email=owner@shop.com
GET /api/e/subscribers?limit=100&offset=0&status=subscribed

# Opt out (sticky, like the unsubscribe link)
POST /api/e/unsubscribe          { "email": "owner@shop.com" }

# Hard delete (contact + history; opted-out addresses leave a tombstone)
DELETE /api/e/subscribers        { "email": "owner@shop.com" }

Backfill & errors

Seed existing merchants by looping your database through identify with the secret key (600/min ≈ 36k contacts/hour) with alreadyInstalled: true, or CSV import in the dashboard — columns email + shop_domain(required), name, status (subscribed /unsubscribed, so opt-outs from your previous tool carry over); other columns become attributes. Imports never fire flows themselves. Errors are JSON { "error": "<reason>" }with proper status codes — 400 validation (the message states exactly what's missing or malformed), 401 unknown key, 403 origin not allowed, 413 body over 32 KB, 429 rate limited with Retry-After.

Building with an AI agent? Point it at https://www.appstorepulse.com/docs/emails-api.md — the same reference as plain markdown. Looking for the read-only data API (rankings, reviews, competitors)? That's the REST API.