Summary and recommendation
Spree's Platform API (`/api/v2/platform/users`) provides full CRUD over user accounts and is the correct integration surface for automated provisioning.
Authentication uses OAuth 2.0 via the Doorkeeper gem;
all Platform API calls require a `client_credentials` grant with `admin` scope.
The API follows JSON:API spec, uses offset-based pagination (`page`/`per_page`, max 500), and has no built-in rate limiting - throttling must be added at the infrastructure layer via `rack-attack` or a reverse proxy.
For teams building identity graph pipelines, the `public_metadata` and `private_metadata` fields on the user object are the designated extension points for storing external system identifiers (e.g., IdP subject IDs, CRM references) without schema changes.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 Bearer Token (doorkeeper gem) |
| Base URL | Official docs |
| SCIM available | No |
Authentication
Auth method: OAuth 2.0 Bearer Token (doorkeeper gem)
Setup steps
- Install and configure the doorkeeper gem (included in spree_auth_devise by default).
- Create an OAuth application via the Spree admin panel at /admin/oauth_applications or via Rails console: Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: '', scopes: 'admin').
- Obtain a client_credentials token: POST /api/v2/oauth/token with grant_type=client_credentials, client_id, and client_secret.
- Include the token in all requests as: Authorization: Bearer
. - For user-scoped (storefront) tokens use grant_type=password with username and password.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| admin | Full access to Platform API resources including user management CRUD. | All Platform API /api/v2/platform/* endpoints including users. |
| api_v2 | General Storefront API access for the authenticated user's own data. | Storefront API /api/v2/storefront/* endpoints (own account only). |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer | Unique user identifier. | auto-assigned | read-only | |
| string | User's email address; used as login. | required | optional | Must be unique per store. | |
| password | string | Plain-text password (write-only). | required | optional | Never returned in responses. |
| password_confirmation | string | Must match password on create/update. | required | optional | Write-only. |
| first_name | string | User's first name. | optional | optional | |
| last_name | string | User's last name. | optional | optional | |
| phone | string | User's phone number. | optional | optional | |
| spree_role_ids | array[integer] | Array of Spree::Role IDs assigned to the user. | optional | optional | Use role name 'admin' to grant admin access. |
| ship_address_id | integer | Default shipping address ID. | optional | optional | |
| bill_address_id | integer | Default billing address ID. | optional | optional | |
| created_at | datetime | ISO 8601 timestamp of account creation. | auto-assigned | read-only | |
| updated_at | datetime | ISO 8601 timestamp of last update. | auto-assigned | auto-assigned | |
| completed_orders_count | integer | Number of completed orders. | read-only | read-only | |
| store_credits_total | decimal | Total store credit balance. | read-only | read-only | |
| public_metadata | object | Arbitrary key-value metadata visible to storefront. | optional | optional | |
| private_metadata | object | Arbitrary key-value metadata visible to admins only. | optional | optional |
Core endpoints
List Users
- Method: GET
- URL:
/api/v2/platform/users - Watch out for: Requires admin-scoped OAuth token. Storefront tokens only return the authenticated user's own data.
Request example
GET /api/v2/platform/users?page=1&per_page=25
Authorization: Bearer <admin_token>
Response example
{
"data": [{"id":"1","type":"user","attributes":{"email":"alice@example.com",...}}],
"meta": {"count":1,"total_count":1,"total_pages":1},
"links": {"self":"...","next":null,"prev":null}
}
Get User
- Method: GET
- URL:
/api/v2/platform/users/{id} - Watch out for: Returns 404 if user belongs to a different store in a multi-store setup.
Request example
GET /api/v2/platform/users/42
Authorization: Bearer <admin_token>
Response example
{
"data": {
"id": "42",
"type": "user",
"attributes": {"email":"bob@example.com","first_name":"Bob"}
}
}
Create User
- Method: POST
- URL:
/api/v2/platform/users - Watch out for: password and password_confirmation are required. Duplicate email returns HTTP 422 with validation errors.
Request example
POST /api/v2/platform/users
Authorization: Bearer <admin_token>
Content-Type: application/json
{"user":{"email":"new@example.com","password":"s3cr3t","password_confirmation":"s3cr3t"}}
Response example
{
"data": {
"id": "99",
"type": "user",
"attributes": {"email":"new@example.com","created_at":"2024-01-01T00:00:00.000Z"}
}
}
Update User
- Method: PATCH
- URL:
/api/v2/platform/users/{id} - Watch out for: Sending spree_role_ids replaces all existing roles; omitting the field leaves roles unchanged.
Request example
PATCH /api/v2/platform/users/99
Authorization: Bearer <admin_token>
Content-Type: application/json
{"user":{"first_name":"Alice","spree_role_ids":[1]}}
Response example
{
"data": {
"id": "99",
"type": "user",
"attributes": {"first_name":"Alice","email":"new@example.com"}
}
}
Delete User
- Method: DELETE
- URL:
/api/v2/platform/users/{id} - Watch out for: Spree soft-deletes users by default (paranoia/discard gem). The record is not permanently removed unless explicitly purged.
Request example
DELETE /api/v2/platform/users/99
Authorization: Bearer <admin_token>
Response example
HTTP 204 No Content
Get Current Storefront User
- Method: GET
- URL:
/api/v2/storefront/account - Watch out for: Uses a password-grant user token, not a client_credentials admin token.
Request example
GET /api/v2/storefront/account
Authorization: Bearer <user_token>
Response example
{
"data": {
"id": "42",
"type": "user",
"attributes": {"email":"bob@example.com","first_name":"Bob"}
}
}
Update Current Storefront User
- Method: PATCH
- URL:
/api/v2/storefront/account - Watch out for: Users cannot update their own roles via this endpoint; role changes require admin token.
Request example
PATCH /api/v2/storefront/account
Authorization: Bearer <user_token>
Content-Type: application/json
{"user":{"first_name":"Bob","phone":"555-1234"}}
Response example
{
"data": {
"id": "42",
"type": "user",
"attributes": {"first_name":"Bob","phone":"555-1234"}
}
}
Obtain OAuth Token
- Method: POST
- URL:
/api/v2/oauth/token - Watch out for: client_credentials grant yields an admin-level token. Use grant_type=password with username/password for user-scoped tokens.
Request example
POST /api/v2/oauth/token
Content-Type: application/json
{"grant_type":"client_credentials","client_id":"APP_ID","client_secret":"APP_SECRET"}
Response example
{
"access_token": "abc123...",
"token_type": "Bearer",
"expires_in": 7200,
"created_at": 1700000000
}
Rate limits, pagination, and events
Rate limits: Spree is self-hosted open-source software. No built-in rate limiting is enforced by the framework itself. Rate limiting must be implemented by the deployer via Rack middleware (e.g., rack-attack gem) or an upstream reverse proxy/CDN.
Rate-limit headers: No
Retry-After header: No
Rate-limit notes: Official docs do not specify any default rate limits or rate-limit response headers. Operators must configure rack-attack or equivalent independently.
Pagination method: offset
Default page size: 25
Max page size: 500
Pagination pointer: page / per_page
Webhooks available: Yes
Webhook notes: Spree supports webhooks natively from v4.3+. Webhook subscribers can be configured via the admin UI or Platform API at /api/v2/platform/webhooks/subscribers. Events are dispatched asynchronously via ActiveJob.
Alternative event strategy: For older Spree versions without native webhooks, use ActiveRecord callbacks or the spree_webhooks community gem.
Webhook events: order.completed, order.canceled, product.created, product.updated, product.deleted, user.created, user.updated, user.deleted
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Not documented
- Endpoint: Not documented
Limitations:
- Spree has no native SCIM 2.0 implementation.
- As a self-hosted Rails application, SCIM provisioning must be built custom (e.g., using the scimitar or scim_rails gem).
- No official Okta, Entra ID, or Google Workspace SCIM connector exists for Spree.
Common scenarios
Three scenarios cover the primary provisioning lifecycle.
To provision an admin: obtain a client_credentials token, resolve the admin role ID via GET /api/v2/platform/roles?filter[name]=admin, then POST /api/v2/platform/users with spree_role_ids set - omitting that field creates a Customer, not an Admin.
To deprovision: DELETE /api/v2/platform/users/{id} returns HTTP 204 and soft-deletes the record via the paranoia/discard gem;
the user disappears from list responses but remains in the database and requires really_destroy! for a hard delete.
To sync external identity data: PATCH /api/v2/platform/users/{id} with private_metadata for internal attributes and public_metadata for storefront-visible fields
note that public_metadata is readable by the authenticated user via the Storefront API, so sensitive identifiers belong in private_metadata only.
Provision a new admin user via Platform API
- POST /api/v2/oauth/token with grant_type=client_credentials to obtain an admin Bearer token.
- Retrieve the admin role ID: GET /api/v2/platform/roles?filter[name]=admin to find the role ID (typically 1).
- POST /api/v2/platform/users with body {"user":{"email":"admin@co.com","password":"...","password_confirmation":"...","spree_role_ids":[1]}}.
- Confirm HTTP 201 response and store the returned user id for future management.
Watch out for: If spree_role_ids is omitted, the user is created as a regular customer with no admin access.
Deactivate (soft-delete) a user
- Obtain admin Bearer token via POST /api/v2/oauth/token.
- Identify the user: GET /api/v2/platform/users?filter[email]=target@example.com.
- DELETE /api/v2/platform/users/{id} - returns HTTP 204.
- Verify the user no longer appears in GET /api/v2/platform/users list responses (soft-deleted records are filtered out).
Watch out for: Soft-deleted users are not permanently removed. To hard-delete, execute Spree::User.with_deleted.find(id).really_destroy! in the Rails console or a custom admin action.
Update user metadata for external system integration
- Obtain admin Bearer token.
- PATCH /api/v2/platform/users/{id} with body {"user":{"public_metadata":{"crm_id":"CRM-001"},"private_metadata":{"internal_tier":"gold"}}}.
- Confirm updated_at changes in the response to verify the write succeeded.
- Use GET /api/v2/platform/users/{id} to read back and confirm metadata fields.
Watch out for: public_metadata is visible via the Storefront API to the authenticated user; do not store sensitive data there. private_metadata is admin-only.
Why building this yourself is a trap
Several behaviors will cause silent failures if not handled explicitly. spree_role_ids on a PATCH request is a full replacement - sending only the new role ID strips all existing roles; omit the field entirely when roles are not changing.
Spree is multi-store aware: user records and API responses are scoped to the store resolved from the request's Host header, so cross-store lookups will return HTTP 404 rather than a cross-store result.
Token expiry defaults to 7200 seconds with no automatic refresh unless the refresh_token grant is explicitly configured - unhandled expiry will produce 401s mid-pipeline. Finally, there is no native SCIM 2.0 endpoint;
integrations with Okta, Entra ID, or Google Workspace require a custom SCIM layer built with a gem such as scimitar or scim_rails, and no official connector exists.
Teams using an orchestration layer like an MCP server with 60+ deep IT/identity integrations should account for the absence of SCIM when mapping Spree into a broader identity graph.
Automate Spree Commerce 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.