> ## Documentation Index
> Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Metadata

## Overview

Metadata provides simple, unstructured key-value storage on Spree resources — similar to [Stripe's metadata](https://docs.stripe.com/api/metadata). It's ideal for storing integration IDs, tracking data, or any arbitrary information that doesn't need validation or admin UI.

Metadata is a **permanent, first-class system** in Spree. It is designed to coexist alongside [Metafields](/developer/core-concepts/metafields) (structured, typed, admin-managed). The two systems serve different purposes and are not interchangeable — think of it as **metadata for machines, metafields for humans**.

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.

<Info>
  For structured, type-safe custom attributes with admin UI support, use [Metafields](/developer/core-concepts/metafields) instead.
</Info>

## Store API

### Cart creation

Set metadata when creating a new cart:

```bash theme={"theme":"night-owl"}
POST /api/v3/store/carts
Content-Type: application/json

{
  "metadata": {
    "source": "mobile_app",
    "campaign": "summer_sale"
  }
}
```

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

```

### Adding items

Set metadata when adding items to the cart:

```bash theme={"theme":"night-owl"}
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."
  }
}
```

```typescript theme={"theme":"night-owl"}
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.

```bash theme={"theme":"night-owl"}
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:

```typescript theme={"theme":"night-owl"}
// 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

```bash theme={"theme":"night-owl"}
PATCH /api/v3/store/carts/:id
Content-Type: application/json

{
  "metadata": {
    "utm_source": "google",
    "utm_campaign": "summer_sale"
  }
}
```

```typescript theme={"theme":"night-owl"}
await client.carts.update(cartId, {
  metadata: { utm_source: 'google' },
}, { spreeToken })
```

## Admin API

Metadata is **readable** in Admin API responses on orders and line items:

```json theme={"theme":"night-owl"}
{
  "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. Always use `metadata` in Ruby code — do not call `public_metadata=` or `private_metadata=` directly. (SQL queries still reference the underlying `private_metadata` column name — see querying examples below.)

```ruby theme={"theme":"night-owl"}
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

```ruby theme={"theme":"night-owl"}
# 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](https://docs.stripe.com/api/metadata).

```
# 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

Spree has two permanent, complementary systems for custom data. They are not interchangeable and neither is going away.

|                 | Metadata                                                            | [Metafields](/developer/core-concepts/metafields)      |
| --------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
| **Purpose**     | Developer escape hatch — integration data, sync state, ad-hoc flags | Merchant-defined structured attributes with admin UI   |
| **Schema**      | Schemaless JSON — no definition required                            | Defined via MetafieldDefinitions (typed, validated)    |
| **Validation**  | None — accepts any JSON-serializable data                           | Type-specific (text, number, boolean, rich text, JSON) |
| **Visibility**  | Write-only in Store API, readable in Admin API                      | Configurable (front-end, back-end, both)               |
| **Admin UI**    | JSON preview only                                                   | Dedicated management forms                             |
| **API pattern** | Stripe-style: `metadata: { key: value }`                            | Expand-based: `?expand=custom_fields`                  |
| **Queryable**   | Via JSONB operators (PostgreSQL)                                    | Via SQL joins, Ransack scopes, search providers        |

### 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
* Syncing state with external integrations (webhooks, ETL pipelines)

### When to use metafields

* Custom product specifications shown to customers (material, dimensions, certifications)
* Admin-managed fields with validation and type safety
* Data that needs to appear in the admin UI with dedicated form inputs
* Querying/filtering by custom attributes (search facets, product filtering)
* CSV import/export of structured product data

## 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`)

## Deprecation: public\_metadata

<Warning>
  `public_metadata` is deprecated and will be removed in Spree 6.0. Use `metadata` instead.
</Warning>

The `public_metadata` column was never exposed in Store API responses and in practice served the same purpose as `private_metadata`. It will be removed in Spree 6.0 and calling `public_metadata=` will emit a deprecation warning.

Always use the single `metadata` accessor for all schemaless key-value storage. If you need data visible to customers on the storefront, use [Metafields](/developer/core-concepts/metafields) instead.

```ruby theme={"theme":"night-owl"}
# Deprecated — will be removed in 6.0
order.public_metadata = { 'gift_message' => 'Happy Birthday!' }

# Use metadata for internal storage
order.metadata = { 'gift_message' => 'Happy Birthday!' }

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