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

# Request & Response Standards

> Conventions for field selection, related-entity expansion, pagination, filtering, sorting, metadata, dates, and error responses - across V2 and V3.

Snappy maintains two API versions in parallel: **V2** (legacy, fully supported) and **V3** (current, JSON:API-aligned). Most conventions on this page differ between the two - this page documents both, with V3 as the going-forward standard.

<Note>
  **Same API key, different conventions.** The same `X-Api-Key` authenticates both V2 and V3 - the version is in the URL path (`/v2/...` vs `/v3/...`). Pick the version that matches your endpoint; the conventions on this page follow the version of the endpoint you're calling.
</Note>

***

## Version Conventions at a Glance

| Convention                 | V2                               | V3                                                                                                       |
| :------------------------- | :------------------------------- | :------------------------------------------------------------------------------------------------------- |
| Related-entity hydration   | `expand[]=product` (array param) | `include=brand,tags` (JSON:API, comma-separated)                                                         |
| Pagination                 | `skip` / `limit` (offset)        | `page[number]` / `page[size]` (page-number) - or `page[cursor]` / `page[size]` on product list endpoints |
| Filtering                  | Bespoke per-endpoint             | `filter[field]` JSON:API style; ranges via `filter[field][gte]` / `[lte]`                                |
| Sorting                    | Bespoke per-endpoint             | `sort=field` (ascending) or `sort=-field` (descending); single field                                     |
| Field selection (`fields`) | Projection (same in V2 and V3)   | Projection (same in V2 and V3)                                                                           |
| Error response             | Flat object (same in V2 and V3)  | Flat object (same in V2 and V3)                                                                          |
| Dates                      | ISO 8601 UTC                     | ISO 8601 UTC                                                                                             |
| Metadata                   | Up to 50 key-value pairs         | Up to 50 key-value pairs                                                                                 |

## Field Selection

Snappy lets you control which fields a response includes via the `fields` query parameter on `GET` requests. This is particularly useful for mobile integrations and high-volume data processing where bandwidth and parsing speed matter. **The `fields` parameter works identically in V2 and V3.**

| Value                                                        | Behavior                                                                         |
| :----------------------------------------------------------- | :------------------------------------------------------------------------------- |
| Comma-separated list (e.g. `fields=id,name,status`)          | Return only the listed fields                                                    |
| `full`                                                       | Return every available field, bypassing defaults                                 |
| omitted                                                      | Return a predefined "common" subset for the endpoint (typically `id` and `name`) |
| **Example.** Fetching campaigns to populate a dropdown menu: |                                                                                  |

```text theme={null}
GET /v2/campaigns?fields=id,name
```

```json theme={null}
{
  "id": "cmp_12345",
  "name": "Holiday 2024 Wellness"
}
```

### Constraints

<Note>
  * **Parent object only.** `fields` applies to the primary object in the response - you can't project fields on nested objects via this parameter.
  * **No spaces.** Use commas only (`id,name,type`), not `id, name, type`.
  * **Case-sensitive.** Field names must match the casing in the API reference exactly.
  * **Defaults vary by endpoint.** Each endpoint documents its own default field set - check the endpoint reference if you're seeing fewer fields than expected.
</Note>

## Related-Entity Hydration

To reduce round-trips, both V2 and V3 let you hydrate related entities inline - replacing a foreign-key reference with the full related object. The parameter name and syntax differ.

### V3 - `include` (JSON:API)

In V3, the `include` query parameter takes a **comma-separated list** of related-entity names. Hydrated entities appear inline within their parent.
**Example:**

```text theme={null}
GET /v3/products/prd_98765?include=brand,tags
```

The `brand` field is replaced by the full Brand object, and `tags` returns the full Tag objects rather than just IDs.
Common V3 expandable entities:

* **Products / Collections** - `brand`, `tags`
* **Orders** - *(no V3 includes; responses already carry full line items, recipient, and fulfillments)*
  The API reference for each endpoint lists supported `include` values.

### V2 - `expand[]`

In V2, the `expand[]` query parameter takes an **array** of expandable entity names. Each ID-based reference becomes the full object.
**Default V2 response:**

