Shopify does not send emails via its own API — pair the GraphQL `customers` query and `customers/create` webhook with an external ESP (SendGrid, Resend) to automate customer emails. Query customer segments using tags and order history, manage opt-in consent with `customerEmailMarketingConsentUpdate`, then trigger personalized sends via your ESP. GraphQL budget on Standard plans: 1,000 points, 50 pts/sec restore.
API Quick Reference
OAuth 2.0 / Access Token
1,000-point bucket, 50 pts/sec restore
JSON
Available
Understanding Shopify Customer Emails via the API
Shopify's Admin API gives you full access to customer data — email addresses, tags, order history, and marketing consent status — but it does not provide an endpoint to send emails directly. Shopify Email (the native feature) is limited to 10,000 free emails/month and offers no programmable API access. For custom automated email flows, you must export customer data from Shopify and deliver emails through an external Email Service Provider (ESP).
The automation flow uses Shopify webhooks (`customers/create`, `orders/create`) to detect trigger events in real time, then queries the GraphQL `customers` object to fetch full customer data and context (recent orders, tags, lifetime value). This data is sent to your ESP (SendGrid, Resend, Mailchimp, etc.) to trigger a pre-built email template.
For GDPR compliance and customer trust, check `emailMarketingConsent` before adding customers to marketing lists — only customers with `subscribed` consent status should receive marketing emails. Transactional emails (order confirmations, shipping notifications) do not require marketing consent. See https://shopify.dev/docs/api/admin-graphql/2026-04/objects/Customer for the full customer object reference.
https://{shop}.myshopify.com/admin/api/2026-04/graphql.jsonSetting Up Shopify API Authentication for Customer Emails
This automation needs read access to customer data and the ability to update email marketing consent. Create a custom app via the Shopify Dev Dashboard with `read_customers` and `write_customers` scopes. You will also need an API key from your ESP (SendGrid, Resend, etc.) stored separately in environment variables.
- 1Go to dev.shopify.com and log in with your Partner account.
- 2Click 'Create app', choose 'Custom app' for single-store automation.
- 3Under 'Admin API access scopes', enable: read_customers, write_customers, read_orders.
- 4Click 'Save', then 'Install app' and copy the generated access token.
- 5Store Shopify token in environment variable: export SHOPIFY_ACCESS_TOKEN='shpat_...'
- 6Also store your ESP API key: export SENDGRID_API_KEY='SG...'
- 7Register webhooks for customers/create and orders/create via the `webhookSubscriptionCreate` GraphQL mutation.
1import os2import requests34SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN']5ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']67GRAPHQL_URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'8HEADERS = {9 'X-Shopify-Access-Token': ACCESS_TOKEN,10 'Content-Type': 'application/json',11}1213# Test: fetch first 3 customers14query = '{ customers(first: 3) { edges { node { id email displayName emailMarketingConsent { marketingState } } } } }'15resp = requests.post(GRAPHQL_URL, json={'query': query}, headers=HEADERS)16print(resp.json())Security notes
- •Store all API keys in environment variables — both Shopify token and ESP key.
- •Validate webhook HMAC using base64(HMAC-SHA256(rawBody, appSecret)) before processing any webhook payload.
- •Never send marketing emails to customers with emailMarketingConsent.marketingState other than 'SUBSCRIBED'.
- •Customer email addresses are PII — do not log them in plain text in production logs.
- •Use HTTPS endpoints for all webhooks.
- •Rate-limit your ESP calls to avoid triggering spam filters from burst sends.
Key endpoints
/admin/api/2026-04/graphql.jsonGraphQL query for `customers` — fetch customers with their email, tags, marketing consent, and order history for segmentation.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | optional | Shopify search query for filtering (e.g., 'tag:vip', 'email_marketing_consent_state:subscribed', 'orders_count:>5') |
first | number | required | Number of customers per page (max 250) |
Request
1{"query": "{ customers(first: 50, query: \"tag:vip AND email_marketing_consent_state:subscribed\") { edges { node { id email displayName tags numberOfOrders totalSpentV2 { amount currencyCode } emailMarketingConsent { marketingState } } } pageInfo { hasNextPage endCursor } } }"}Response
1{"data": {"customers": {"edges": [{"node": {"id": "gid://shopify/Customer/11223344", "email": "jane@example.com", "displayName": "Jane Smith", "tags": ["vip"], "numberOfOrders": 12, "totalSpentV2": {"amount": "458.75", "currencyCode": "USD"}, "emailMarketingConsent": {"marketingState": "SUBSCRIBED"}}}], "pageInfo": {"hasNextPage": false}}}}/admin/api/2026-04/graphql.jsonGraphQL mutation `customerEmailMarketingConsentUpdate` — updates a customer's email marketing consent status. Required before adding to marketing lists.
| Parameter | Type | Required | Description |
|---|---|---|---|
customerId | string | required | Customer global ID |
marketingState | string | required | SUBSCRIBED, UNSUBSCRIBED, PENDING, INVALID, NOT_SUBSCRIBED |
Request
1{"query": "mutation customerEmailMarketingConsentUpdate($input: CustomerEmailMarketingConsentUpdateInput!) { customerEmailMarketingConsentUpdate(input: $input) { customer { id emailMarketingConsent { marketingState } } userErrors { field message } } }", "variables": {"input": {"customerId": "gid://shopify/Customer/11223344", "emailMarketingConsent": {"marketingState": "SUBSCRIBED", "marketingOptInLevel": "SINGLE_OPT_IN", "consentUpdatedAt": "2026-04-15T10:00:00Z"}}}}Response
1{"data": {"customerEmailMarketingConsentUpdate": {"customer": {"id": "gid://shopify/Customer/11223344", "emailMarketingConsent": {"marketingState": "SUBSCRIBED"}}, "userErrors": []}}}/admin/api/2026-04/graphql.jsonGraphQL mutation `webhookSubscriptionCreate` — registers webhooks for customers/create and orders/create events.
| Parameter | Type | Required | Description |
|---|---|---|---|
topic | string | required | CUSTOMERS_CREATE or ORDERS_CREATE |
Request
1{"query": "mutation { webhookSubscriptionCreate(topic: CUSTOMERS_CREATE, webhookSubscription: {callbackUrl: \"https://yourapp.com/webhooks/customers-create\", format: JSON}) { userErrors { field message } webhookSubscription { id } } }"}Response
1{"data": {"webhookSubscriptionCreate": {"userErrors": [], "webhookSubscription": {"id": "gid://shopify/WebhookSubscription/445566"}}}}Step-by-step automation
Register Webhooks for Customer and Order Events
Why: Webhooks give you real-time triggers for the most valuable email moments: new customer signup and post-purchase.
Register `CUSTOMERS_CREATE` for welcome email sequences and `ORDERS_CREATE` for post-purchase follow-ups. Your endpoint must respond within 5 seconds — queue email processing asynchronously.
1# Register customers/create webhook2curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \3 -H 'X-Shopify-Access-Token: shpat_your_token' \4 -H 'Content-Type: application/json' \5 -d '{"query": "mutation { webhookSubscriptionCreate(topic: CUSTOMERS_CREATE, webhookSubscription: {callbackUrl: \\"https://yourapp.com/webhooks/customers-create\\", format: JSON}) { userErrors { field message } webhookSubscription { id } } }"}'Pro tip: For the orders/create webhook, filter for first-time buyers in your handler (`order.customer.orders_count === 1`) to differentiate between first-purchase and repeat-purchase email sequences.
Expected result: Two webhook subscriptions created — one for new customer signups, one for new orders.
Fetch Customer Data and Check Marketing Consent
Why: You need full customer context (order history, tags, LTV) to personalize the email, and must verify marketing consent before sending marketing messages.
When a webhook fires with a customer ID, query the GraphQL `customer` object to enrich the data. Check `emailMarketingConsent.marketingState` — only send marketing emails to customers with state `SUBSCRIBED`. Transactional emails (welcome, order confirmation) can go to all customers regardless of marketing consent.
1curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \2 -H 'X-Shopify-Access-Token: shpat_your_token' \3 -H 'Content-Type: application/json' \4 -d '{"query": "{ customer(id: \\"gid://shopify/Customer/11223344\\") { id email displayName firstName tags numberOfOrders totalSpentV2 { amount currencyCode } emailMarketingConsent { marketingState } orders(first: 1, sortKey: CREATED_AT, reverse: true) { edges { node { id name totalPriceV2 { amount } } } } } }"}'Pro tip: Cache customer data for 30 minutes if you're sending multi-step sequences — fetching the full customer object on each step is wasteful.
Expected result: Full customer object with marketing consent status. Use `isSubscribed` flag to gate marketing email delivery.
Send Personalized Email via External ESP
Why: Shopify has no email sending API — you must route through SendGrid, Resend, Mailchimp, or similar to actually deliver the email.
Use the customer data from Step 2 to build a personalized email payload and send via your ESP's API. The example uses SendGrid's `POST /v3/mail/send`. For Resend users, the endpoint is `POST https://api.resend.com/emails`. Use dynamic template IDs and pass Shopify customer data as template variables.
1# SendGrid dynamic template email2curl -X POST 'https://api.sendgrid.com/v3/mail/send' \3 -H 'Authorization: Bearer $SENDGRID_API_KEY' \4 -H 'Content-Type: application/json' \5 -d '{6 "from": {"email": "hello@yourstore.com", "name": "Your Store"},7 "personalizations": [{8 "to": [{"email": "jane@example.com", "name": "Jane"}],9 "dynamic_template_data": {10 "first_name": "Jane",11 "orders_count": 12,12 "total_spent": "458.75"13 }14 }],15 "template_id": "d-your-sendgrid-template-id"16 }'Pro tip: For Resend (modern alternative to SendGrid), use `POST https://api.resend.com/emails` with `{ from, to, subject, react }` — it's simpler and cheaper at low volumes (free tier: 3,000 emails/month).
Expected result: SendGrid returns HTTP 202 Accepted when the email is queued for delivery. The customer receives the personalized welcome email within seconds.
Segment and Batch Win-Back Campaigns
Why: Beyond real-time triggers, you can query Shopify for churned customers to send targeted re-engagement campaigns on a scheduled basis.
Use the `customers` GraphQL query with filter parameters to find customers who haven't ordered in 90+ days but were previously active. Paginate through results and batch-send via your ESP. Check `emailMarketingConsent.marketingState === 'SUBSCRIBED'` before including in any marketing campaign.
1# Query customers with marketing consent who haven't ordered in 90 days2curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \3 -H 'X-Shopify-Access-Token: shpat_your_token' \4 -H 'Content-Type: application/json' \5 -d '{"query": "{ customers(first: 100, query: \\"email_marketing_consent_state:subscribed AND last_order_date:<2026-01-07\\") { edges { node { id email firstName numberOfOrders } } pageInfo { hasNextPage endCursor } } }"}'Pro tip: Rate-limit your ESP sends when batching large segments — SendGrid has a 600 emails/minute limit on free plans. Use a delay of 100ms between sends to stay within limits.
Expected result: Paginated list of subscribers who haven't purchased in 90+ days, ready for win-back campaign sends.
Complete working code
This script handles the full welcome email flow: receives the `customers/create` webhook, validates the HMAC, fetches enriched customer data, checks marketing consent, and sends a personalized welcome email via SendGrid.
1import os, hmac, hashlib, base64, logging, requests2from flask import Flask, request, abort34logging.basicConfig(level=logging.INFO)5app = Flask(__name__)67SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN']8ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']9APP_SECRET = os.environ['SHOPIFY_APP_SECRET']10SENDGRID_KEY = os.environ['SENDGRID_API_KEY']11TEMPLATE_ID = os.environ['SENDGRID_WELCOME_TEMPLATE_ID']1213SHOPIFY_URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'14SHOPIFY_HEADERS = {'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json'}1516def gql(query):17 resp = requests.post(SHOPIFY_URL, json={'query': query}, headers=SHOPIFY_HEADERS)18 resp.raise_for_status()19 return resp.json()2021def get_customer(customer_id):22 q = f'{{ customer(id: "gid://shopify/Customer/{customer_id}") {{ id email firstName numberOfOrders totalSpentV2 {{ amount currencyCode }} emailMarketingConsent {{ marketingState }} }} }}'23 return gql(q)['data']['customer']2425def send_email(customer):26 payload = {27 'from': {'email': 'hello@yourstore.com'},28 'personalizations': [{29 'to': [{'email': customer['email'], 'name': customer.get('firstName', '')}],30 'dynamic_template_data': {31 'first_name': customer.get('firstName', 'there'),32 'total_spent': customer.get('totalSpentV2', {}).get('amount', '0')33 }34 }],35 'template_id': TEMPLATE_ID36 }37 resp = requests.post('https://api.sendgrid.com/v3/mail/send', json=payload, headers={'Authorization': f'Bearer {SENDGRID_KEY}'})38 return resp.status_code == 2023940@app.route('/webhooks/customers-create', methods=['POST'])41def customer_created():42 hmac_header = request.headers.get('X-Shopify-Hmac-Sha256', '')43 computed = base64.b64encode(hmac.new(APP_SECRET.encode(), request.get_data(), hashlib.sha256).digest()).decode()44 if not hmac.compare_digest(computed, hmac_header):45 abort(401)46 47 data = request.get_json()48 customer_id = data['id']49 customer = get_customer(customer_id)50 51 if not customer or not customer.get('email'):52 return '', 20053 54 # Always send transactional welcome (no consent check needed)55 success = send_email(customer)56 logging.info(f'Welcome email for {customer_id}: {"sent" if success else "failed"}')57 return '', 2005859if __name__ == '__main__':60 app.run(port=3000)Error handling
SendGrid: emails bounced (hard bounce logged in Activity)Email address is invalid, domain doesn't exist, or the recipient server permanently rejected the message.
Subscribe to SendGrid's Event Webhook to receive bounce notifications. On hard bounce, update the customer's `emailMarketingConsent` to UNSUBSCRIBED via `customerEmailMarketingConsentUpdate` to comply with email best practices.
Do not retry hard bounces — the address is invalid. Log and remove from future sends.
ThrottledGraphQL point bucket exhausted when batch-querying large customer segments.
Add a sleep between page fetches during batch operations. Monitor `extensions.cost.throttleStatus.currentlyAvailable` and pause when below 100 points.
Wait for bucket to restore: `sleep((requestedCost - currentlyAvailable) / 50)` on Standard plans.
SendGrid: Forbidden — sender identity not verifiedThe From email address or domain has not been verified in your SendGrid account.
Go to SendGrid Settings > Sender Authentication and verify your sending domain or single sender email address. Domain authentication (SPF/DKIM) is required for production sends.
No retry until domain is verified.
customerId is invalidThe customer ID from the webhook payload was passed without the `gid://shopify/Customer/` prefix.
Always wrap numeric IDs in the global ID format: `gid://shopify/Customer/{numeric_id}`. The webhook payload contains numeric IDs; GraphQL queries require global IDs.
Fix the ID formatting — no retry needed.
Webhook: HMAC validation failedUsing hex digest instead of base64, or computing the digest against the parsed JSON body instead of the raw bytes.
Use `hmac.new(secret.encode(), raw_body_bytes, hashlib.sha256).digest()` and then `base64.b64encode(...)`. Never decode the JSON before computing the HMAC.
Fix the HMAC computation logic — all future webhooks will fail until corrected.
Rate Limits for Shopify Customer API
| Scope | Limit | Window |
|---|---|---|
| GraphQL Admin API — Standard plan | 1,000 points | Restores at 50 points/second |
| Customer query cost | 5-20 points per query | Varies based on fields and connections requested |
| SendGrid free tier | 100 emails/day | Resets daily; 600 emails/minute rate limit |
1import time23def gql_paginate_customers(url, headers, filter_query, max_retries=3):4 cursor, customers = None, []5 while True:6 after = f', after: "{cursor}"' if cursor else ''7 q = f'{{ customers(first: 100{after}, query: "{filter_query}") {{ edges {{ node {{ id email firstName }} }} pageInfo {{ hasNextPage endCursor }} }} }}'8 for attempt in range(max_retries):9 resp = requests.post(url, json={'query': q}, headers=headers)10 if resp.status_code == 429:11 time.sleep(2 ** attempt)12 continue13 break14 data = resp.json()['data']['customers']15 customers.extend(e['node'] for e in data['edges'])16 available = resp.json().get('extensions', {}).get('cost', {}).get('throttleStatus', {}).get('currentlyAvailable', 1000)17 if available < 100:18 time.sleep(2)19 if not data['pageInfo']['hasNextPage']:20 break21 cursor = data['pageInfo']['endCursor']22 return customers- Request only the customer fields you need — each additional connection (orders, addresses) adds query cost.
- Cache customer segments for scheduled campaigns — don't re-query Shopify for each individual send.
- Use the `query` parameter to filter customers server-side rather than fetching all and filtering client-side.
- Add 100ms delays between ESP sends when batch-processing large segments to avoid triggering spam filters.
- Monitor both Shopify GraphQL quota and ESP daily send limits — either can bottleneck large campaigns.
Security checklist
- Validate every webhook HMAC using base64(HMAC-SHA256(rawBody)) — never use hex digest.
- Store both SHOPIFY_ACCESS_TOKEN and SENDGRID_API_KEY in environment variables or a secrets manager.
- Check emailMarketingConsent.marketingState before adding customers to any marketing list.
- Never log customer email addresses in plain text production logs.
- Respect unsubscribe events from your ESP — update customerEmailMarketingConsentUpdate to UNSUBSCRIBED when a customer opts out.
- Use HTTPS for all webhook callback endpoints.
- Implement rate limiting on your webhook endpoint to prevent abuse from replay attacks.
- Honor GDPR/CCPA data deletion requests by responding to Shopify's customers/redact compliance webhook.
Automation use cases
Welcome Email Series
beginnerSend a 3-part welcome sequence to new customers: immediate welcome, product highlights on day 2, and a discount on day 7.
Post-Purchase Follow-Up
beginner7 days after an order ships, send a review request email with the order details linked from Shopify data.
Win-Back Campaign
intermediateWeekly cron queries customers who haven't ordered in 90 days and sends a targeted re-engagement offer.
VIP Tier Upgrade Notifications
intermediateWhen a customer's total_spent crosses a threshold, add a tag in Shopify and trigger a VIP welcome email.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier available; Starter $19.99/monthZapier connects Shopify's new customer event directly to Mailchimp, SendGrid, or ActiveCampaign to send welcome emails without any code.
- + Zero code setup
- + Native Shopify + Mailchimp integration
- + Easy multi-step flows
- - Limited customer data access vs GraphQL
- - Per-task pricing expensive at scale
- - No complex segmentation logic
Make (formerly Integromat)
Free tier (1,000 ops/month); Core $9/monthMake supports Shopify webhooks and can send personalized emails via SendGrid or Mailchimp with customer data transformations.
- + Affordable for moderate volumes
- + Visual workflow builder
- + Better data transformation than Zapier
- - Less beginner-friendly
- - No GraphQL support natively
- - Limited debugging tools
n8n
Self-hosted free; Cloud Starter €20/monthn8n with the Shopify node and SendGrid/Resend node can handle the full welcome email automation self-hosted.
- + Self-hosted for free
- + Good email node support
- + Flexible scheduling
- - Requires hosting setup
- - No native GraphQL node
- - Less documentation
Best practices
- Shopify does not send emails via API — always use an external ESP (SendGrid, Resend, Mailchimp) for delivery.
- Distinguish transactional emails (welcome, order confirmation) from marketing emails — only marketing emails require explicit opt-in consent.
- Check emailMarketingConsent.marketingState === 'SUBSCRIBED' before sending any promotional email.
- Use the `query` filter parameter when fetching customers for campaigns — avoid pulling all customers and filtering client-side.
- Build an unsubscribe handler that calls `customerEmailMarketingConsentUpdate` to sync opt-outs back to Shopify.
- For Shopify Email's built-in 10,000 free monthly emails, use it for simple transactional sends; switch to an ESP when you need custom logic.
- Test email templates with Shopify's native preview feature before automating sends at scale.
- Always include an unsubscribe link in marketing emails — CAN-SPAM and GDPR compliance requirement.
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Shopify customer email automation using the GraphQL Admin API (2026-04). I'm subscribing to the customers/create webhook and sending welcome emails via SendGrid. My problem is: [describe issue]. Here's my webhook handler: [paste code]. The Shopify response I'm seeing is: [paste]. Help me fix this and ensure I'm checking marketing consent correctly before sending marketing vs transactional emails.
Build a Shopify customer email automation dashboard with Supabase as the backend. It should show recent webhook events (customer created, order created), allow configuring email templates and trigger rules, and display sent email history with open/click rates from SendGrid. Use shadcn/ui with Tailwind CSS. Include a segment preview that calls the Shopify API to show how many customers match each filter.
Frequently asked questions
Can I send emails directly through the Shopify API?
No. Shopify's API does not have an email sending endpoint. Shopify Email (the native feature) is only accessible via the Shopify admin UI, not the API. To automate email sending, you must connect Shopify to an external ESP like SendGrid, Resend, Mailchimp, or Klaviyo. Use Shopify webhooks and the customers GraphQL query to get data, then trigger sends via your ESP's API.
What is the difference between transactional and marketing emails in Shopify?
Transactional emails (order confirmation, shipping notification, password reset) are service communications — they can be sent to all customers regardless of marketing consent. Marketing emails (promotions, newsletters, win-back campaigns) require explicit opt-in. Check `emailMarketingConsent.marketingState === 'SUBSCRIBED'` before sending marketing emails. Sending marketing emails to unsubscribed customers violates CAN-SPAM, GDPR, and CASL.
What happens when I hit the Shopify GraphQL rate limit while querying customers?
You receive HTTP 429 or a GraphQL response with throttling data in `extensions.cost.throttleStatus`. On Standard plans, the 1,000-point bucket restores at 50 pts/sec. For large customer queries with order history included, cost can be 10-20 points per page. Monitor `currentlyAvailable` and sleep when it drops below 100 points. Alternatively, use `bulkOperationRunQuery` for large exports — it has no rate-limit cost.
How do I sync Shopify unsubscribes back from Mailchimp/Klaviyo?
Subscribe to your ESP's unsubscribe webhook. When a customer unsubscribes in your ESP, call Shopify's `customerEmailMarketingConsentUpdate` mutation with `marketingState: UNSUBSCRIBED`. This keeps Shopify's consent record in sync. Example: Klaviyo sends a webhook on profile unsubscribe → your handler calls the Shopify mutation.
Can I use Shopify's built-in flow automation instead?
Shopify Flow (available on Shopify and Shopify Plus plans) handles basic email triggers without code, but uses Shopify Email which is limited to 10,000 free emails/month and lacks advanced personalization. For more than 10,000 emails/month, custom ESP integration, or complex multi-step sequences, the API approach gives you full control.
Is the Shopify API free?
Yes — the Admin API is free for apps installed on Shopify stores. Email delivery costs depend on your ESP: SendGrid is free for 100 emails/day, Resend free for 3,000/month, Mailchimp free for 500 contacts. At scale, ESP costs vary from $15 to $300+/month depending on volume.
Can RapidDev help build a custom Shopify email automation?
Yes. RapidDev has built 600+ apps including complete Shopify customer lifecycle email systems integrated with SendGrid, Klaviyo, and Resend. We handle webhook setup, customer segmentation, ESP integration, and compliance (GDPR opt-in management). Book a free consultation at rapidevelopers.com.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation