Skip to main content

Overview

Metadata provides simple, unstructured key-value storage on Spree resources — similar to Stripe’s metadata. It’s ideal for storing integration IDs, tracking data, or any arbitrary information that doesn’t need validation or admin UI. Metadata is write-only in the Store API — you can set it when creating or updating resources, but it is never returned in Store API responses. It is visible in Admin API responses for administrative use.
For structured, type-safe custom attributes with admin UI support, use Metafields instead.

Store API

Cart creation

Set metadata when creating a new cart:
POST /api/v3/store/carts
Content-Type: application/json

{
  "metadata": {
    "source": "mobile_app",
    "campaign": "summer_sale"
  }
}
// @spree/sdk
const cart = await client.carts.create({
  metadata: { source: 'mobile_app', campaign: 'summer_sale' }
})

// @spree/next
const cart = await getOrCreateCart({ source: 'mobile_app' })

Adding items

Set metadata when adding items to the cart:
POST /api/v3/store/carts/:cart_id/items
Content-Type: application/json

{
  "variant_id": "variant_k5nR8xLq",
  "quantity": 1,
  "metadata": {
    "gift_note": "Happy Birthday!",
    "engraving": "J.D."
  }
}
const order = await client.carts.items.create(cartId, {
  variant_id: 'variant_k5nR8xLq',
  quantity: 1,
  metadata: { gift_note: 'Happy Birthday!' },
})

Updating items

Metadata is merged with existing values on update. Set a key to null to remove it.
PATCH /api/v3/store/carts/:cart_id/items/:id
Content-Type: application/json

{
  "metadata": {
    "engraving": "A.B.",
    "gift_note": null
  }
}
You can update metadata without changing quantity, or update both at once:
// Metadata only
await client.carts.items.update(cartId, lineItemId, {
  metadata: { engraving: 'A.B.' },
})

// Both quantity and metadata
await client.carts.items.update(cartId, lineItemId, {
  quantity: 3,
  metadata: { gift_note: 'Happy Birthday!' },
})

Updating carts

PATCH /api/v3/store/carts/:id
Content-Type: application/json

{
  "metadata": {
    "utm_source": "google",
    "utm_campaign": "summer_sale"
  }
}
await client.carts.update(cartId, {
  metadata: { utm_source: 'google' },
}, { spreeToken })

Admin API

Metadata is readable in Admin API responses on orders and line items:
{
  "id": "or_m3Rp9wXz",
  "number": "R123456",
  "metadata": {
    "source": "mobile_app",
    "utm_campaign": "summer_sale"
  },
  "items": [
    {
      "id": "li_x8Kp2qWz",
      "metadata": {
        "gift_note": "Happy Birthday!"
      }
    }
  ]
}
When there is no metadata, the field is null.

Ruby / Backend

Reading and writing

Every model that includes Spree::Metadata has a metadata accessor (alias for private_metadata):
order = Spree::Order.find_by!(number: 'R123456')

# Write
order.metadata = { 'source' => 'mobile_app' }
order.save!

# Read
order.metadata['source'] # => "mobile_app"

# Merge
order.metadata = order.metadata.merge('campaign' => 'summer')
order.save!

Querying

# Find orders with specific metadata value (PostgreSQL)
Spree::Order.where("private_metadata->>'source' = ?", "mobile_app")

# Check for key existence
Spree::Order.where("private_metadata ? 'source'")

Merge semantics

Metadata updates use merge semantics — existing keys are preserved, new keys are added, and keys set to null are removed. This matches Stripe’s behavior.
# Initial metadata
{ "source": "mobile_app", "campaign": "summer" }

# Update with
{ "campaign": "winter", "new_key": "value" }

# Result
{ "source": "mobile_app", "campaign": "winter", "new_key": "value" }

Metadata vs Metafields

MetadataMetafields
Use caseIntegration data, tracking, simple key-valueStructured custom attributes with admin UI
ValidationNoneType-specific (text, number, boolean, JSON)
VisibilityWrite-only in Store API, readable in Admin APIConfigurable (front-end, back-end, both)
Admin UIViewable in JSON previewFull admin management
SchemaSchemaless JSONDefined via MetafieldDefinitions
API patternStripe-style flat key-valueShopify-style typed resources

When to use metadata

  • Storing external system IDs (e.g., Stripe payment intent ID, ERP order ID)
  • Tracking attribution data (UTM parameters, referral source)
  • Passing context from the storefront that doesn’t need validation
  • Any write-and-forget data that only needs to be read by backend systems

When to use metafields

  • Custom product specifications shown to customers
  • Admin-managed fields with validation
  • Data that needs to appear in the admin UI
  • Querying/filtering by custom attributes

Supported resources

All models that include the Spree::Metadata concern support metadata. This includes all core models: Orders, Line Items, Products, Variants, Taxons, Payments, Shipments, and more. The Store API currently supports writing metadata on:
  • Carts — on creation and update (POST /api/v3/store/carts, PATCH /api/v3/store/carts/:id)
  • Items — on create and update (POST/PATCH /api/v3/store/carts/:id/items)

Migration from public_metadata

The public_metadata column is deprecated. Metadata is no longer returned in Store API responses. If you were using public_metadata for data that needs to be visible to customers, migrate to Metafields with display_on: 'both'.
# Before (deprecated)
order.public_metadata = { 'gift_message' => 'Happy Birthday!' }

# After — use metadata for write-only storage
order.metadata = { 'gift_message' => 'Happy Birthday!' }

# After — use metafields for customer-visible structured data
order.set_metafield('custom.gift_message', 'Happy Birthday!')