docs/plans/6.0-admin-api.md
Status: In Progress
Target: Spree 6.0
Depends on: 6.0-admin-spa.md, 6.0-platform-auth.md
Author: damian
Last updated: 2026-06-25
A new REST API namespace (/api/v3/admin/*) that exposes every admin resource and operation currently served by the spree/admin Rails engine. Provides the programmatic backbone for the new React admin SPA, the TypeScript SDK, the CLI, and any other admin-facing tooling. Follows the same conventions as the Store API (flat JSON, prefixed IDs, Ransack filters, Pagy pagination, CanCanCan authorization) with a separate JWT audience and admin-scoped authentication.
Spree::Api::V3::Admin::ResourceController — no bespoke controllers unless absolutely necessary.POST to create, PATCH to update, DELETE to destroy. No custom verbs except for state-machine actions (/cancel, /approve, etc.).admin_api) — admin tokens MUST NOT be valid on the Store API and vice versa. Uses Spree.admin_user_class.spree_sk_xxx) via Authorization: Bearer for server-to-serverAuthorization: Bearer for SPA sessionsdocs/api-reference/store-api/errors.mdx).record.update! would embed non-trivial business rules in the controller./orders/:id/line_items/:id is fine, /orders/:id/shipments/:id/rates/:id is not. Flatten with filter params when necessary.Spree::Asset (ActiveStorage + alt text + ordering + variant scoping).Spree::Api::V3::BaseController # shared concerns: pagination, Ransack, caching
└── Spree::Api::V3::Admin::BaseController
└── Spree::Api::V3::Admin::ResourceController # default CRUD
└── Spree::Api::V3::Admin::ProductsController
└── Spree::Api::V3::Admin::OrdersController
└── ...
ResourceController provides a default index/show/create/update/destroy implementation. Subclasses override model_class, serializer_class, and optionally scope, permitted_params, find_resource, scope_includes. Most admin controllers should fit in 30–80 lines.
Spree::Api::V3::AdminAuthentication concern added to Admin::BaseControllerSpree::RefreshToken — rotated on every refresh/auth/login, /auth/refresh live on Admin::AuthController which skips authenticate_admin!{ token, refresh_token, user } shape{ id: ..., name: ... } not { product: { id: ... } }){ data: [...], meta: { total_count, page, pages, limit } }prod_86Rf07xd4z, variant_k5nR8xLq)?expand=variants,variants.prices to include associations (max 4 levels)?q[name_cont]=shirt for Ransack filtering?sort=-created_at for sorting (JSON:API convention){
"name": "Test product",
"tax_category_id": "taxcat_abc1234",
"taxon_ids": ["taxon_abc123", "taxon_def456"],
"tags": ["eco", "best-seller"],
"variants": [
{
"option_type": "size",
"option_value": "small",
"total_on_hand": 10,
"track_inventory": true,
"width": 10.5,
"height": 5.2,
"depth": 2.1,
"weight": 0.5,
"dimensions_unit": "cm",
"weight_unit": "kg",
"prices": [
{ "currency": "USD", "amount": 10.99 },
{ "currency": "EUR", "amount": 9.99 }
]
}
]
}
Validation errors return 422 with:
{
"error": {
"code": "validation_error",
"message": "Name can't be blank",
"details": { "name": ["can't be blank"] }
}
}
{
"currency": "USD",
"locale": "en-US",
"line_items": [
{ "variant_id": "variant_123456789", "quantity": 1 },
{ "variant_id": "variant_987654321", "quantity": 2 }
]
}
GET /meGET /api/v3/admin/me returns the current admin user plus a serialized list of their permissions (CanCanCan rules flattened). The SPA consumes this to drive UI permission checks; the server still enforces authorize! on every action.
{
"user": { "id": "admin_xxx", "email": "...", ... },
"permissions": [
{ "allow": true, "actions": ["manage"], "subjects": ["all"], "has_conditions": false },
{ "allow": false, "actions": ["destroy"], "subjects": ["Spree::Order"], "has_conditions": true }
]
}
Per-record conditions (CanCanCan blocks/hashes) are not serialized — only has_conditions: true as a hint. The SPA shows actions optimistically and handles 403 from the API.
Spree::Api::V3::BulkOperations), custom_fieldable route concern, Active Storage direct uploads — all in place.6.0-inventory-operations.md)decisions.md 2026-06-16bundle exec rake typelizer:generate + pnpm generate:zod@spree/admin-sdk[x] in this planbundle exec rake rswag:specs:swaggerizeAdmin::ResourceController hierarchy without a written exception in this plan.record.update is almost always enough.This plan reflects the 6.0 model/naming landscape. Endpoints removed from 5.x or renamed are tracked in the Removed / Renamed endpoints table at the top so contributors don't accidentally re-implement them.
| 5.x endpoint | 6.0 endpoint | Reason |
|---|---|---|
/api/v3/admin/taxonomies/* | removed | Spree::Taxonomy dropped; Category belongs directly to store. See 6.0-replace-taxons-with-categories.md |
/api/v3/admin/taxons/* | /api/v3/admin/categories/* | Spree::Taxon → Spree::Category rename |
/api/v3/admin/shipping_methods/* | /api/v3/admin/delivery_methods/* | ShippingMethod → DeliveryMethod rename |
/api/v3/admin/orders/:id/shipments/* | /api/v3/admin/orders/:id/fulfillments/* | Shipment → Fulfillment rename |
/api/v3/admin/shipping_categories/* | removed | Replaced by ProductType.fulfillment_types. See 6.0-fulfillment-and-delivery.md |
/api/v3/admin/zones/* | removed | Tax zones → direct country/state FKs on TaxRate. Shipping zones → new DeliveryZone. See 6.0-tax-provider.md, 6.0-fulfillment-and-delivery.md |
/api/v3/admin/return_authorizations/* | /api/v3/admin/returns/* | ReturnAuthorization → Return. See 6.0-returns-exchanges-claims.md |
/api/v3/admin/customer_returns/* | removed | Merged into Return + ReturnItem |
/api/v3/admin/orders/:id/reimbursements/* | removed | Replaced by direct Refund on Returns/Exchanges/Claims |
/api/v3/admin/reimbursement_types/* | removed | Concept dropped |
/api/v3/admin/prototypes/* | /api/v3/admin/product_types/* | Prototype → ProductType rename. See 6.0-product-types.md |
/api/v3/admin/metafield_definitions/* | /api/v3/admin/custom_field_definitions/* | Metafield → CustomField rename. See 5.4-6.0-custom-fields-rename.md |
/api/v3/admin/orders/:id/adjustments/* | /api/v3/admin/orders/:id/tax_lines, discounts, fees (read-only) | Polymorphic Adjustment → typed TaxLine, Discount, Fee. See 6.0-split-adjustments.md |
?expand=master_variant | ?expand=default_variant | is_master dropped, default_variant_id FK. See 6.0-remove-master-variant.md |
?expand=images | ?expand=media | Image / Asset → Media. See 5.4-6.0-product-media-system.md |
The following field names change across all endpoints. See 5.4-store-api-naming-standardization.md for the full list.
| Model | 5.x | 6.0 |
|---|---|---|
Payment | state | status |
Fulfillment (was Shipment) | state | status |
Fulfillment | shipped_at | fulfilled_at |
FulfillmentItem (was InventoryUnit) | state | status |
Return (was ReturnAuthorization) | state | status |
GiftCard | state | status |
Order | shipment_state | fulfillment_status |
Order | payment_state | payment_status |
Order | shipment_total | delivery_total |
Order | promo_total | discount_total |
Order | item_count | total_quantity |
Order | special_instructions | customer_note |
Order | bill_address_id | billing_address_id |
Order | ship_address_id | shipping_address_id |
LineItem | promo_total | discount_total |
Address | firstname | first_name |
Address | lastname | last_name |
Address | zipcode | postal_code |
OptionType, OptionValue, CustomFieldDefinition | presentation | label |
CreditCard | cc_type | brand |
CreditCard | last_digits | last4 |
CustomFieldDefinition | display_on (string) | storefront_visible (boolean) |
CustomFieldDefinition | metafield_type | field_type |
Also tracked in the SDK type definitions. See each linked plan for rationale.
| Model | 5.x prefix | 6.0 prefix |
|---|---|---|
Fulfillment (was Shipment) | shipment_ | ful_ |
DeliveryMethod (was ShippingMethod) | — | dm_ |
DeliveryRate (was ShippingRate) | — | dr_ |
FulfillmentItem (was InventoryUnit) | — | fi_ |
DeliveryZone (new) | — | dz_ |
StockReservation (new) | — | res_ |
StockMovement | — | sm_ |
Return (was ReturnAuthorization) | — | ret_ |
Exchange (new) | — | exch_ |
Claim (new) | — | claim_ |
Channel (new) | — | ch_ |
Catalog (new) | — | cat_ |
Company (new) | — | comp_ |
CompanyLocation (new) | — | cloc_ |
CompanyContact (new) | — | cc_ |
ProductPublication (replaces StoreProduct) | — | pp_ |
Collection (new) | — | coll_ |
ProductType (was Prototype) | proto_ | pt_ |
Customer (was User) | user_ | cust_ |
AdminUser | admin_ | adm_ |
CustomField (was Metafield) | mf_ | cf_ |
CustomFieldDefinition (was MetafieldDefinition) | mfdef_ | cfdef_ |
Media (was Image/Asset) | image_/asset_ | media_ |
Each endpoint below is tracked as [ ] (pending) or [x] (shipped). When you implement one, flip the checkbox in the same PR. Endpoints are grouped by resource. Removed/renamed 5.x endpoints are NOT listed — see the table above.
POST /api/v3/admin/auth/login — admin user login (JWT + httpOnly refresh cookie)POST /api/v3/admin/auth/refresh — rotate refresh token (read from signed cookie), return new JWTPOST /api/v3/admin/auth/logout — invalidate JWT and refresh token, clear cookieGET /api/v3/admin/me — current admin user + serialized permissionsPATCH /api/v3/admin/me — self-service profile updates (e.g. the admin's own UI language)GET /api/v3/admin/auth/invitations/:id/lookup — public invitation lookup (prefixed ID + token in URL act as credential)POST /api/v3/admin/auth/invitations/:id/accept — public invitation acceptance (sets password, issues tokens)5.x → 6.0: Logout shipped as
POST(notDELETE); the refresh token moved to an httpOnly signed cookie and is no longer returned in the login response body. See5.5-admin-auth-cookie-refresh.md,6.0-platform-auth.md.
GET /api/v3/admin/products — list (Ransack, sort, paginate)GET /api/v3/admin/products/:idPOST /api/v3/admin/products — with nested variants, prices, category_idsPATCH /api/v3/admin/products/:idDELETE /api/v3/admin/products/:id — soft deletePOST /api/v3/admin/products/:id/clonePOST /api/v3/admin/products/bulk_status_update — bulk status changePOST /api/v3/admin/products/bulk_add_to_categories / bulk_remove_from_categoriesPOST /api/v3/admin/products/bulk_add_to_channels / bulk_remove_from_channelsPOST /api/v3/admin/products/bulk_add_tags / bulk_remove_tagsDELETE /api/v3/admin/products/bulk_destroyBulk envelope decision (resolved): per-action bulk routes (not a single
/bulk), bodies{ ids: [...], ... }, shared via theSpree::Api::V3::BulkOperationsconcern. Resolves the "Bulk operations envelope" open question.
6.0 field changes:
shipping_category_idis dropped (fulfillment types come fromProductType.fulfillment_types).prototype_id→product_type_id.taxon_ids→category_ids. Product hasdefault_variant_idFK (no moreis_master).
GET /api/v3/admin/products/:product_id/variantsGET /api/v3/admin/products/:product_id/variants/:idPOST /api/v3/admin/products/:product_id/variants — with nested pricesPATCH /api/v3/admin/products/:product_id/variants/:idDELETE /api/v3/admin/products/:product_id/variants/:idGET /api/v3/admin/variants — flat variant search (e.g. order line-item picker); supports custom_fieldsGET /api/v3/admin/variants/:idA standalone Spree::Price resource covers both base prices (price_list_id: nil) and price-list overrides, filtered via Ransack on index. (Variant create/update may still set prices inline via nested params, but prices are independently addressable.)
GET /api/v3/admin/prices — list (filter by variant, currency, price_list)GET /api/v3/admin/prices/:idPOST /api/v3/admin/pricesPATCH /api/v3/admin/prices/:idDELETE /api/v3/admin/prices/:id — soft deletePOST /api/v3/admin/prices/bulk_upsert — upsert on (variant_id, currency, price_list_id)DELETE /api/v3/admin/prices/bulk_destroyGET /api/v3/admin/products/:product_id/media — list (filterable by media_type)POST /api/v3/admin/products/:product_id/media — upload via multipart OR source_url param (async download). Params: media_type, alt, position, variant_idsPATCH /api/v3/admin/products/:product_id/media/:id — update alt, position, variant assignment (variant_ids: [])DELETE /api/v3/admin/products/:product_id/media/:idGET|POST /api/v3/admin/products/:product_id/variants/:variant_id/media + PATCH|DELETE .../media/:id — variant-level media (legacy path retained in 5.5)6.0: Media defaults to
viewable_type: 'Spree::Product'; variant↔product-asset linking via thevariant_idsarray (Spree::VariantMediajoin).expand=images→expand=media. See5.4-6.0-product-media-system.md.
GET /api/v3/admin/products/:product_id/digital_assetsPOST /api/v3/admin/products/:product_id/digital_assetsPATCH /api/v3/admin/products/:product_id/digital_assets/:idDELETE /api/v3/admin/products/:product_id/digital_assets/:idGET /api/v3/admin/option_typesGET /api/v3/admin/option_types/:idPOST /api/v3/admin/option_typesPATCH /api/v3/admin/option_types/:idDELETE /api/v3/admin/option_types/:idManaged via nested payload on the Option Type (option_values: [{ id, name, label, position, color_code, image, _destroy }]) — no standalone endpoints. Position drives drag-and-drop reorder; kind (dropdown/color_swatch/buttons) lives on the Option Type and color_code/image on each value. See 5.4-option-type-enhancements.md.
GET /api/v3/admin/product_typesGET /api/v3/admin/product_types/:idPOST /api/v3/admin/product_typesPATCH /api/v3/admin/product_types/:idDELETE /api/v3/admin/product_types/:id6.0:
fulfillment_typesJSON array column (['shipping']default). Product types can require specificCustomFieldDefinitions via nested definition records. See6.0-product-types.md.
GET /api/v3/admin/categories — list with tree metadataGET /api/v3/admin/categories/:idPOST /api/v3/admin/categoriesPATCH /api/v3/admin/categories/:idDELETE /api/v3/admin/categories/:idPATCH /api/v3/admin/categories/:id/reposition — reposition in tree6.0:
Spree::Categorybelongs directly toSpree::Store(no Taxonomy).Classification→ProductCategory. See6.0-replace-taxons-with-categories.md.
GET /api/v3/admin/categories/:category_id/productsPOST /api/v3/admin/categories/:category_id/products — assignPATCH /api/v3/admin/categories/:category_id/products/:product_id — update positionDELETE /api/v3/admin/categories/:category_id/products/:product_id — unassignGET /api/v3/admin/collectionsGET /api/v3/admin/collections/:idPOST /api/v3/admin/collections — with nested rules (automatic) or manual product listPATCH /api/v3/admin/collections/:idDELETE /api/v3/admin/collections/:id6.0: Collections replace the "smart taxon" (
automatic: true,rules_match_policy) feature.CollectionRulereplacesTaxonRule. See6.0-replace-taxons-with-categories.md.
GET /api/v3/admin/channelsGET /api/v3/admin/channels/:idPOST /api/v3/admin/channelsPATCH /api/v3/admin/channels/:idDELETE /api/v3/admin/channels/:idPOST /api/v3/admin/channels/:id/add_products — bulk publish (product_ids, optional published_at/unpublished_at)POST /api/v3/admin/channels/:id/remove_products — bulk unpublish (product_ids)6.0: A Channel is a sales surface (POS, Storefront A, Marketplace, etc.).
Channel#defaultresolves the store's default channel. See6.0-channels-catalogs-b2b.md,6.0-order-routing.md.
Nested product_publications CRUD was cut — the SPA never used it and there's no industry precedent. Publication is managed through Channel member actions (/channels/:id/add_products, /remove_products) and the product-side bulk routes (/products/bulk_add_to_channels, /products/bulk_remove_from_channels). See 6.0-channels-catalogs-b2b.md.
GET /api/v3/admin/catalogsGET /api/v3/admin/catalogs/:idPOST /api/v3/admin/catalogsPATCH /api/v3/admin/catalogs/:idDELETE /api/v3/admin/catalogs/:idGET /api/v3/admin/catalogs/:id/productsPOST /api/v3/admin/catalogs/:id/products — bulk assignDELETE /api/v3/admin/catalogs/:id/products/:product_idPOST /api/v3/admin/catalogs/:id/assign — assign catalog to companies/locationsGET /api/v3/admin/companiesGET /api/v3/admin/companies/:idPOST /api/v3/admin/companiesPATCH /api/v3/admin/companies/:idDELETE /api/v3/admin/companies/:idGET /api/v3/admin/companies/:company_id/locationsPOST /api/v3/admin/companies/:company_id/locationsGET /api/v3/admin/company_locations/:idPATCH /api/v3/admin/company_locations/:idDELETE /api/v3/admin/company_locations/:idGET /api/v3/admin/company_locations/:location_id/contactsPOST /api/v3/admin/company_locations/:location_id/contactsPATCH /api/v3/admin/company_contacts/:idDELETE /api/v3/admin/company_contacts/:idGET /api/v3/admin/ordersGET /api/v3/admin/orders/:idPOST /api/v3/admin/orders — create draftPATCH /api/v3/admin/orders/:idDELETE /api/v3/admin/orders/:idPATCH /api/v3/admin/orders/:id/completePATCH /api/v3/admin/orders/:id/cancelPATCH /api/v3/admin/orders/:id/approvePATCH /api/v3/admin/orders/:id/resumePOST /api/v3/admin/orders/:id/resend_confirmation6.0: Order state machine dropped (derived status from line items + fulfillments + payments — see
6.0-cart-order-split.md).PATCH /nextand/advanceno longer apply and have been removed from the 5.x admin API ahead of the 6.0 cart/order split. Cart and Order are separate models;LineItem.owneris polymorphic. Order gainschannel_id,company_location_id,withdrawal_eligible_untilFKs.
/carts/:id/items convention)GET /api/v3/admin/orders/:order_id/itemsGET /api/v3/admin/orders/:order_id/items/:idPOST /api/v3/admin/orders/:order_id/itemsPATCH /api/v3/admin/orders/:order_id/items/:idDELETE /api/v3/admin/orders/:order_id/items/:idGET /api/v3/admin/orders/:order_id/fulfillmentsGET /api/v3/admin/orders/:order_id/fulfillments/:idPOST /api/v3/admin/orders/:order_id/fulfillments — manual fulfillment creation (external carrier / 3PL sync): moves not-yet-shipped units out of routed fulfillments, bypassing order routing; optional items, delivery_method_id, tracking, status: 'shipped'PATCH /api/v3/admin/orders/:order_id/fulfillments/:idPATCH /api/v3/admin/orders/:order_id/fulfillments/:id/fulfill — mark as fulfilledPATCH /api/v3/admin/orders/:order_id/fulfillments/:id/cancelPATCH /api/v3/admin/orders/:order_id/fulfillments/:id/resumePATCH /api/v3/admin/orders/:order_id/fulfillments/:id/split6.0: Third-party fulfillment via
FulfillmentProviderinterface.pickup_point_dataJSON snapshot for pickup points. See6.0-fulfillment-and-delivery.md.
GET /api/v3/admin/orders/:order_id/paymentsGET /api/v3/admin/orders/:order_id/payments/:idPOST /api/v3/admin/orders/:order_id/payments — supports off-session admin charges via source_id (saved Spree::CreditCard belonging to the order's customer)PATCH /api/v3/admin/orders/:order_id/payments/:id/capturePATCH /api/v3/admin/orders/:order_id/payments/:id/voidOff-session charges: pass
{ payment_method_id, source_id, amount }to charge a saved card. There is no separate "PaymentSession" endpoint on the admin side — the cart/store API haspayment_sessionsfor SCA/3DS storefront flows; admin-initiated charges use saved sources directly.
GET /api/v3/admin/orders/:order_id/refundsPOST /api/v3/admin/orders/:order_id/refunds — with payment_id in bodyPATCH /api/v3/admin/orders/:order_id/refunds/:idGET /api/v3/admin/orders/:order_id/adjustmentsGET /api/v3/admin/orders/:order_id/adjustments/:id5.x → 6.0: The 5.x admin API exposes
/adjustmentsas read-only (no create/update/destroy) so client integrations don't depend on a write surface that is going away. Manual adjustment management has been removed. In 6.0, this endpoint is replaced by the three typed endpoints below.
GET /api/v3/admin/orders/:order_id/tax_linesGET /api/v3/admin/orders/:order_id/discountsGET /api/v3/admin/orders/:order_id/fees6.0: Polymorphic
Spree::Adjustmentis dropped. Tax, discount, and fee data is returned via typed, read-only endpoints. See6.0-split-adjustments.md.
Customer assignment is handled via the standard PATCH /api/v3/admin/orders/:id endpoint with customer_id in the body. No dedicated endpoint. Pass customer_id: null to unassign.
PATCH /api/v3/admin/orders/:id { customer_id } — assign / reassign / unassign customer (5.x bridge: also accepts user_id)POST /api/v3/admin/orders/:order_id/gift_cards { code } — apply a gift card by codeDELETE /api/v3/admin/orders/:order_id/gift_cards/:id — remove the applied gift cardPOST /api/v3/admin/orders/:order_id/store_credits { amount? } — apply customer's available store credit (auto up to balance, or a specific amount)DELETE /api/v3/admin/orders/:order_id/store_credits — remove store credit applicationPOST /api/v3/admin/orders/:order_id/discounts — apply promotion or couponDELETE /api/v3/admin/orders/:order_id/discounts/:id — removeGET /api/v3/admin/returnsGET /api/v3/admin/orders/:order_id/returnsGET /api/v3/admin/returns/:idPOST /api/v3/admin/orders/:order_id/returnsPATCH /api/v3/admin/returns/:idDELETE /api/v3/admin/returns/:idPOST /api/v3/admin/returns/:id/approvePOST /api/v3/admin/returns/:id/receivePOST /api/v3/admin/returns/:id/refundPATCH /api/v3/admin/returns/:return_id/items/:id — reception + acceptance statusGET /api/v3/admin/exchangesGET /api/v3/admin/orders/:order_id/exchangesGET /api/v3/admin/exchanges/:idPOST /api/v3/admin/orders/:order_id/exchangesPATCH /api/v3/admin/exchanges/:idPOST /api/v3/admin/exchanges/:id/approvePOST /api/v3/admin/exchanges/:id/receivePOST /api/v3/admin/exchanges/:id/fulfillGET /api/v3/admin/claimsGET /api/v3/admin/orders/:order_id/claimsGET /api/v3/admin/claims/:idPOST /api/v3/admin/orders/:order_id/claimsPATCH /api/v3/admin/claims/:idPOST /api/v3/admin/claims/:id/approvePOST /api/v3/admin/claims/:id/resolve6.0: Returns, Exchanges, and Claims are first-class models. Replaces the
ReturnAuthorization → CustomerReturn → Reimbursementchain. See6.0-returns-exchanges-claims.md.
GET /api/v3/admin/customersGET /api/v3/admin/customers/:idPOST /api/v3/admin/customersPATCH /api/v3/admin/customers/:idDELETE /api/v3/admin/customers/:idPOST /api/v3/admin/customers/bulk_add_to_groups / bulk_remove_from_groupsPOST /api/v3/admin/customers/bulk_add_tags / bulk_remove_tagsPOST /api/v3/admin/customers/:id/anonymize — GDPR right to erasure (Phase 2)GET /api/v3/admin/customers/:id/export — GDPR data export, returns JSON download URL (Phase 2)6.0:
Spree.user_class→Spree.customer_class.Spree::LegacyUser→Spree::Customer. Customers + nested addresses/credit_cards/store_credits + CustomerGroups shipped in 5.5 (5.5-admin-customers-api.md). Anonymize + export endpoints are Phase 2 GDPR (5.4-6.0-eu-legal-compliance.md); customer group membership is managed via the customer bulk routes above, not nestedmembersendpoints.
GET /api/v3/admin/customers/:customer_id/addressesGET /api/v3/admin/customers/:customer_id/addresses/:idPOST /api/v3/admin/customers/:customer_id/addressesPATCH /api/v3/admin/customers/:customer_id/addresses/:idDELETE /api/v3/admin/customers/:customer_id/addresses/:idDefault-address handling via
is_default_billing/is_default_shippingflags on the address PATCH (rotates the previous default in a transaction) — no separate FK write target.
GET /api/v3/admin/customers/:customer_id/credit_cardsGET /api/v3/admin/customers/:customer_id/credit_cards/:idDELETE /api/v3/admin/customers/:customer_id/credit_cards/:idNo create/update — cards are tokenized through the storefront/SCA flow; admin can only view and remove them.
cc_type→brand,last_digits→last4.
GET /api/v3/admin/customers/:customer_id/store_creditsGET /api/v3/admin/customers/:customer_id/store_credits/:idPOST /api/v3/admin/customers/:customer_id/store_creditsPATCH /api/v3/admin/customers/:customer_id/store_credits/:idDELETE /api/v3/admin/customers/:customer_id/store_credits/:idGET /api/v3/admin/gift_cardsGET /api/v3/admin/gift_cards/:idPOST /api/v3/admin/gift_cardsPATCH /api/v3/admin/gift_cards/:idDELETE /api/v3/admin/gift_cards/:idGET /api/v3/admin/gift_card_batchesGET /api/v3/admin/gift_card_batches/:idPOST /api/v3/admin/gift_card_batches — batch createGET /api/v3/admin/customer_groupsGET /api/v3/admin/customer_groups/:idPOST /api/v3/admin/customer_groupsPATCH /api/v3/admin/customer_groups/:idDELETE /api/v3/admin/customer_groups/:idNested members CRUD was not built — membership is managed from the customer side via POST /api/v3/admin/customers/bulk_add_to_groups / bulk_remove_from_groups (body: { ids: [...], customer_group_ids: [...] }). Group contents are read via GET /api/v3/admin/customers?q[customer_group_members_customer_group_id_eq]=....
GET /api/v3/admin/newsletter_subscribersDELETE /api/v3/admin/newsletter_subscribers/:idGET /api/v3/admin/companies/:company_id/tax_exemption_certificatesPOST /api/v3/admin/companies/:company_id/tax_exemption_certificatesPATCH /api/v3/admin/tax_exemption_certificates/:idDELETE /api/v3/admin/tax_exemption_certificates/:idGET /api/v3/admin/promotionsGET /api/v3/admin/promotions/:idPOST /api/v3/admin/promotionsPATCH /api/v3/admin/promotions/:idDELETE /api/v3/admin/promotions/:idPOST /api/v3/admin/promotions/:id/cloneGET /api/v3/admin/promotions/:promotion_id/promotion_actionsGET /api/v3/admin/promotions/:promotion_id/promotion_actions/:idPOST /api/v3/admin/promotions/:promotion_id/promotion_actionsPATCH /api/v3/admin/promotions/:promotion_id/promotion_actions/:idDELETE /api/v3/admin/promotions/:promotion_id/promotion_actions/:idGET /api/v3/admin/promotion_actions/types — registered action subclasses + preference schemas (for the "Add action" picker)GET /api/v3/admin/promotion_actions/calculators — registered calculator subclasses + preference schemas for CalculatedAdjustments actionsGET /api/v3/admin/promotions/:promotion_id/promotion_rulesGET /api/v3/admin/promotions/:promotion_id/promotion_rules/:idPOST /api/v3/admin/promotions/:promotion_id/promotion_rulesPATCH /api/v3/admin/promotions/:promotion_id/promotion_rules/:idDELETE /api/v3/admin/promotions/:promotion_id/promotion_rules/:idGET /api/v3/admin/promotion_rules/types — registered rule subclasses + preference schemas (for the "Add rule" picker)GET /api/v3/admin/promotions/:promotion_id/coupon_codesGET /api/v3/admin/promotions/:promotion_id/coupon_codes/:idGET /api/v3/admin/stock_locationsGET /api/v3/admin/stock_locations/:idPOST /api/v3/admin/stock_locationsPATCH /api/v3/admin/stock_locations/:idDELETE /api/v3/admin/stock_locations/:idGET /api/v3/admin/stock_items — filterable by stock_location, variantGET /api/v3/admin/stock_items/:idPATCH /api/v3/admin/stock_items/:idDELETE /api/v3/admin/stock_items/:idGET /api/v3/admin/stock_movements — filterable by kind, fulfillment_id, order_id, return_id, stock_transfer_id6.0:
StockMovementgainskindenum (received,allocated,shipped,released,adjusted) + concrete FKs to the originating event. See6.0-typed-stock-movements.md.
GET /api/v3/admin/stock_reservations — filterable by variant, order, cartGET /api/v3/admin/stock_reservations/:idDELETE /api/v3/admin/stock_reservations/:id — manual release6.0: Time-limited reservations during checkout. See
6.0-stock-reservations.md.
GET /api/v3/admin/stock_transfersGET /api/v3/admin/stock_transfers/:idPOST /api/v3/admin/stock_transfersDELETE /api/v3/admin/stock_transfers/:id — draft onlyPATCH /api/v3/admin/stock_transfers/:idPOST /api/v3/admin/stock_transfers/:id/mark_readyPOST /api/v3/admin/stock_transfers/:id/mark_in_transitPOST /api/v3/admin/stock_transfers/:id/receive — body { items: [{ id, quantity_received, discrepancy_reason }] } (partial receive)POST /api/v3/admin/stock_transfers/:id/cancel6.0: Transfer lifecycle (draft → ready_to_ship → in_transit → received) + partial receive are pending. See
6.0-inventory-operations.md.
GET /api/v3/admin/suppliersGET /api/v3/admin/suppliers/:idPOST /api/v3/admin/suppliersPATCH /api/v3/admin/suppliers/:idDELETE /api/v3/admin/suppliers/:id6.0:
Spree::Supplieris the procurement source for purchase orders (see6.0-inventory-operations.md). Not to be confused with marketplace Vendors below —Spree::Vendoris the marketplace seller (6.0-multi-vendor-marketplace.md,decisions.md2026-07-14).
GET /api/v3/admin/purchase_ordersGET /api/v3/admin/purchase_orders/:idPOST /api/v3/admin/purchase_ordersPATCH /api/v3/admin/purchase_orders/:idDELETE /api/v3/admin/purchase_orders/:id — draft onlyPOST /api/v3/admin/purchase_orders/:id/mark_orderedPOST /api/v3/admin/purchase_orders/:id/receive — body { items: [{ id, quantity_received }] }POST /api/v3/admin/purchase_orders/:id/cancel6.0:
Spree::PurchaseOrder+Spree::Supplierreplace today's "external receive" hack. See6.0-inventory-operations.md.
Per 6.0-multi-vendor-marketplace.md. Vendor prefix is ven_ (matches the legacy Enterprise gem).
GET /api/v3/admin/vendorsGET /api/v3/admin/vendors/:idPOST /api/v3/admin/vendorsPATCH /api/v3/admin/vendors/:idDELETE /api/v3/admin/vendors/:idPOST /api/v3/admin/vendors/:id/invite — (re)send the vendor invitation (Spree::Invitation, resource: vendor)POST /api/v3/admin/vendors/:id/approve / POST /api/v3/admin/vendors/:id/reject / POST /api/v3/admin/vendors/:id/suspend — lifecycle transitions (approve also un-suspends)GET /api/v3/admin/commission_ratesGET /api/v3/admin/commission_rates/:idPOST /api/v3/admin/commission_ratesPATCH /api/v3/admin/commission_rates/:id — saves rate fields + nested rules: [] in one round-trip (PriceList-style)DELETE /api/v3/admin/commission_rates/:idGET /api/v3/admin/commission_lines — read-only snapshot ledger (filter by vendor/order)GET /api/v3/admin/vendor_transfers — read-only earnings ledger (filter by vendor/payout/status)GET /api/v3/admin/vendor_payouts / GET /:idPOST /api/v3/admin/vendor_payouts/:id/complete — manual settlement (system provider)GET /api/v3/admin/order_groups / GET /:id — the multi-order transaction containerAPI key scopes: read_vendors / write_vendors, read_commissions / write_commissions, read_payouts / write_payouts.
GET /api/v3/admin/price_listsGET /api/v3/admin/price_lists/:idPOST /api/v3/admin/price_listsPATCH /api/v3/admin/price_lists/:id — saves name/schedule/match policy + product_ids: [] + nested rules: [] + prices: [] in one round-tripDELETE /api/v3/admin/price_lists/:idPATCH /api/v3/admin/price_lists/:id/activate — draft|inactive → active (or scheduled when starts_at is in the future)PATCH /api/v3/admin/price_lists/:id/deactivateGET /api/v3/admin/price_lists/:id/prices — spreadsheet data feed (filter by ?currency=)GET /api/v3/admin/price_lists/price_rule_types — registered rule subclasses + preference schemas (for the "Add rule" picker)No standalone nested endpoints. Price-list rules ride along on the price list's PATCH body (rules: [{ id, type, preferences }]); product membership rides along as product_ids: [...]. Individual price overrides flow through the standalone /prices resource (or the price list's prices: [] payload). See the Prices section above.
GET /api/v3/admin/products/:product_id/price_history — per-variant history for Omnibus "lowest price in 30 days"6.0: See
5.4-6.0-eu-legal-compliance.md.
GET /api/v3/admin/payment_methodsGET /api/v3/admin/payment_methods/:idPOST /api/v3/admin/payment_methodsPATCH /api/v3/admin/payment_methods/:idDELETE /api/v3/admin/payment_methods/:idGET /api/v3/admin/payment_methods/types — registered gateway subclasses + preference schemas (for the "Add payment method" picker)GET /api/v3/admin/delivery_methodsGET /api/v3/admin/delivery_methods/:idPOST /api/v3/admin/delivery_methodsPATCH /api/v3/admin/delivery_methods/:idDELETE /api/v3/admin/delivery_methods/:id6.0: No more
calculator_id. New fields:rate_provider(class name),fulfillment_provider(class name),pickup_point_provider(for third-party pickup),fulfillment_type(enum:shipping,pickup,pickup_point,digital,local_delivery). See6.0-delivery-rate-provider.md,6.0-fulfillment-and-delivery.md.
GET /api/v3/admin/delivery_zonesGET /api/v3/admin/delivery_zones/:idPOST /api/v3/admin/delivery_zonesPATCH /api/v3/admin/delivery_zones/:idDELETE /api/v3/admin/delivery_zones/:idGET /api/v3/admin/delivery_zones/:zone_id/membersPOST /api/v3/admin/delivery_zones/:zone_id/membersDELETE /api/v3/admin/delivery_zones/:zone_id/members/:idGET /api/v3/admin/tax_categoriesGET /api/v3/admin/tax_categories/:idPOST /api/v3/admin/tax_categoriesPATCH /api/v3/admin/tax_categories/:idDELETE /api/v3/admin/tax_categories/:idGET /api/v3/admin/tax_ratesGET /api/v3/admin/tax_rates/:idPOST /api/v3/admin/tax_ratesPATCH /api/v3/admin/tax_rates/:idDELETE /api/v3/admin/tax_rates/:id6.0:
zone_idFK is dropped.TaxRategets directcountry_id+state_idFKs. Tax logic moves behind aTaxProviderinterface. See6.0-tax-provider.md.
GET /api/v3/admin/marketsGET /api/v3/admin/markets/:idPOST /api/v3/admin/marketsPATCH /api/v3/admin/markets/:idDELETE /api/v3/admin/markets/:idGET /api/v3/admin/countries — supports ?expand=states for state/province dropdownsGET /api/v3/admin/countries/:idGET /api/v3/admin/storePATCH /api/v3/admin/storeGET /api/v3/admin/policiesGET /api/v3/admin/policies/:idPOST /api/v3/admin/policiesPATCH /api/v3/admin/policies/:idDELETE /api/v3/admin/policies/:idGET /api/v3/admin/admin_usersGET /api/v3/admin/admin_users/:idPATCH /api/v3/admin/admin_users/:idDELETE /api/v3/admin/admin_users/:idNo
POST /admin_users— admin users are created by accepting an invitation (/auth/invitations/:id/accept), not by direct creation. 6.0:Spree::LegacyAdminUser→Spree::AdminUser. Owns auth stack (no Devise). See6.0-platform-auth.md.
GET /api/v3/admin/roles — read-only (feeds role pickers on invitations + admin users)GET /api/v3/admin/roles/:idPOST /api/v3/admin/rolesPATCH /api/v3/admin/roles/:idDELETE /api/v3/admin/roles/:idCustom-role CRUD is deferred; roles are currently seeded/predefined and exposed read-only.
GET /api/v3/admin/invitationsGET /api/v3/admin/invitations/:idPOST /api/v3/admin/invitationsDELETE /api/v3/admin/invitations/:idPATCH /api/v3/admin/invitations/:id/resendPublic acceptance endpoints (
GET/POST /auth/invitations/:id/...) live under Authentication above.
GET /api/v3/admin/allowed_originsGET /api/v3/admin/allowed_origins/:idPOST /api/v3/admin/allowed_originsPATCH /api/v3/admin/allowed_origins/:idDELETE /api/v3/admin/allowed_origins/:idPOST /api/v3/admin/direct_uploads — pre-signed blob upload helper for media/import attachmentsGET /api/v3/admin/custom_field_definitionsGET /api/v3/admin/custom_field_definitions/:idPOST /api/v3/admin/custom_field_definitionsPATCH /api/v3/admin/custom_field_definitions/:idDELETE /api/v3/admin/custom_field_definitions/:id6.0:
display_on→storefront_visible(boolean).metafield_type→field_type. See5.4-6.0-custom-fields-rename.md.
Mounted via the custom_fieldable route concern — custom_fields is nested under each parent that includes Spree::Metafields, not a single flat /:resource_type/:resource_id/custom_fields route. Resolves the "Polymorphic custom fields routing" open question.
GET|POST /api/v3/admin/products/:product_id/custom_fields + PATCH|DELETE .../custom_fields/:id…/variants/:variant_id/custom_fields…/categories/:category_id/custom_fields…/option_types/:option_type_id/custom_fields…/customers/:customer_id/custom_fields…/orders/:order_id/custom_fieldsNew parents get custom-field endpoints for free by adding
concerns: :custom_fieldableto their route + includingSpree::Metafieldson the model.
GET /api/v3/admin/:resource_type/:resource_id/translationsPATCH /api/v3/admin/:resource_type/:resource_id/translationsGET /api/v3/admin/tags — autocomplete for product/order/customer tag inputsPOST /api/v3/admin/tagsDELETE /api/v3/admin/tags/:idTags are created/removed implicitly through their taggable resources (e.g.
products/bulk_add_tags, thetags: []param on a product). Standalone create/destroy is not exposed.
GET /api/v3/admin/refund_reasonsGET /api/v3/admin/refund_reasons/:idPOST /api/v3/admin/refund_reasonsPATCH /api/v3/admin/refund_reasons/:idDELETE /api/v3/admin/refund_reasons/:idGET /api/v3/admin/return_reasonsGET /api/v3/admin/return_reasons/:idPOST /api/v3/admin/return_reasonsPATCH /api/v3/admin/return_reasons/:idDELETE /api/v3/admin/return_reasons/:idGET /api/v3/admin/store_credit_categories — read-only (feeds the store-credit category dropdown)GET /api/v3/admin/store_credit_categories/:idPOST /api/v3/admin/store_credit_categoriesPATCH /api/v3/admin/store_credit_categories/:idDELETE /api/v3/admin/store_credit_categories/:idGET /api/v3/admin/webhook_endpointsGET /api/v3/admin/webhook_endpoints/:idPOST /api/v3/admin/webhook_endpointsPATCH /api/v3/admin/webhook_endpoints/:idDELETE /api/v3/admin/webhook_endpoints/:idPOST /api/v3/admin/webhook_endpoints/:id/send_test — fire a test deliveryPATCH /api/v3/admin/webhook_endpoints/:id/enable / disableGET /api/v3/admin/webhook_endpoints/:webhook_endpoint_id/deliveriesGET /api/v3/admin/webhook_endpoints/:webhook_endpoint_id/deliveries/:idPOST /api/v3/admin/webhook_endpoints/:webhook_endpoint_id/deliveries/:id/redeliverGET /api/v3/admin/api_keysGET /api/v3/admin/api_keys/:idGET /api/v3/admin/api_keys/current — the key the request authenticated withPOST /api/v3/admin/api_keysPATCH /api/v3/admin/api_keys/:idDELETE /api/v3/admin/api_keys/:idPATCH /api/v3/admin/api_keys/:id/revokeKeys carry Shopify-style
read_*/write_*scopes. See5.5-admin-api-key-scopes.md.
GET /api/v3/admin/integrationsGET /api/v3/admin/integrations/:idPOST /api/v3/admin/integrationsPATCH /api/v3/admin/integrations/:idDELETE /api/v3/admin/integrations/:id6.0: Integration records back the TaxProvider, DeliveryRateProvider, FulfillmentProvider, and PickupPointProvider interfaces (along with external search, email, etc.).
GET /api/v3/admin/reportsGET /api/v3/admin/reports/:idPOST /api/v3/admin/reports — generateGET /api/v3/admin/exportsGET /api/v3/admin/exports/:id — includes done, download_url, filename, byte_sizePOST /api/v3/admin/exports — type Spree::Exports::{Products,Orders,Customers} + optional search_params / record_selectionDELETE /api/v3/admin/exports/:idGET /api/v3/admin/exports/:id/download — redirect to the generated fileNo
PATCH— exports are immutable once created. See5.5-admin-spa-csv-export.md.
GET /api/v3/admin/importsGET /api/v3/admin/imports/:id — status, row countsPOST /api/v3/admin/imports — multipart/form-dataPATCH /api/v3/admin/imports/:id/complete_mappingGET /api/v3/admin/dashboard/analyticsbulk_status_update, bulk_add_to_categories, bulk_add_to_channels, bulk_add_tags, bulk_destroy, prices/bulk_upsert, customers/bulk_add_to_groups, …), bodies { ids: [...], ... }, shared via the Spree::Api::V3::BulkOperations concern. No single /bulk envelope.PATCH /orders/:id { customer_id } (5.x bridge also accepts user_id). No dedicated sub-resource.custom_fields mounted via the custom_fieldable route concern under each parent resource, not a flat /:resource_type/:resource_id/custom_fields route.5.4-centralized-translations-admin.md, 5.4-metafield-translations.md.?action= discriminator on one route per resource would read better once the full set is in.docs/api-reference/store-api/errors.mdx — error response conventions (shared)docs/plans/6.0-admin-spa.md — consumer of this APIdocs/plans/6.0-platform-auth.md — auth stack and Spree.admin_user_class renamedocs/plans/5.4-6.0-custom-fields-rename.md — Metafields → Custom Fieldsdocs/plans/6.0-fulfillment-and-delivery.md — shipping category dropped