```json theme={null}
{
  "id": "gft_abcd123",
  "product_id": "prd_98765",
  "status": "sent"
}
```

**Request with expansion:**

```text theme={null}
GET /v2/gifts/gft_abcd123?expand[]=product
```

**Expanded response:**

```json theme={null}
{
  "id": "gft_abcd123",
  "product": {
    "id": "prd_98765",
    "name": "Luxury Wellness Set",
    "description": "A curated set of wellness items...",
    "value": 50.00
  },
  "status": "sent"
}
```

Common V2 expandable entities: `product`, `recipient`, `campaign`.

### Best practices (both versions)

* Only hydrate what your client actually needs - every expansion adds latency and payload weight.
* Expansion is especially valuable for mobile clients where round-trip count is the bottleneck.
* In V2 you can combine `expand[]` with `fields` projection; in V3 you can combine `include` with additive `fields`.

***

## Pagination

V2 uses offset-based pagination; V3 uses JSON:API page-number pagination (with cursor pagination on a small number of high-volume endpoints).

### V3 - `page[number]` / `page[size]` (page-number, default)

Most V3 list endpoints use 1-indexed page-number pagination.

| Parameter                                                                                                                                                                                                                                                                                                                | Description           | Default | Max                                         |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------- | :------ | :------------------------------------------ |
| `page[number]`                                                                                                                                                                                                                                                                                                           | 1-indexed page number | `1`     | -                                           |
| `page[size]`                                                                                                                                                                                                                                                                                                             | Items per page        | `100`   | varies per endpoint (typically `150`–`300`) |
| The response includes a top-level `links` object with `first`, `next`, and `prev` URLs - all required, all nullable. Use `links.next` verbatim to fetch the next page rather than constructing URLs manually. Many list endpoints also include `meta.total` with the count of items matching the query across all pages. |                       |         |                                             |
| **Example response shape:**                                                                                                                                                                                                                                                                                              |                       |         |                                             |

```json theme={null}
{
  "data": [ /* items */ ],
  "links": {
    "first": "/v3/collections?page[number]=1&page[size]=100",
    "next":  "/v3/collections?page[number]=2&page[size]=100",
    "prev":  null
  },
  "meta": {
    "total": 247
  }
}
```

### V3 - `page[cursor]` / `page[size]` (cursor, product list endpoints)

A small number of high-volume V3 endpoints - currently `GET /v3/products`, `GET /v3/collections/{collectionId}/products`, and `GET /v3/products/{productId}/variants` - use **cursor pagination** instead.

| Parameter                                                                                                                                                                                                                                      | Description                              | Default               | Max   |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- | :-------------------- | :---- |
| `page[cursor]`                                                                                                                                                                                                                                 | Opaque cursor returned from a prior page | (omit for first page) | -     |
| `page[size]`                                                                                                                                                                                                                                   | Items per page                           | `100`                 | `300` |
| The `links` object structure is the same as page-number, except `links.prev` is always `null` - backward navigation is not supported on cursor-paginated endpoints. Use `links.next` verbatim; **do not parse or construct cursors manually**. |                                          |                       |       |

<Tip>
  **Why both styles?** Cursor pagination is more efficient and stable on high-volume, append-heavy catalogs (where new products keep arriving). Page-number is more intuitive for finite collections like Orders or Accounts. The endpoint documentation always specifies which style applies.
</Tip>

### V2 - `skip` / `limit` (offset)

V2 list endpoints use offset-based pagination.

| Parameter                                                              | Description                              | Default | Max                                           |
| :--------------------------------------------------------------------- | :--------------------------------------- | :------ | :-------------------------------------------- |
| `limit`                                                                | Items per page                           | `100`   | `100` (varies per endpoint, some up to `200`) |
| `skip`                                                                 | Number of items to offset from the start | `0`     | -                                             |
| The formula for any page is `skip = (page_number - 1) * limit`.        |                                          |         |                                               |
| **Identifying the end of data.** Continue fetching pages until one of: |                                          |         |                                               |

