Summary and recommendation
Shopify's Admin API splits user management across two distinct surfaces that must not be conflated. The REST Customer API (base URL: https://{store}.myshopify.com/admin/api/2024-01) manages storefront buyers - not staff or admin users.
Staff management requires either the GraphQL StaffMember type (read_staff_members / write_staff_members scopes, Plus org-level only) or the SCIM 2.0 endpoint (https://{organization}.myshopify.com/scim/v2, Plus + SAML prerequisite). Auth is OAuth 2.0 or a static Admin API access token passed as X-Shopify-Access-Token - Bearer token format is not accepted.
Building an identity graph across Shopify requires stitching together three separate identity surfaces: storefront customers (REST), staff members (GraphQL), and org-level provisioned users (SCIM), each with different scopes, endpoints, and plan requirements.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (for public/custom apps) or Admin API access token (for private apps/custom apps via Partner Dashboard) |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Shopify Plus (with SAML SSO configured) |
Authentication
Auth method: OAuth 2.0 (for public/custom apps) or Admin API access token (for private apps/custom apps via Partner Dashboard)
Setup steps
- Create an app in the Shopify Partner Dashboard or directly in the store's Admin under 'Apps and sales channels > Develop apps'.
- For OAuth 2.0: Register your redirect URI, obtain client_id and client_secret, redirect merchant to https://{store}.myshopify.com/admin/oauth/authorize with required scopes.
- Exchange the authorization code for a permanent access token via POST to https://{store}.myshopify.com/admin/oauth/access_token.
- Include the token in all requests as the X-Shopify-Access-Token header.
- For private/custom apps: generate an Admin API access token directly in the store Admin and use it as X-Shopify-Access-Token.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| read_customers | Read customer records (storefront-facing users). | GET /customers, GET /customers/{id} |
| write_customers | Create, update, and delete customer records. | POST /customers, PUT /customers/{id}, DELETE /customers/{id} |
| read_orders | Read order data associated with customers. | GET /customers/{id}/orders |
| read_staff_members | Read staff/organization user records (GraphQL only; Shopify Plus org-level). | GraphQL query: staffMembers |
| write_staff_members | Create and update staff/organization users (GraphQL only; Shopify Plus org-level). | GraphQL mutation: staffMemberCreate, staffMemberUpdate |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer (REST) / gid string (GraphQL) | Unique identifier for the customer. | auto-generated | immutable | GraphQL uses global ID format: gid://shopify/Customer/{id} |
| string | Customer's email address. | required (if phone not provided) | optional | Must be unique per store. | |
| first_name | string | Customer's first name. | optional | optional | |
| last_name | string | Customer's last name. | optional | optional | |
| phone | string | Customer's phone number in E.164 format. | optional (required if email not provided) | optional | Must be unique per store. |
| verified_email | boolean | Whether the customer's email has been verified. | auto-set | read-only | |
| state | string (enum) | Account state: disabled, invited, enabled, declined. | auto-set | read-only | Controlled by invitation/account enable flows, not directly settable. |
| tags | string (comma-separated) | Tags attached to the customer. | optional | optional | Max 250 tags; each tag max 255 characters. |
| note | string | Internal note about the customer. | optional | optional | |
| accepts_marketing | boolean | Whether the customer has opted into marketing emails. | optional | optional | Deprecated in favor of email_marketing_consent. |
| email_marketing_consent | object | Marketing consent state, opt-in level, and consent updated timestamp. | optional | optional | Preferred over accepts_marketing. |
| sms_marketing_consent | object | SMS marketing consent state and opt-in level. | optional | optional | |
| addresses | array of address objects | List of addresses associated with the customer. | optional | optional | Max 3 addresses per customer via REST. |
| default_address | object | The customer's default address. | auto-set from addresses | settable via /customers/{id}/addresses/{address_id}/default | |
| currency | string (ISO 4217) | Currency the customer used on their last order. | auto-set | read-only | |
| orders_count | integer | Number of orders the customer has placed. | auto-set | read-only | |
| total_spent | string (decimal) | Total amount spent by the customer across all orders. | auto-set | read-only | |
| tax_exempt | boolean | Whether the customer is exempt from taxes. | optional | optional | |
| metafields | array of metafield objects | Custom metadata attached to the customer. | optional | optional | Requires separate metafield endpoints or GraphQL. |
| created_at | datetime (ISO 8601) | Timestamp when the customer was created. | auto-set | read-only |
Core endpoints
List customers
- Method: GET
- URL:
https://{store}.myshopify.com/admin/api/2024-01/customers.json - Watch out for: Cursor-based pagination only; do not use page param. Use Link header rel='next' page_info value.
Request example
GET /admin/api/2024-01/customers.json?limit=50
X-Shopify-Access-Token: {token}
Response example
{
"customers": [
{"id": 207119551, "email": "bob@example.com",
"first_name": "Bob", "last_name": "Doe",
"state": "enabled"}
]
}
Get single customer
- Method: GET
- URL:
https://{store}.myshopify.com/admin/api/2024-01/customers/{customer_id}.json - Watch out for: Returns 404 if customer does not exist in the store; IDs are store-scoped.
Request example
GET /admin/api/2024-01/customers/207119551.json
X-Shopify-Access-Token: {token}
Response example
{
"customer": {
"id": 207119551,
"email": "bob@example.com",
"first_name": "Bob",
"state": "enabled"
}
}
Create customer
- Method: POST
- URL:
https://{store}.myshopify.com/admin/api/2024-01/customers.json - Watch out for: email or phone is required. Duplicate email returns 422. New customers default to state=disabled until they set a password.
Request example
POST /admin/api/2024-01/customers.json
Content-Type: application/json
{
"customer": {
"first_name": "Alice",
"email": "alice@example.com",
"verified_email": true
}
}
Response example
{
"customer": {
"id": 1073339461,
"email": "alice@example.com",
"first_name": "Alice",
"state": "disabled"
}
}
Update customer
- Method: PUT
- URL:
https://{store}.myshopify.com/admin/api/2024-01/customers/{customer_id}.json - Watch out for: id must be included in the request body. Partial updates are supported; omitted fields are unchanged.
Request example
PUT /admin/api/2024-01/customers/207119551.json
Content-Type: application/json
{
"customer": {
"id": 207119551,
"note": "VIP customer"
}
}
Response example
{
"customer": {
"id": 207119551,
"note": "VIP customer",
"updated_at": "2024-01-15T10:00:00-05:00"
}
}
Delete customer
- Method: DELETE
- URL:
https://{store}.myshopify.com/admin/api/2024-01/customers/{customer_id}.json - Watch out for: Customers with existing orders cannot be deleted; returns 422. Consider anonymizing instead via GraphQL customerDeletePayload.
Request example
DELETE /admin/api/2024-01/customers/207119551.json
X-Shopify-Access-Token: {token}
Response example
{}
Search customers
- Method: GET
- URL:
https://{store}.myshopify.com/admin/api/2024-01/customers/search.json - Watch out for: Search uses Shopify's query syntax (field:value). Not all fields are searchable. Does not support cursor pagination; limited to 250 results.
Request example
GET /admin/api/2024-01/customers/search.json?query=email:alice@example.com&limit=10
X-Shopify-Access-Token: {token}
Response example
{
"customers": [
{"id": 1073339461, "email": "alice@example.com",
"first_name": "Alice"}
]
}
Send account invite
- Method: POST
- URL:
https://{store}.myshopify.com/admin/api/2024-01/customers/{customer_id}/send_invite.json - Watch out for: Sends a storefront account activation email. Does not apply to staff/admin users. Customer state changes to 'invited'.
Request example
POST /admin/api/2024-01/customers/207119551/send_invite.json
Content-Type: application/json
{"customer_invite": {}}
Response example
{
"customer_invite": {
"to": "bob@example.com",
"from": "store@example.com",
"subject": "Welcome to Example Store"
}
}
List staff members (GraphQL)
- Method: POST
- URL:
https://{store}.myshopify.com/admin/api/2024-01/graphql.json - Watch out for: staffMembers query is only available to the store owner token or apps with read_staff_members scope. Organization-level staff management requires Shopify Plus and the Organization Admin API.
Request example
POST /admin/api/2024-01/graphql.json
{
"query": "{ staffMembers(first: 10) { edges { node { id name email active } } } }"
}
Response example
{
"data": {
"staffMembers": {
"edges": [
{"node": {"id": "gid://shopify/StaffMember/1",
"name": "Jane Admin", "email": "jane@store.com",
"active": true}}
]
}
}
}
Rate limits, pagination, and events
- Rate limits: Shopify uses a leaky-bucket algorithm for REST and a calculated-cost system for GraphQL. REST allows 2 requests/second with a bucket of 40 requests. GraphQL uses a point-cost model with 1,000 points/second restore rate and a 50,000-point bucket by default. Shopify Plus stores receive higher limits.
- Rate-limit headers: Yes
- Retry-After header: Yes
- Rate-limit notes: REST headers: X-Shopify-Shop-Api-Call-Limit (e.g., 32/40). GraphQL headers: X-GraphQL-Cost-Include-Fields, throttleStatus in extensions. Retry-After header returned on 429 responses.
- Pagination method: cursor
- Default page size: 50
- Max page size: 250
- Pagination pointer: page_info (cursor-based via Link header); limit param controls page size
| Plan | Limit | Concurrent |
|---|---|---|
| Basic / Shopify / Advanced | REST: 2 req/s (bucket 40); GraphQL: 1,000 points/s restore, 50,000-point bucket | 0 |
| Shopify Plus | REST: 4 req/s (bucket 80); GraphQL: 2,000 points/s restore, 100,000-point bucket | 0 |
- Webhooks available: Yes
- Webhook notes: Shopify supports webhooks for customer lifecycle events. Webhooks can be registered via the Admin API or the Partner Dashboard. Payloads are sent as HTTP POST to your endpoint.
- Alternative event strategy: Shopify also supports GraphQL webhook subscriptions (webhookSubscriptionCreate mutation) and EventBridge/Pub-Sub delivery for higher reliability.
- Webhook events: customers/create, customers/update, customers/delete, customers/enable, customers/disable, customer_groups/create, customer_groups/update, customer_groups/delete
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Shopify Plus (with SAML SSO configured)
Endpoint: https://{organization}.myshopify.com/scim/v2
Supported operations: GET /Users (list users), GET /Users/{id} (get user), POST /Users (provision user), PUT /Users/{id} (replace user), PATCH /Users/{id} (update user), DELETE /Users/{id} (deprovision user), GET /Groups (list groups), POST /Groups (create group), PATCH /Groups/{id} (update group members), DELETE /Groups/{id} (delete group)
Limitations:
- Requires Shopify Plus plan; not available on Basic, Shopify, or Advanced plans.
- SAML SSO must be configured before SCIM provisioning can be enabled.
- SCIM manages organization-level staff users, not storefront customers.
- Supported IdPs with documented integration: Okta, Microsoft Entra ID (Azure AD), OneLogin.
- Google Workspace is not officially supported as a SCIM IdP.
- SCIM token is generated in the Shopify Organization Admin under Security settings.
- Group support maps to Shopify user groups/permissions; not all IdP group attributes are supported.
- Deprovisioning via DELETE removes the user from the organization but does not delete their Shopify account.
Common scenarios
Three integration patterns cover the primary use cases. For staff provisioning, SCIM 2.
0 via Okta or Entra ID is the supported path: generate a SCIM token in Organization Admin → Security, configure the SCIM 2. 0 base URL and Bearer token in the IdP, then assign users - Shopify handles POST /Users on assignment and DELETE /Users on deprovisioning.
Note that DELETE removes org access but does not delete the underlying Shopify account. For storefront customer lifecycle management, combine the REST Customer API with webhook subscriptions (customers/create, customers/update, customers/delete) registered via POST /admin/api/2024-01/webhooks.
json; implement idempotency using X-Shopify-Webhook-Id to handle duplicate deliveries.
For GDPR erasure, DELETE /customers/{id} returns 422 for any customer with order history - use the GraphQL customerAnonymize mutation instead, which is irreversible and should be gated behind explicit confirmation logic in your integration.
Provision a new staff user via SCIM (Okta integration)
- Ensure Shopify Plus plan is active and SAML SSO is configured in Organization Admin > Security.
- Generate a SCIM API token in Shopify Organization Admin > Security > SCIM.
- In Okta, configure the Shopify app with SCIM 2.0 base URL: https://{organization}.myshopify.com/scim/v2 and Bearer token authentication.
- Assign the user to the Shopify app in Okta; Okta sends POST /Users to the SCIM endpoint.
- Shopify creates the staff user in the organization and sends an activation email.
- Verify user appears in Shopify Organization Admin > Users.
Watch out for: SCIM token must be regenerated if rotated; update it in Okta immediately to avoid provisioning failures. Deprovisioning removes org access but the Shopify account persists.
Create and tag a storefront customer, then listen for updates via webhook
- Register a webhook for customers/create via POST /admin/api/2024-01/webhooks.json with topic and address fields.
- POST /admin/api/2024-01/customers.json with email, first_name, last_name, and tags fields.
- Shopify creates the customer and fires the customers/create webhook to your endpoint.
- Validate the webhook payload using HMAC-SHA256 with X-Shopify-Hmac-Sha256 header.
- To update tags later, PUT /admin/api/2024-01/customers/{id}.json with updated tags string.
- customers/update webhook fires automatically on any field change.
Watch out for: Webhook delivery is not guaranteed exactly-once; implement idempotency using the X-Shopify-Webhook-Id header to deduplicate.
Anonymize a customer for GDPR compliance
- Identify the customer ID from GET /admin/api/2024-01/customers/search.json?query=email:{email}.
- Attempt DELETE /admin/api/2024-01/customers/{id}.json; if customer has orders, this returns 422.
- For customers with orders, use GraphQL mutation: customerAnonymizePayload via POST to /admin/api/2024-01/graphql.json.
- Mutation: mutation { customerAnonymize(customerId: "gid://shopify/Customer/{id}") { anonymizedCustomer { id } userErrors { field message } } }
- Verify the customer's PII fields are replaced with anonymized values in the response.
Watch out for: customerAnonymize is irreversible. Anonymized customers cannot be restored. Ensure you have confirmed the erasure request before executing.
Why building this yourself is a trap
Several non-obvious constraints cause integration failures in production. Cursor-based pagination is mandatory for the customers endpoint - the legacy page param is silently ignored or errors; always follow the Link header rel='next' page_info value.
GraphQL uses a point-cost rate limit (50,000-point bucket, 1,000 points/second restore on non-Plus; doubled on Plus) that is separate from and additive to REST limits - deeply nested GraphQL queries can exhaust the bucket faster than expected; inspect the throttleStatus field in the extensions response object.
REST API versions are date-based and supported for approximately one year; apps that do not proactively upgrade will hit deprecation failures. SCIM provisioning operates only at the organization level on Shopify Plus - it cannot be scoped to a single store, and Google Workspace is not an officially supported SCIM IdP.
Finally, the read_staff_members and write_staff_members scopes are only meaningful on Plus org tokens; requesting them on a standard store token returns empty results without an explicit error.
Automate Shopify workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.