docs/developer/core-concepts/customers.mdx
import { Since } from '/snippets/since.mdx';
Customers interact with your store through the Store API. They can register, log in, manage their profile, and view order history.
erDiagram
Customer ||--o{ Order : "places"
Customer ||--o{ Address : "has many"
Customer ||--o{ Wishlist : "has many"
Customer ||--o{ StoreCredit : "has many"
Customer ||--o{ PaymentSource : "has many"
Customer {
string id
string email
string first_name
string last_name
}
Order {
string number
string state
}
Address {
string firstname
string lastname
string address1
string city
}
Wishlist {
string name
boolean is_default
}
const { token, user } = await client.customers.create({
email: '[email protected]',
password: 'password123',
password_confirmation: 'password123',
first_name: 'John',
last_name: 'Doe',
})
// token => JWT token for subsequent authenticated requests
// user => { id: "cus_abc123", email: "[email protected]", first_name: "John", ... }
curl -X POST 'https://api.mystore.com/api/v3/store/customers' \
-H 'X-Spree-Api-Key: pk_xxx' \
-H 'Content-Type: application/json' \
-d '{
"email": "[email protected]",
"password": "password123",
"password_confirmation": "password123",
"first_name": "John",
"last_name": "Doe"
}'
import { createAdminClient } from '@spree/admin-sdk'
const client = createAdminClient({
baseUrl: 'https://store.example.com',
secretKey: 'sk_xxx',
})
const customer = await client.customers.create({
email: '[email protected]',
first_name: 'John',
last_name: 'Doe',
tags: ['wholesale'],
})
const { token, user } = await client.auth.login({
email: '[email protected]',
password: 'password123',
})
// Use the token for authenticated requests
curl -X POST 'https://api.mystore.com/api/v3/store/auth/login' \
-H 'X-Spree-API-Key: pk_xxx' \
-H 'Content-Type: application/json' \
-d '{
"email": "[email protected]",
"password": "password123"
}'
The response includes a JWT token and a user object. Pass the token in subsequent requests via the Authorization: Bearer <token> header.
Refresh an expiring token to keep the session alive. For how client.auth.login and client.auth.refresh fit into configuring the SDK for guest vs customer (JWT) auth, see the SDK authentication guide:
const { token, refresh_token } = await client.auth.refresh({
refresh_token: existingRefreshToken,
})
// Persist the rotated refresh_token for the next refresh
curl -X POST 'https://api.mystore.com/api/v3/store/auth/refresh' \
-H 'X-Spree-Api-Key: pk_xxx' \
-H 'Content-Type: application/json' \
-d '{ "refresh_token": "rt_xxx" }'
Password reset is a two-step flow. First, request a reset email. Then, use the token from the email to set a new password.
await client.passwordResets.create({
email: '[email protected]',
redirect_url: 'https://myshop.com/reset-password',
})
// Always returns { message: "..." } — even if the email doesn't exist
// This prevents email enumeration
curl -X POST 'https://api.mystore.com/api/v3/store/password_resets' \
-H 'X-Spree-Api-Key: pk_xxx' \
-H 'Content-Type: application/json' \
-d '{
"email": "[email protected]",
"redirect_url": "https://myshop.com/reset-password"
}'
The optional redirect_url parameter specifies where the password reset link in the email should point to. The token will be appended as a query parameter (e.g., https://myshop.com/reset-password?token=...). If the store has Allowed Origins configured, the redirect_url must match one of them.
This fires a customer.password_reset_requested event with the reset token in the payload. If you're using the spree_emails package, the email is sent automatically. Otherwise, subscribe to this event to send the reset email yourself (see Events).
const { token, user } = await client.passwordResets.update(
'reset-token-from-email',
{
password: 'newsecurepassword',
password_confirmation: 'newsecurepassword',
}
)
// Returns JWT token + user (auto-login)
curl -X PATCH 'https://api.mystore.com/api/v3/store/password_resets/RESET_TOKEN' \
-H 'X-Spree-Api-Key: pk_xxx' \
-H 'Content-Type: application/json' \
-d '{
"password": "newsecurepassword",
"password_confirmation": "newsecurepassword"
}'
On success, the user is automatically logged in and a JWT token is returned. The reset token expires after 15 minutes (configurable via Spree::Config.customer_password_reset_expires_in) and is single-use (changing the password invalidates it).
Headless storefronts often need to collect newsletter signups before account creation (footer forms, popup overlays). The Store API exposes a double opt-in subscription flow that mirrors the password reset webhook pattern.
await client.newsletterSubscribers.create({
email: '[email protected]',
redirect_url: 'https://myshop.com/newsletter/confirm',
})
// Returns the NewsletterSubscriber (verified: false for guests)
curl -X POST 'https://api.mystore.com/api/v3/store/newsletter_subscribers' \
-H 'X-Spree-Api-Key: pk_xxx' \
-H 'Content-Type: application/json' \
-d '{
"email": "[email protected]",
"redirect_url": "https://myshop.com/newsletter/confirm"
}'
The optional redirect_url points at the storefront page that will receive the verification token. It is validated against the store's Allowed Origins — URLs that do not match are silently dropped from the webhook payload (secure-by-default).
This fires a newsletter_subscriber.subscription_requested event whose payload includes email, verification_token, and the validated redirect_url. Subscribe to this event from your storefront's webhook handler to send the confirmation email — the link in the email should point to <redirect_url>?token=<verification_token>. The bundled spree_emails package also listens to this event and sends a default confirmation email if you're not running a headless storefront.
If the request is authenticated via a customer JWT and the JWT's email matches the subscribed email, the subscription is auto-verified and no event is fired (the user has already proven email ownership).
await client.newsletterSubscribers.verify({
token: 'token-from-email',
})
// Returns the verified NewsletterSubscriber
curl -X POST 'https://api.mystore.com/api/v3/store/newsletter_subscribers/verify' \
-H 'X-Spree-Api-Key: pk_xxx' \
-H 'Content-Type: application/json' \
-d '{ "token": "token-from-email" }'
On success, the subscriber is marked verified. If the subscription is linked to a customer record, that customer's accepts_email_marketing flag is also set to true. Consent is preserved across registration: if a guest subscribes and later registers with the same email, the existing subscriber is reused — registration won't accidentally reset their opt-in state.
// Get current customer
const customer = await client.customer.get()
// {
// id: "cus_xxx",
// email: "[email protected]",
// first_name: "John",
// last_name: "Doe",
// default_shipping_address: { ... },
// default_billing_address: { ... },
// addresses: [{ ... }, { ... }],
// }
// Update profile
const updated = await client.customer.update({
first_name: 'Jonathan',
accepts_email_marketing: true,
})
# Get current customer
curl 'https://api.mystore.com/api/v3/store/customers/me' \
-H 'X-Spree-API-Key: pk_xxx' \
-H 'Authorization: Bearer <jwt_token>'
# Update profile
curl -X PATCH 'https://api.mystore.com/api/v3/store/customers/me' \
-H 'X-Spree-API-Key: pk_xxx' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{ "first_name": "Jonathan", "accepts_email_marketing": true }'
Authenticated customers have access to these resources:
| Resource | Description |
|---|---|
| Addresses | Billing and shipping addresses with default selection |
| Orders | Past order history |
| Credit Cards | Saved credit cards for checkout |
| Payment Sources | Other saved payment methods (PayPal, Klarna, etc.) |
| Store Credits | Balance assigned by the store, usable at checkout |
| Gift Cards | Gift cards owned by or assigned to the customer |
| Wishlists | Saved product lists |
Customers don't need to register to purchase. Guest checkout uses an order token (X-Spree-Token) to identify the cart. See Orders — Cart for details.