1. The number of items in `results` is **less than** `limit`.
2. `results` is **empty**.

***

## Filtering (V3)

V3 standardizes filtering with **JSON:API-style** `filter[field]` query parameters. Range filters use nested brackets: `filter[field][gte]` (inclusive lower bound) and `filter[field][lte]` (inclusive upper bound).

```text theme={null}
GET /v3/products?filter[catalog]=marketplace&filter[price][gte]=25&filter[price][lte]=200
```

Common patterns across V3:

* **Exact match** - `filter[status]=active`
* **Multi-value** - `filter[idempotencyKey]=key_a,key_b,key_c` (comma-separated)
* **Range** - `filter[price][gte]=25&filter[price][lte]=200`
* **Free-text search** - `filter[search]=cold+brew`
  V2 endpoints filter too, but the parameter names and shapes are bespoke per endpoint - check the endpoint documentation.

***

## Sorting (V3)

V3 standardizes sorting with a single `sort` query parameter. Prefix the field with `-` for descending order. Single field per request.

```text theme={null}
GET /v3/orders?sort=-createdAt      # newest first (default for orders)
GET /v3/products?sort=minPrice      # cheapest first
GET /v3/collections?sort=rank       # curated display order
```

Each endpoint documents its supported sort fields and default sort order.
V2 endpoints sort too, but the parameter is bespoke per endpoint.

***

## Metadata

Metadata lets you attach custom key-value pairs to Snappy resources (Gifts, Orders, etc.). Metadata round-trips: any pairs you provide at creation appear in subsequent `GET` responses and in webhook payloads.
**Supported on both V2 and V3.**

| Attribute            | Constraint                                                           |
| :------------------- | :------------------------------------------------------------------- |
| Max key-value pairs  | 50 per object                                                        |
| Key                  | Alphanumeric (plus `-`, `_`, `.`), max 40 characters                 |
| Value                | Alphanumeric (plus whitespace and `-`, `_`, `.`), max 500 characters |
| **Example payload:** |                                                                      |

```json theme={null}
{
  "campaignId": "cmp_12345",
  "recipient": { "email": "developer@example.com" },
  "metadata": {
    "internal_employee_id": "EMP-9982",
    "salesforce_opp_id": "0061a00000abc123",
    "cost_center": "Marketing_Q4"
  }
}
```

***

## Date Formats

Both V2 and V3 use **ISO 8601** for all date and time fields. All timestamps are returned in **UTC**.

* **Format:** `YYYY-MM-DDTHH:mm:ss.sssZ`
* **Example:** `2026-04-13T12:00:00.000Z`

***

## Error Responses

When a request can't be processed, Snappy returns a flat error object alongside the relevant HTTP status code. **The error shape is the same in V2 and V3.** The structure differs slightly between validation errors (HTTP `400`) and all other errors.
**Validation errors (HTTP 400):**

```json theme={null}
{
  "path": "campaignId",
  "errorCode": "INVALID_REQUEST",
  "message": "The campaignId provided does not exist."
}
```

**All other errors (401, 403, 404, 409, 422, 5xx):**

```json theme={null}
{
  "status": 404,
  "errorCode": "NOT_FOUND",
  "message": "The resource you requested could not be found."
}
```

| Field                                                                                                                       | Type   | Description                                                                                                                                                 |
| :-------------------------------------------------------------------------------------------------------------------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`                                                                                                                      | string | *(400 only)* Dot-separated path to the field that caused the validation error.                                                                              |
| `status`                                                                                                                    | number | *(non-400 only)* The HTTP status code.                                                                                                                      |
| `errorCode`                                                                                                                 | string | Granular error identifier. V3 uses the structured format `{STATUS}_{DOMAIN}_{SEQUENCE}` (e.g., `404_PROD_001`); V2 uses symbolic codes (e.g., `NOT_FOUND`). |
| `message`                                                                                                                   | string | Human-readable description. Don't switch on this - switch on `errorCode`.                                                                                   |
| For per-status-code recovery guidance, retry strategy, and idempotency patterns, see **[Error Handling](/error-handling)**. |        |                                                                                                                                                             |
