Skip to main content
Before you start customizing Spree API endpoints, make sure you reviewed all existing API endpoints in the Spree API docs. For a step-by-step walkthrough of adding a complete new resource (model, serializer, controller, routes), see the API tutorial.

Customizing JSON Responses

Spree uses Alba serializers to build API v3 responses. You can replace any serializer via Spree Dependencies.

Adding Custom Attributes

Let’s say you want to add a my_custom_attribute column to the Product API response. Create a custom serializer that inherits from the core one:
app/serializers/my_app/product_serializer.rb
Register it in config/initializers/spree.rb:
Restart the server and the Product API will include your new attribute.

Adding an Association

Let’s say you’ve created a Spree::Brand model that belongs to Product (see the tutorial for the full example). Create a serializer for the new model:
app/serializers/spree/api/v3/brand_serializer.rb
Then subclass the Product serializer to include the brand association:
app/serializers/my_app/product_serializer.rb
Register it in config/initializers/spree.rb:
The brand data is available via ?expand=brand:

Adding a Field from an Extension

Replacing a serializer works for your own application, but the setting holds one replacement. An extension that replaced admin_product_serializer would be silently undone by an application that replaces it too. An extension adds read-only fields to a core serializer instead: an attribute, written as it is, or an association, a related record or a list of them written by the extension’s own serializer:
server/config/initializers/my_extension.rb
  • The name is the response key and the record’s method that gives the value: method: for an attribute, one: or many: for an association. one: :warranty writes product.warranty with the given serializer; many: :warranty_claims writes each of product.warranty_claims. An extension adds such methods to core models with model extensions.
  • A simple computed value needs no method: name the field and pass value:, as in attribute(:admin_product_serializer, :warranty_months, value: ->(product) { ... }). An association takes value: the same way, beside its one: or many:.
  • Registered by dependency key, such as admin_product_serializer. If the application replaces that serializer with one that inherits from a core serializer, as shown above, the field still renders on the replacement.
  • expand: true shows the field only when the request asks for it with ?expand=warranty. Without it, the field is always shown. An association’s nested expands reach its serializer, so ?expand=warranty.claims passes claims on, the same way ?expand=variants.media works. Lists apply the same expands to every item.
  • Associations read on lists are batched when they are Active Record associations, as core ones are. A value computed by value: is read for each record.
  • Names are unique per serializer. Registering a name that the serializer or another extension already uses raises an error at boot.
  • Not in the generated SDK types. The extension that adds a field declares its TypeScript type.
This is the read side of adding an attribute to a core resource, which makes a field writable.

Serializer Dependency Keys

Here are the most commonly customized v3 serializers: See Spree::Api::ApiDependencies for the full list.

Adding New Endpoints

Controller

Inherit from Spree::Api::V3::Store::ResourceController for Store API endpoints:
app/controllers/spree/api/v3/store/brands_controller.rb
The base ResourceController provides: Override these methods to customize:

Routes

Add routes in your app’s config/routes.rb:

Request Parameters

For endpoints that accept writes (create/update), declare what the body takes with request_params, at the top of the controller. Each attribute names its type, the way a model attribute or a preference does:
app/controllers/spree/api/v3/admin/brands_controller.rb
This one declaration is the endpoint’s contract: the controller permits exactly these keys and nesting, and nothing else. Spree’s own endpoints publish the same declarations in the API reference, and the SDK request types (ProductCreateParams, ProductUpdateParams, …) are generated from it, so the controller, the reference and the TypeScript types cannot drift apart. A custom action declares its own body with a name ending in Params: request_params 'BrandMergeParams', action: :merge do |body| ... end. A shape several endpoints accept, such as an address or a price, is a class in app/request_schemas, declared the way a model declares its attributes. A subclass starts from its parent’s attributes:
app/request_schemas/spree/api/v3/brand_logo_request_schema.rb
Endpoints then reference it, body.attribute :logo, :object, shape: Spree::Api::V3::BrandLogoRequestSchema, or take its attributes into their own body with body.include_attributes Spree::Api::V3::BrandLogoRequestSchema. Without publishes the class is a set of attributes other declarations include, not a type of its own.
An endpoint that needs logic of its own can still override resource_permitted_attributes with a plain params.permit list, but then its body is neither documented nor typed. Never override permitted_attributes: it is what appends extension-contributed attributes to your list, so overriding it replaces them.

Adding an Attribute to a Core Resource

To make a new column writable on a Spree model without decorating the controllers that write it, append to the model’s permitted attributes:
config/initializers/spree.rb
Standard resource endpoints append these to their own allowlist, so one declaration covers the model’s create and update endpoints. Use +=, never = — assigning replaces what another extension added — and declare only attributes of your own, since redeclaring a key the controller already permits replaces its filter rather than widening it.
For merchant-managed data, prefer Custom Fields, which need no code and are filterable and sortable. This is for extensions that add real database columns.

Writable by sellers

The Seller API accepts a narrower set of attributes than the Admin API, so the list above does not apply to it — an attribute you open to operators stays operator-only. To let sellers write it too, for example from a field on the seller panel’s product page, declare it again on the seller list:
config/initializers/spree.rb
Only declare attributes a seller may safely set on their own records. Two endpoints deliberately ignore this hook because their parameters are authorization data rather than resource data: API keys and invitations.

Consuming Custom Endpoints with the SDK

See the SDK tutorial for how to call custom endpoints from TypeScript and extend the generated types.