]` style.
#### Permissions
All Base Products endpoints require the `products:read` scope on your API key.
***
## How to Work with Swag
**List base products**
```text theme={null} theme={null}
GET /v3/base-products
```
Returns a paginated list of Base Products. Filter by brand using the `brandIds` query parameter.
**Get a single base product**
```text theme={null} theme={null}
GET /v3/base-products/{baseProductId}
```
**List base variants for a base product**
```text theme={null} theme={null}
GET /v3/base-products/{baseProductId}/variants
```
# Get billing method by ID
Source: https://docs.snappy.com/modules/api/v3/billing-methods/get-billing-method-by-id
get /v3/billing-methods/{billingMethodId}
Use this endpoint to retrieve a single billing method by its identifier, scoped to the account specified in the `Snappy-Account-Id` header. Use this when you want to confirm a billing method is usable through the public API, fetch its remaining balance, or read its status and metadata before placing marketplace orders or triggering gifts.
**Required parameters**
- `billingMethodId` - the billing method identifier, passed as a path parameter.
**Required headers**
- `Snappy-Account-Id` - account scope.
**Optional headers**
- `Snappy-Company-Id` - further scope to a specific Company.
**Please note**
- Returns `404` if no billing method exists for the supplied `billingMethodId`, or if it belongs to a different account or company than the one in scope.
- Returns `422` for billing methods that exist but are not supported through the public API (e.g. `Express`). The error envelope identifies the unsupported type.
- The response includes a `spendingLimit` object with a `remaining` balance in **USD dollars** (e.g. `7250.5` = $7,250.50), or `null` when the billing method has no cap.
- `expirationDate` is `null` when no expiry is configured.
#### Permissions
- Requires: `billingMethods:read`
# Get billing methods
Source: https://docs.snappy.com/modules/api/v3/billing-methods/get-billing-methods
get /v3/billing-methods
Use this endpoint to retrieve the billing methods of an account that are usable through the public API. Use this when you need to discover which billing methods are available - and how much budget remains on each - before placing marketplace orders or triggering gifts.
**Filtering options**
- `filter[type]` - exact match on billing method type. One of `Prepay`, `Invoice`, `PO`, `CC`.
- `filter[status]` - exact match on billing method status. Allowed values depend on the type:
- `Invoice`: `active`, `archived`
- `CC`: `active`, `archived`, `expired`
- `PO`: `draft`, `active`, `archived`, `expired`
- `Prepay`: `draft`, `active`, `archived`
- `filter[remainingBalance][gte]` - return billing methods with a `spendingLimit.remaining` **greater than or equal to** this value. Value is in **USD dollars** and must be greater than 0 (e.g. `1000` = $1,000).
- `filter[remainingBalance][lte]` - return billing methods with a `spendingLimit.remaining` **less than or equal to** this value. Value is in **USD dollars** and must be greater than 0 (e.g. `5000` = $5,000).
**Required headers**
- `Snappy-Account-Id` - account scope.
**Optional headers**
- `Snappy-Company-Id` - further scope to a specific Company.
**Please note**
- `Express` billing methods are intentionally excluded from this endpoint. They exist on some accounts but cannot be used through the public API.
- Each billing method's response includes a `spendingLimit` object with a `remaining` balance in **USD dollars**, or `null` when the billing method has no cap (e.g. `Invoice`).
- `expirationDate` is `null` when no expiry is configured on the billing method.
- `filter[remainingBalance]` filters apply only to billing methods that have a `spendingLimit`. `Invoice` billing methods (which have `spendingLimit: null`) are excluded when either range bound is supplied.
#### Permissions
- Requires: `billingMethods:read`
# Billing Methods API: Account Funding Sources and Remaining Balances
Source: https://docs.snappy.com/modules/api/v3/billing-methods/overview
Discover which billing methods are available on an account and how much budget remains on each - before placing marketplace orders or triggering gifts.
A **Billing Method** is how an Account pays for marketplace orders, triggered gifts, and associated fees. Each Account can have one or more Billing Methods, with one set as the default - applied automatically to Campaigns created via the API (Triggered Gifting) and available for reference on marketplace orders (Embedded Marketplace).
The Billing Methods API exposes an Account's billing methods, so you can discover which are available through the public API, check remaining budget, and confirm a billing method is valid before placing marketplace orders or triggering gifts.
Want to understand how **Billing Methods** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page.
Billing Methods are referenced in V3 API requests by the `fundingSourceId` field (for example, on `POST /orders`).
***
## The Billing Method Object
| Field | Type | Description |
| :--------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Unique identifier of the billing method (e.g. `6a2515d0982480c2a4fd6b81`). |
| `name` | string | Display name assigned to the billing method in the Snappy dashboard. |
| `type` | enum | Billing method type. One of: `Prepay`, `Invoice`, `PO`, `CC`. |
| `status` | enum | Current status. One of: `draft`, `active`, `archived`, `expired` (allowed values depend on the type - see Key Concepts below). |
| `createdAt` | string (ISO 8601) | Timestamp when the billing method was created. |
| `expirationDate` | string (ISO 8601) | Timestamp when the billing method expires. `null` when no expiry is configured. |
| `spendingLimit` | object | Spending limit details, with a `remaining` balance field in USD dollars. `null` when the billing method has no cap (e.g. `Invoice`). |
***
## Key Concepts & Business Rules
#### Billing Method types
Snappy supports four billing method types through the public API:
| Type | Description |
| :-------- | :----------------------------------------------------------------------------------------------------- |
| `Prepay` | Pre-funded balance - the Account loads funds upfront and they're drawn down as gifts are sent. |
| `Invoice` | Net-terms billing - gifts are sent and Snappy invoices the Account periodically. No spending cap. |
| `PO` | Purchase order - a fixed-budget allocation typically tied to a department, initiative, or time period. |
| `CC` | Credit card on file - direct charges per gift. |
**`Express`** billing methods exist for some accounts but are **intentionally excluded** from the public API. They will not appear in `GET /v3/billing-methods` responses. `GET /v3/billing-methods/{billingMethodId}` returns `422` when called against an Express method or when the target Account is inactive.
#### Status values vary by type
Allowed `status` values depend on the billing method type:
| Type | Allowed statuses |
| :-------- | :--------------------------------------- |
| `Invoice` | `active`, `archived` |
| `CC` | `active`, `archived`, `expired` |
| `PO` | `draft`, `active`, `archived`, `expired` |
| `Prepay` | `draft`, `active`, `archived` |
Only **`active`** billing methods can be used to place marketplace orders or trigger gifts. `draft` methods are configured but not yet ready; `archived` methods have been retired; `expired` methods have passed their expiration date.
#### Remaining balance
The `spendingLimit.remaining` field reports the current balance available on the billing method, **in USD dollars** (e.g. `7250.5` = \$7,250.50). For `Invoice` billing methods, `spendingLimit` is `null` because there's no spending cap.
Always check remaining balance before placing high-value marketplace orders or triggering high-value gifts - Snappy rejects order and gift creation when the selected billing method has insufficient funds.
#### Default Billing Method
Each Account has one Billing Method set as the default. This default is applied automatically to any Campaign created via the API. For Embedded Marketplace (`POST /orders`), the billing method is referenced via the `fundingSourceId` field, and a matching Campaign is auto-selected or created based on that funding source.
#### Account scoping
Billing Methods are scoped to an Account. All Billing Methods endpoints **require** the `Snappy-Account-Id` header to identify which Account's billing methods to query.
#### Permissions
All Billing Methods endpoints require the `billingMethods:read` scope on your API key.
***
## How to Work with Billing Methods
**List billing methods**
```text theme={null}
GET /v3/billing-methods
```
Returns the billing methods on the Account that are usable through the public API. Filter by `type`, `status`, or `remainingBalance` range.
**Get a single billing method**
```text theme={null}
GET /v3/billing-methods/{billingMethodId}
```
Returns a single billing method by its ID. Returns `422` when called against an unsupported billing method type (e.g. `Express`) or an inactive Account.
***
Billing Methods are currently **read-only** through the public API. To create, update, or archive Billing Methods, use the Snappy Dashboard.
# Get collection by ID
Source: https://docs.snappy.com/modules/api/v3/collections/get-collection-by-id
get /v3/collections/{collectionId}
Use this endpoint to retrieve a single Collection's full metadata - name, description, cover image, thumbnails, rank, and provenance. Use this when rendering a Collection detail page before drilling into its Products via `GET /v3/collections/{collectionId}/products`.
###### Required path parameters
- `collectionId` - the Collection identifier.
###### Optional query parameters
- `filter[location]` - comma-separated ISO 3166-1 alpha-2 country codes. Thumbnails and `priceRange` use the first location. Defaults to `US`.
- `filter[maxPrice]` - budget bucket selector that controls which thumbnail set is returned.
- `fields` - comma-separated list of optional fields to include. One or more of `priceRange`, `updatedBy`, `createdVia`.
###### Required headers
- `Snappy-Account-Id` - account scope.
###### Optional headers
- `Snappy-Company-Id` - further scope to a specific Company within the Account.
###### Please note
- Always returns `id`, `name`, `description`, `tags`, `media`, `coverImage`, `rank`, `createdBy`, `createdAt`, and `updatedAt`. The `fields` parameter *adds* the listed optional fields on top of these defaults.
- Returns `404` (`404_PBLC_004`) if the Collection is not found **or** is not visible to the calling Account. The two cases are intentionally indistinguishable - visibility is treated as identical to non-existence to avoid leaking other Accounts' Collections.
#### Permissions
- Requires: `collections:read`
# Get collection products
Source: https://docs.snappy.com/modules/api/v3/collections/get-collection-products
get /v3/collections/{collectionId}/products
Use this endpoint to retrieve a paginated list of products within a specific collection. Designed for marketplace and browse experiences where you need a lightweight product list.
#### Filtering options
- Catalog (`marketplace`, `swag`, `giftCards`, or `donations`; required, defaults to `marketplace`)
#### Please note
Variants are not returned on this endpoint. Use `GET /v3/products/{productId}/variants` to retrieve the variants list for a specific product.
#### Permissions
- Requires: `products:read`
# Get collections
Source: https://docs.snappy.com/modules/api/v3/collections/get-collections
get /v3/collections
Use this endpoint to retrieve a paginated list of Collections available to the calling Account. Use this when you want to render a collection browse experience - a homepage tile grid, a budget-filtered category page, or a swag store landing page.
####### Filtering options
- `filter[location]` - comma-separated ISO 3166-1 alpha-2 country codes (e.g. `US,CA`). Only Collections supporting all requested locations are returned. Thumbnails and `priceRange` use the first location in the list. Defaults to `US`.
- `filter[maxPrice]` - budget bucket selector that controls which thumbnail set is returned.
- `filter[tag]` - comma-separated collection tags. One or more of `gifts`, `swag`, `custom`.
- `filter[search]` - free-text search on collection name (max 100 characters).
####### Pagination and sorting
- `page[number]` - 1-indexed page number (default `1`).
- `page[size]` - number of collections per page (max `150`, default `100`).
- `sort` - `rank` (default - curated display order), `name`, `-name`, `createdAt`, or `-createdAt`.
###### Field expansion
- `fields` - comma-separated list of optional fields to include. One or more of `priceRange`, `description`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, `createdVia`.
###### Required headers
- `Snappy-Account-Id` - account scope.
###### Optional headers
- `Snappy-Company-Id` - further scope to a specific Company within the Account.
###### Please note
- Uses page-number pagination, unlike `GET /v3/collections/{collectionId}/products` which uses cursor pagination. The response includes a top-level `links` object (`first`, `next`, `prev`).
- Returns Collections by curated `rank` by default - lower rank values surface first.
- Returns `404` if the supplied `Snappy-Account-Id` does not match a known Account.
#### Permissions
- Requires: `collections:read`
# Collections API: Curated Gift Catalogs
Source: https://docs.snappy.com/modules/api/v3/collections/overview
Retrieve products within a curated collection for marketplace and browse experiences. Filter by catalog, price, type, and search - with cursor pagination and JSON:API conventions.
A **Collection** is a curated catalog of gift items tailored to a specific theme, budget range, or audience (e.g. "Birthday Gifts Under \$50"). Use Collections to give recipients a focused, branded gifting experience - or to power a marketplace browse flow inside your own platform.
The V3 Collections API lets you **list Collections**, **retrieve a single Collection's metadata**, and **retrieve the Products within a specific Collection** - all using the JSON:API filtering, pagination, and field-expansion conventions used elsewhere in V3.
Want to understand how **Collections** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page.
V3 exposes three Collections endpoints: list Collections (`GET /v3/collections`), retrieve a single Collection (`GET /v3/collections/{collectionId}`), and retrieve the Products within a Collection (`GET /v3/collections/{collectionId}/products`). The list and by-ID endpoints **require** the `Snappy-Account-Id` header. A few endpoints - fetching budgets, counting products, and retrieving a single Product within a Collection - remain on V2 (see [V2 Endpoints](#v2-endpoints-legacy) below) and will be migrated to V3 in a future release.
***
## What V3 returns
The list (`GET /v3/collections`) and by-ID (`GET /v3/collections/{collectionId}`) endpoints return **Collection** objects - identity, description, media, cover image, and `rank`. The products endpoint (`GET /v3/collections/{collectionId}/products`) returns **Product** objects, not the Collection itself. Each Product carries identity, media, category, catalog, and optional brand, tags, `priceRange`, and `variantsCount` (when requested via `include` or `fields`).
→ See [Products & Variants Overview](/modules/api/v2/products/overview) for the full Product schema and `include` / `fields` reference.
***
## Key Concepts & Business Rules
#### Catalog selection
Snappy maintains parallel catalogs. The V3 endpoint accepts a `filter[catalog]` parameter to choose between them:
| Catalog | What it contains |
| :---------------------- | :----------------------------------------------------------------- |
| `marketplace` (default) | Curated gifts from third-party brands - physical and digital items |
| `swag` | Branded merchandise templates |
| `giftCards` | Gift cards |
| `donations` | Charitable donations |
If you don't specify `filter[catalog]`, the endpoint defaults to `marketplace`.
#### Cursor pagination
The products endpoint (`GET /v3/collections/{collectionId}/products`) uses **cursor pagination** (`page[cursor]` and `page[size]`, max 300, default 100). The response includes a top-level `links` object with `first`, `next`, and `prev` URLs. Use `links.next` verbatim to fetch the next page - do not parse or construct cursors manually. `links.prev` is always `null` on cursor-paginated endpoints; backward navigation is not supported.
The list endpoint (`GET /v3/collections`) uses **page-number pagination** (`page[number]` / `page[size]`) instead, with a `meta.total` count of matching Collections.
→ See [Request & Response Standards](/pages/request-response-standards) for the full V3 pagination contract.
#### Filtering, sorting, and field expansion
V3 follows JSON:API conventions:
* **Filtering** - `filter[catalog]`, `filter[search]` (free-text across product title, category, and brand), `filter[price][gte]` / `filter[price][lte]`
* **Sorting** - `sort=minPrice` or `sort=createdAt` (prefix with `-` for descending; single field only)
* **Include related entities** - `include=brand,tags` returns full Brand and Tag objects inline
* **Field expansion** - `fields=priceRange,variantsCount` requests additional computed fields.
#### Location scoping
Pass `location` (ISO 3166-1 alpha-2 country code, e.g. `US`, `GB`, `DE`) to scope pricing and availability to a specific recipient country. Defaults to `US`.
#### Account scoping
On the products endpoint, the `Snappy-Account-Id` header is **optional** and scopes queries to a specific Account - used primarily for swag validation and filtering. The list and by-ID endpoints **require** `Snappy-Account-Id` (a `404` is returned if it does not match a known Account).
#### Permissions
| Endpoint | Required scope |
| :-------------------------------------------- | :----------------- |
| `GET /v3/collections` | `collections:read` |
| `GET /v3/collections/{collectionId}` | `collections:read` |
| `GET /v3/collections/{collectionId}/products` | `products:read` |
The products endpoint shares the `products:read` scope with the V3 Products and Variants endpoints, since its response is a Product list.
***
## How to Work with Collections
**List Collections**
```text theme={null} theme={null}
GET /v3/collections
```
Returns a paginated list of Collections (page-number pagination), ordered by curated `rank` by default. Requires the `Snappy-Account-Id` header.
**Retrieve a single Collection**
```text theme={null} theme={null}
GET /v3/collections/{collectionId}
```
Returns a single Collection's metadata - name, description, cover image, thumbnails, rank, and provenance. Requires the `Snappy-Account-Id` header. Returns `404` (`404_PBLC_004`) if the Collection ID does not exist.
**Retrieve products within a Collection**
```text theme={null} theme={null}
GET /v3/collections/{collectionId}/products
```
Returns a paginated list of Products in the specified Collection. Filter by catalog, type, price, free-text search, or country. Sort by `minPrice` or `createdAt`. Expand related entities (`brand`, `tags`) and computed fields (`priceRange`, `variantsCount`) as needed.
Returns `404` (`404_PBLC_004`) if the Collection ID does not exist.
Variants are **not** returned by this endpoint. To retrieve the variants for a specific Product, use `GET /v3/products/{productId}/variants`.
***
## V2 Endpoints (Legacy)
The following V2 Collections endpoints remain available while the migration to V3 is in progress:
* `GET /collections` - list available Collections
* `GET /collections/budgets` - retrieve Collection budget ranges
* `GET /collections/{id}/products/{productId}` - get a specific Product within a Collection
* `GET /collections/{id}/products/count` - count Products in a Collection
New integrations should use V3 endpoints where available. Existing V2 endpoints will continue to work; they will not be deprecated without advance notice in the [Changelog](/pages/changelog).
# Get digital card access code
Source: https://docs.snappy.com/modules/api/v3/digital-cards/get-digital-card-access-code
get /v3/digital-cards/{digitalCardId}/access-code
Use this endpoint to retrieve the static access code for a digital card that uses access-code authentication.
###### Required parameters:
- `digitalCardId` - the digital card identifier, passed as a path parameter. Look this up via `GET /v3/digital-cards?filter[orderId]={orderId}`.
###### Behavior notes:
- Returns `{ digitalCardId, accessCode }`.
- Returns `422` if the digital card's `authentication.method` is `OTP`. Digital cards using `OTP` authentication do not have a static access code - the recipient authenticates via a one-time password delivered to their email at redemption time.
- Returns `404` if the digital card does not exist or belongs to a different Company.
###### Permissions:
- Requires: `digital-card:read`
#### Permissions
- Requires: `digitalCards:read`
# Get digital card by ID
Source: https://docs.snappy.com/modules/api/v3/digital-cards/get-digital-card-by-id
get /v3/digital-cards/{digitalCardId}
Use this endpoint to retrieve a single digital card by its stable ID.
###### Required parameters:
- `digitalCardId` - the digital card identifier, passed as a path parameter.
###### Optional query parameters:
- `include` - related entities to expand. Currently supports `brand` (returns full brand details on the digital card).
###### Behavior notes:
- Returns the digital card's redemption URL and `authentication.method` (`OTP` or `accessCode`).
- Returns `404` if the digital card does not exist or belongs to a different Company.
###### Permissions:
- Requires: `digital-card:read`
#### Permissions
- Requires: `digitalCards:read`
# List digital cards
Source: https://docs.snappy.com/modules/api/v3/digital-cards/get-digital-cards
get /v3/digital-cards
Use this endpoint to retrieve a paginated list of digital cards - typically the first step in retrieving an access code for a digital card that uses `accessCode` authentication.
###### Optional query parameters:
- `filter[orderId]` - scope the list to digital cards created for a specific order. Omit to list all digital cards visible to the caller.
- `include` - related entities to expand. Currently supports `brand` (returns full brand details on each digital card).
- `page[number]` - 1-indexed page number (default 1).
- `page[size]` - number of digital cards per page (max 150, default 100).
###### Behavior notes:
- Returns each digital card's `digitalCardId`, redemption URL, and `authentication.method`.
- If `authentication.method` is `accessCode`, follow up with `GET /v3/digital-cards/{digitalCardId}/access-code` to retrieve the code.
- Cards from other Companies are never returned - the list is scoped to the calling Company.
###### Permissions:
- Requires: `digital-card:read`
#### Permissions
- Requires: `digitalCards:read`
# Digital Cards API
Source: https://docs.snappy.com/modules/api/v3/digital-cards/overview
Use the **Digital Cards API** to look up digital cards issued through Snappy, and — for integrations that use access-code authentication — to retrieve the access codes needed to complete each redemption.
A digital card is created whenever someone places an order for a digital card product; it captures the card-specific aspects of that order (redemption URL and authentication method today, with more card-level details planned). Digital cards include gift cards, prepaid cards, and similar card-based rewards. All are delivered by email rather than physical shipment.
Digital cards are created through order placement, not directly. To create a new digital card, place an order for a digital card variant via `POST /v3/orders`. The digital card entity is created and activated as part of the order flow and can then be retrieved via this API.
## Authentication methods
Every digital card is redeemed with one of two authentication methods. The method is set for your Company during onboarding by the Snappy team and applies to all your digital cards — you don't need to check `authentication.method` per card, and changing methods later requires coordination with Snappy.
* `OTP` — the recipient authenticates by entering a one-time password. When they open the digital card page in Snappy's UI, Snappy sends a fresh, single-use code; a new code is issued on each access attempt. Snappy manages the entire flow end-to-end: notifications, code generation and delivery, and any recipient support. This is Snappy's default configuration for most integrations.
* `accessCode` — the recipient authenticates with a static code that Snappy generates at order placement and returns to you via this API. From that point on, you own the recipient communication (email, SMS, in-app message, or however your platform reaches recipients) and any related support. This setup fits integrations that want to embed the code in a single, unified notification they send themselves rather than a separate Snappy email.
If you're not sure which method your Company is on — or you want to change it — talk to your Snappy representative.
## When to use
Whether you need these endpoints depends on your Company's authentication method:
* If your Company is on `OTP`: Snappy handles the entire redemption flow, so you don't need to call these endpoints to complete a redemption. They're still available for auditing — use them to verify that a digital card was created for an order, or to keep a local record for reporting.
* If your Company is on `accessCode`: use this API to retrieve access codes after order placement, then send them to recipients through your own channel. Typical flow:
1. Place an order for a digital card variant via `POST /v3/orders`.
2. List digital cards with `GET /v3/digital-cards?filter[orderId]={orderId}` to get each `digitalCardId`.
3. Call `GET /v3/digital-cards/{digitalCardId}/access-code` to retrieve the code.
4. Include the code in your notification to the recipient.
Digital card products live in a dedicated catalog. Use `GET /v3/products?filter[catalog]=digitalCards` to browse available digital card products and their variants before placing an order.
## Endpoints
**List digital cards**
```text theme={null}
GET /v3/digital-cards
```
Retrieves a paginated list of digital cards. Supports `filter[orderId]` to scope to a specific order, `include=brand` to expand brand details, and `page[number]` / `page[size]` for pagination (max 150, default 100).
**Get digital card by ID**
```text theme={null}
GET /v3/digital-cards/{digitalCardId}
```
Retrieves a single digital card by its stable ID. Add `include=brand` to expand brand details on the response.
**Get digital card access code**
```text theme={null}
GET /v3/digital-cards/{digitalCardId}/access-code
```
Retrieves the static access code for a digital card. Returns `422` if your Company is on `OTP` authentication.
## Permissions
All Digital Cards endpoints require the `digital-card:read` scope.
## Error responses
All errors follow the standard V3 error envelope:
```json theme={null}
{
"status": 404,
"errorCode": "404_DGTC_001",
"message": "Digital card not found."
}
```
# Create collections export
Source: https://docs.snappy.com/modules/api/v3/exports/create-collections-export-job-async
post /v3/collections/exports
Use this endpoint to kick off a background export job for all products in a single collection. Returns an exportId to poll.
###### Required fields
* `collectionId` - the collection to export.
* `catalog` - product catalog. One of `marketplace`, `swag`, `giftCards`, or `donations`.
* `locations` - array of ISO 3166-1 alpha-2 country codes for price localisation.
###### Optional fields
* `format` - output file format. Currently `ndjson` only; defaults to `ndjson`.
###### Optional headers
* `Snappy-Account-Id` - optional account scoping.
* `Snappy-Company-Id` - optional company scoping.
###### Behavior notes
* Returns **200 OK** immediately with an `exportId`. The job runs in the background.
* Poll `GET /v3/products/exports/{exportId}` until the job reaches a terminal state (`completed` or `failed`). On completion, the status response includes a `downloadUrls` map of signed URLs (keyed by file identifier or location code).
* The exported NDJSON file contains one product per line, each with its full variant list and per-country `availability`. See [NDJSON export format](/modules/api/v3/exports/overview#ndjson-export-format) for the full shape.
* The export record expires **48 hours** after creation. Download the file(s) before that - expired records cannot be re-issued; you'll need to create a new export job.
* Returns `404` if the supplied `collectionId` does not exist.
#### Permissions
Requires: `products:read`
# Create products export
Source: https://docs.snappy.com/modules/api/v3/exports/create-products-export-job-async
post /v3/products/exports
Use this endpoint to kick off a background product export job. Use this when the synchronous list endpoints would require many paginated requests, or when you'd rather fire-and-forget the export and poll for completion later.
###### Required fields
* `catalog` - product catalog to export from. One of `marketplace`, `swag`, `giftCards`, or `donations`.
* `format` - output file format. Currently `ndjson` only.
###### Optional filters
* `search` - free-text search filter.
* `brandName` - filter by brand name.
* `brandId` - array of brand IDs.
* `tagId` - array of tag IDs.
* `productIds` - array of specific product IDs to export (max 100).
* `price.gte` / `price.lte` - inclusive price range (flat body fields; see note below).
* `locations` - ISO 3166-1 alpha-2 country codes for price localisation (default `["US"]`).
###### Optional response shaping
* `include` - related entities to include. One or more of `brand`, `tags`.
* `fields` - computed / expanded fields. One or more of `priceRange`, `variantsCount`.
###### Optional headers
* `Snappy-Account-Id` - optional account scoping.
* `Snappy-Company-Id` - optional company scoping.
###### Behavior notes
* Returns **200 OK** immediately with an `exportId`. The job runs in the background.
* Poll `GET /v3/products/exports/{exportId}` until the job reaches a terminal state (`completed` or `failed`). On completion, the status response includes a `downloadUrls` map of signed URLs (keyed by file identifier or location code).
* The exported NDJSON file contains one product per line, each with its full variant list and per-country `availability`. `fields` does not control which variants are included - all variants are always exported. See [NDJSON export format](/modules/api/v3/exports/overview#ndjson-export-format) for the full shape.
* The export record expires **48 hours** after creation. Download the file(s) before that - expired records cannot be re-issued; you'll need to create a new export job.
* Body-level price filters use **flat** `price.gte` / `price.lte` fields rather than the nested `filter[price][gte]` / `filter[price][lte]` form used in query strings on V3 list endpoints. Same semantics, different ergonomic surface.
#### Permissions
Requires: `products:read`
# Export API: Bulk Catalog Ingestion
Source: https://docs.snappy.com/modules/api/v3/exports/overview
Export the Snappy catalog in bulk via asynchronous NDJSON jobs. Filter by product or scope to a collection, then poll for the signed download URL.
The **Export API** powers bulk catalog ingestion. Instead of paging through `/v3/products` or `/v3/collections/{collectionId}/products` to mirror the Snappy catalog into your own platform, you queue an asynchronous export job and pull a single NDJSON file containing the full result set.
Export is the recommended way to keep a local product mirror up to date - pair a nightly bulk export with [Webhooks](/pages/webhook-event-types) for incremental product changes between exports.
Want to understand how Export fits into the bigger picture? See [Snappy Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models).
Exports are **NDJSON-only** for now. Each line in the file is a single JSON object representing one product, with the same shape as `GET /v3/products/{productId}` plus an export-only `variants` array containing every variant with per-country availability. See [NDJSON export format](#ndjson-export-format) below for the full shape.
***
## When to use Export
Use the Export API when:
* You need the **entire catalog** (or a large filtered subset) in a single payload - typically for nightly ingestion into a partner system, search index, or data warehouse.
* The synchronous list endpoints would require dozens or hundreds of paginated requests.
* You'd rather fire-and-forget the job and pick up the result later than hold an HTTP connection open.
Use the synchronous list endpoints (`GET /v3/products`, `GET /v3/collections/{collectionId}/products`) when:
* You need **real-time** browse / search inside your UI.
* The result set is small enough to fit in one or two pages.
***
## How export jobs work
Export is a standard async job pattern:
Call `POST /v3/products/exports` (filtered export) or `POST /v3/collections/exports` (collection-scoped export). Both return **200 OK** with an `exportId` immediately.
Call `GET /v3/products/exports/{exportId}` until the job reaches a terminal state.
When `status` is `completed`, the response includes a `downloadUrls` map of signed URLs. Download the file(s) before the export record expires.
### Status lifecycle
| Status | Description |
| :----------- | :---------------------------------------------------------------------------------------- |
| `pending` | Job has been queued; processing has not started. |
| `processing` | Job is actively running. |
| `completed` | Job finished successfully. `downloadUrls` is populated with signed S3 URLs. |
| `failed` | Job terminated without producing a file. `errorMessage` contains a human-readable reason. |
***
## Key Concepts & Business Rules
#### Download URLs are signed and short-lived
When a job completes, the poll response returns a `downloadUrls` map (keyed by file identifier or location code). These are signed S3 URLs that **expire 48 hours** after the export record is created. After expiry, you'll need to re-issue a new export job.
#### NDJSON, not JSON
Exports produce newline-delimited JSON - one product (with its variants) per line. This makes it safe to stream-parse multi-gigabyte exports without holding the whole payload in memory.
#### Filtering uses flat body fields (not the JSON:API `filter[...]` query convention)
V3 list endpoints accept filters as bracketed query parameters (`filter[price][gte]`, `filter[brandId]`). Export jobs accept the same filters as **flat JSON body fields** (`price.gte`, `brandId`) - this is intentional for JSON ergonomics. Same semantics, different surface.
#### Idempotency is not currently supported
Repeated calls to `POST /v3/products/exports` with identical bodies create separate export jobs. If you need idempotency for nightly runs, persist the `exportId` of the latest in-flight job on your side and skip creating a new one when one is already pending.
#### Locations drive price localisation
The `locations` array (ISO 3166-1 alpha-2 country codes) is used to localise prices and availability in the exported file. Defaults to `["US"]` on the products export endpoint; required on the collections export endpoint.
#### Permissions
All Export endpoints require:
Requires: `products:read`
***
## NDJSON export format
Each line in the export file is a full `Product` JSON with one export-only addition: a `variants` array containing every variant on that product.
### Per-line shape
* **Product fields** - identical to `GET /v3/products/{productId}`, including any `include` (`brand`, `tags`) and `fields` (`priceRange`, `variantsCount`) expansions you requested.
* **`variants`** - an array of the product's variants, each with its full details plus an `availability` object.
### Variant `availability`
Each variant in the export includes an `availability` object with one entry per country listed in your `locations` filter. Each entry follows the same shape as `GET /v3/variants/{variantId}/availability`:
* **`isAvailable`** - whether the variant is orderable in that country.
* **`price`** - the localised price for that country.
* **`priceBreakdown`** - a breakdown of `itemPrice`, `shippingFee`, and `ddp` (duties, taxes, and paid-on-delivery fees). Partner pricing is reflected here when applicable.
**`fields` does not control variants in the export.** Variant data is always fully included on every line - you can't opt into a subset. If you don't need variant details, ignore the `variants` array on the consumer side. Variant-level `fields` (e.g. `price`, `priceBreakdown`, `details`, `brand`) apply only to the synchronous variant endpoints, not to the export.
### Example line
```json theme={null}
{
"id": "655277e68e0719000d6c3fd5",
"title": "NFL 25-Layer StadiumView Wall Art",
"createdAt": "2026-05-11T10:53:01.026Z",
"category": {
"fullName": "Fan Merchandise / NFL / NFL Memorabilia / Autographed Helmets"
},
"catalog": "marketplace",
"media": [
{ "type": "image", "src": "https://media.snappy.com/image/o1xc17wfbda6cl91hm0r6_picture-1.jpg?w=1000&h=1000&q=80&f=auto" }
],
"brand": {
"id": "6511b55142c420000d083a55",
"name": "YouTheFan",
"description": "Officially licensed NFL fan merchandise."
},
"tags": [
{ "id": "6527bb3567f414000c15d2c7", "name": "Snappy's Picks", "backgroundColor": "#13C2C2", "textColor": "#FFFFFF" }
],
"priceRange": {
"min": { "amount": 244.99, "currency": "USD" },
"max": { "amount": 249.99, "currency": "USD" }
},
"variantsCount": 9,
"variants": [
{
"id": "FB6bgFV4lf",
"productId": "655277e68e0719000d6c3fd5",
"title": "NFL 25-Layer StadiumView Wall Art",
"selectedOptions": { "nfl_team": "Denver Broncos" },
"taxable": true,
"media": [
{ "type": "image", "src": "https://media.snappy.com/image/psqy00xpuxrhrdelvkiz?w=1000&h=1000&q=80&f=auto" }
],
"personalization": null,
"descriptionHtml": "The 25-Layer StadiumViews 3D Wall Art ...
",
"details": {
"includes": null,
"features": null,
"specifications": null,
"notices": null
},
"brand": {
"id": "6511b55142c420000d083a55",
"name": "YouTheFan",
"description": "Officially licensed NFL fan merchandise."
},
"availability": {
"US": {
"isAvailable": true,
"price": { "amount": 244.99, "currency": "USD" },
"priceBreakdown": { "ddp": 0, "shippingFee": 0, "itemPrice": 244.99 }
},
"CA": {
"isAvailable": true,
"price": { "amount": 268.50, "currency": "USD" },
"priceBreakdown": { "ddp": 12.50, "shippingFee": 11.01, "itemPrice": 244.99 }
}
}
},
{
"id": "MdjSPF4zHE",
"productId": "655277e68e0719000d6c3fd5",
"title": "NFL 25-Layer StadiumView Wall Art",
"selectedOptions": { "nfl_team": "Philadelphia Eagles" },
"taxable": true,
"media": [
{ "type": "image", "src": "https://media.snappy.com/image/hljxb9yzjomaf4qpbs22?w=1000&h=1000&q=80&f=auto" }
],
"personalization": null,
"descriptionHtml": "...
",
"details": {
"includes": null,
"features": null,
"specifications": null,
"notices": null
},
"brand": {
"id": "6511b55142c420000d083a55",
"name": "YouTheFan",
"description": "Officially licensed NFL fan merchandise."
},
"availability": {
"US": {
"isAvailable": true,
"price": { "amount": 249.99, "currency": "USD" },
"priceBreakdown": { "ddp": 0, "shippingFee": 0, "itemPrice": 249.99 }
},
"CA": {
"isAvailable": true,
"price": { "amount": 273.75, "currency": "USD" },
"priceBreakdown": { "ddp": 12.75, "shippingFee": 11.01, "itemPrice": 249.99 }
}
}
}
]
}
```
***
## How to Work with Export
**Create a filtered product export job**
```text theme={null}
POST /v3/products/exports
```
Queues an async export of products matching the supplied filters. Required: `catalog`, `format`. Optional filters: `search`, `brandName`, `brandId`, `tagId`, `productIds`, `price.gte`/`price.lte`, `locations`, `include`, `fields`. Returns **200 OK** with an `exportId` to poll.
**Create a collection export job**
```text theme={null}
POST /v3/collections/exports
```
Queues an async export of every product in a single collection. Required: `collectionId`, `catalog`, `locations`. Returns **200 OK** with an `exportId` to poll.
**Poll export job status**
```text theme={null}
GET /v3/products/exports/{exportId}
```
Returns the current status of an export job. When `status` is `completed`, includes a `downloadUrls` map with signed URLs. When `status` is `failed`, includes an `errorMessage`. Polls both products and collections export jobs.
***
# Get export status
Source: https://docs.snappy.com/modules/api/v3/exports/poll-export-job-status
get /v3/products/exports/{exportId}
Use this endpoint to check the status of an asynchronous export job. Poll this endpoint after calling `POST /v3/products/exports` or `POST /v3/collections/exports` until the job reaches a terminal state (`completed` or `failed`).
###### Required parameters:
- `exportId` - the export job identifier returned from the create call, passed as a path parameter.
###### Please note:
- Status transitions: `pending` → `processing` → `completed` | `failed`.
- When `status` is `completed`, the response includes a `downloadUrls` map of signed URLs keyed by file identifier (e.g. `"0"`) or location code (e.g. `"US"`). Download the file(s) before the export record expires (48 hours after creation).
- When `status` is `failed`, the response includes an `errorMessage` describing the failure (e.g. `"Export timed out"`).
- The same poll endpoint serves both product export jobs and collection export jobs - `exportId` is sufficient to look up either.
- Returns `404` if the supplied `exportId` does not exist or the export record has expired.
#### Permissions
- Requires: `products:read`
# Autocomplete order address
Source: https://docs.snappy.com/modules/api/v3/orders/autocomplete-order-address
get /v3/orders/addresses/autocomplete
Use this endpoint to retrieve address suggestions based on a partial input string. Use this when you're building an address input field in your platform UI - autocomplete reduces typos and helps end users land on complete, deliverable addresses before order placement.
###### Required parameters:
- `filter[address]` query parameter - partial address text from the user. 4-128 characters.
- `filter[country]` query parameter - two-letter country code to scope the suggestions to.
###### Please note:
- Returns an array of suggestions in the `data` field. Each entry follows the standard address shape (`address1`, `address2`, `city`, `provinceCode`, `postalCode`, `countryCode`).
- The address parameter is free text and the endpoint may be called frequently as the user types. Debounce input by 200-300ms before issuing the request to avoid excessive calls and stay within rate limits.
- Returns `400` when address or country are missing or malformed (e.g. address shorter than 4 characters).
- Returns `422` when the input cannot be processed by the autocomplete service.
- This endpoint is a UI helper - it suggests addresses but does not validate deliverability. Pair it with `POST /v3/orders/addresses/validate` before placing an order if you need verified-deliverable addresses.
#### Permissions
- Requires: `orders:read:masked` or `orders:read:unmasked`
# Cancel order
Source: https://docs.snappy.com/modules/api/v3/orders/cancel-order
post /v3/orders/{orderId}/cancel
Use this endpoint to cancel an order that has not yet been picked up by the fulfillment partner. Use this when the recipient or sender requests a cancellation before the shipment is in transit.
###### Required fields:
- `orderId` - the order identifier, passed as a path parameter.
###### Optional parameters:
- `Snappy-Account-Id` header - optional account scoping.
- `Snappy-Company-Id` header - optional company scoping.
###### Behavior Notes:
- On success, returns the full Order object with `status: "cancelled"`, `fulfillmentStatus: "cancelled"`, and a populated `cancellationDetails` object containing `cancelledAt` and `cancellationReason`.
- `cancellationReason` is currently hardcoded to "customer_requested". Caller-supplied reasons may be added in a future release.
- `cancellationReason` is currently hardcoded to `customer_requested`. Caller-supplied reasons may be added in a future release.
- Returns `404` if no order exists for the supplied `orderId`, or if it exists but belongs to a different Company. Existence is intentionally hidden across Companies.
- Returns `422` when the order cannot be cancelled - typically because it is already in transit, already delivered, or already cancelled. The error envelope identifies which case fired.
- Cancellation is terminal. A cancelled order cannot be re-activated; if the recipient still needs a gift, place a new order via `POST /v3/orders`.
#### Permissions
- Requires: `orders:cancel`
# Get order by ID
Source: https://docs.snappy.com/modules/api/v3/orders/get-order-by-id
get /v3/orders/{orderId}
Use this endpoint to retrieve a single order by its identifier with full detail - line items, recipient, shipping address, fulfillments with tracking information, tags, and metadata. Use this when you need detail beyond what's returned by GET /v3/orders (which returns the same shape) - typically for a single-order detail view or to refresh tracking information.
###### Required fields:
- `orderId` - the order identifier, passed as a path parameter.
###### Optional parameters:
- `Snappy-Account-Id` header - optional account scoping.
- `Snappy-Company-Id` header - optional company scoping.
###### Please note:
- Returns `404` if no order exists for the supplied `orderId`, or if it exists but belongs to a different Company. Existence is intentionally hidden across Companies.
- `fulfillments` is an empty array (`[]`) when the order has not yet been shipped or when tracking information is unavailable.
- `cancellationDetails` is populated only when status is cancelled; null otherwise.
- `metadata` is null when no metadata was supplied at order creation.
- `shippingAddress` is populated in full for physical orders. For digital orders (e.g. gift cards, where the variant's `shippingRequired` is `false`), only `countryCode` is present - the other address fields are omitted since there's no physical delivery.
- PII fields (`recipient name`, `tracking info`) are masked under the `orders:read:masked` scope and returned in full under `orders:read:unmasked`.
#### Permissions
- Requires: `orders:read:masked` or `orders:read:unmasked`
# Get orders
Source: https://docs.snappy.com/modules/api/v3/orders/get-orders
get /v3/orders
Use this endpoint to retrieve a paginated list of orders for the calling Company. Use this when you want to browse orders, look up orders matching specific idempotency keys, or sync recent order activity to your system.
###### Filtering options:
- `filter[status]` - exact match on order status. One of active (in progress or delivered) or cancelled.
- `filter[idempotencyKey]` - comma-separated list of idempotency keys. Returns orders matching any of the supplied keys.
- `filter[createdAt][gte]` - return orders created at or after this ISO 8601 timestamp.
- `filter[createdAt][lte]` - return orders created at or before this ISO 8601 timestamp.
- `Snappy-Account-Id` header - optional account scoping.
- `Snappy-Company-Id` header - optional company scoping.
###### Pagination and sorting:
- `page[number]` - 1-indexed page number (default 1).
- `page[size]` - number of orders per page (max 300, default 100).
- `sort` - -createdAt (default, newest first) or createdAt (oldest first).
###### Please note:
- Uses page-number pagination (not cursor, unlike product list endpoints). The response includes a top-level links object with first, next, and prev URLs.
- The `filter[idempotencyKey]` filter is useful for looking up orders created by specific replays. Follow the order with `GET /v3/orders/{orderId}` if you need additional detail beyond what's in the list response.
- Both Direct Fulfillment orders and Triggered Gifting orders are returned by this endpoint - they share the same response shape.
- Each order in the list includes its `shippingAddress`. For physical orders, the full address is returned. For digital orders (e.g. gift cards), only `countryCode` is present.
- PII masking applies based on the scope used for the request.
#### Permissions
- Requires: `orders:read:masked` or `orders:read:unmasked`
# Orders API: Place, Track, and Manage Orders
Source: https://docs.snappy.com/modules/api/v3/orders/overview
Place orders via Direct Fulfillment in a single idempotent call, retrieve orders with full line item and fulfillment tracking, list and filter orders, and cancel unfulfilled orders.
An **Order** represents the physical fulfillment event - the point at which a gift becomes a shipment. The Order is the **primary integration object for Direct Fulfillment**: you create it via a single call to `POST /v3/orders` with the recipient and variant details, then track its progress through the lifecycle.
Orders are also generated automatically by Snappy when a recipient claims their gift in the **Triggered Gifting** model - those orders are retrievable, listable, and cancellable through the same V3 Orders API.
Want to understand how **Orders** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page.
The V3 Orders API exposes Orders as first-class resources with their own lifecycle, line items, fulfillments, and tracking - independent of the Gift entity. You no longer need to retrieve orders through their parent Gift.
***
## The Order Object
| Field | Type | Description |
| :-------------------- | :----------- | :------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Unique identifier of the Order (e.g. `G7nR4bD9mK`) |
| `status` | enum | Order lifecycle status. One of: `active`, `completed`, `cancelled`, `refunded` |
| `fulfillmentStatus` | enum | Aggregated fulfillment status across all line items. One of: `unfulfilled`, `fulfilled`, `cancelled` |
| `idempotencyKey` | string | Caller-supplied idempotency key from order creation |
| `tags` | string array | Caller-supplied tags for grouping and filtering orders in reports |
| `lineItems` | array | Products included in this Order. See [Line items](#line-items) below. |
| `fulfillments` | array | Fulfillment records - shipments with carrier, tracking info, and delivery status. See [Fulfillments](#fulfillments) below. |
| `recipient` | object | Recipient contact details. See [Recipient](#recipient) below. |
| `shippingAddress` | object | Physical shipping address. See [Shipping address](#shipping-address) below. |
| `metadata` | object | Caller-supplied key-value pairs. `null` when no metadata was supplied. |
| `cancellationDetails` | object | Populated only when `status` is `cancelled`. See [Cancellation details](#cancellation-details) below. |
***
### Order status
| Status | Description |
| :---------- | :----------------------------------- |
| `active` | Being processed or in fulfillment |
| `completed` | Fulfilled and finalized |
| `cancelled` | Cancelled before fulfillment |
| `refunded` | Returned and refunded after delivery |
### Fulfillment status
Aggregated across all line items.
| Status | Description |
| :------------ | :------------------------------------------------- |
| `unfulfilled` | No fulfillments exist yet |
| `fulfilled` | Every line item is covered by a fulfillment record |
| `cancelled` | Order fulfillment was cancelled |
***
### Line items
| Field | Type | Description |
| :---------- | :------ | :------------------------------------------ |
| `variantId` | string | Variant ID of the ordered product |
| `title` | string | Product title captured at the time of order |
| `quantity` | integer | Number of units of this variant ordered |
### Fulfillments
A Fulfillment represents a shipment containing one or more line items. An Order can have multiple Fulfillments (e.g. split shipments), and a Fulfillment can cover multiple line items (e.g. consolidated shipping). This is a many-to-many relationship between line items and fulfillments.
| Field | Type | Description |
| :--------------------- | :----- | :-------------------------------------------------------------------------- |
| `id` | string | Unique identifier of the fulfillment record |
| `status` | enum | Carrier delivery status. See [Delivery statuses](#delivery-statuses) below. |
| `trackingCompany` | string | Carrier name (e.g. `UPS`, `USPS`, `FedEx`) |
| `trackingInfo.number` | string | Carrier tracking number |
| `trackingInfo.url` | string | Carrier tracking page URL |
| `fulfillmentLineItems` | array | Line items included in this fulfillment |
#### Delivery statuses
| Status | Description |
| :----------------- | :--------------------------- |
| `confirmed` | Order received by the vendor |
| `processing` | Being prepared for shipment |
| `in_transit` | Shipped and in transit |
| `out_for_delivery` | Out for delivery |
| `delivered` | Delivered to the recipient |
Use Webhooks to track delivery status changes in real time rather than polling. See [Webhook Event Types](/pages/webhook-event-types).
### Recipient
| Field | Type | Description |
| :---------- | :----- | :---------------------- |
| `firstName` | string | Recipient first name |
| `lastName` | string | Recipient last name |
| `email` | string | Recipient email address |
| `phone` | string | E.164 phone number |
### Shipping address
| Field | Type | Description |
| :------------- | :----- | :--------------------------------------------- |
| `address1` | string | Street address (max 35 chars) |
| `address2` | string | Apartment, suite, floor (max 35 chars) |
| `city` | string | City name |
| `provinceCode` | string | State or province code (2-3 uppercase letters) |
| `postalCode` | string | Postal/ZIP code (alphanumeric, 3-10 chars) |
| `countryCode` | string | ISO 3166-1 alpha-2 country code (uppercase) |
### Cancellation details
| Field | Type | Description |
| :------------------- | :---------------- | :------------------------------------------------------------ |
| `cancelledAt` | string (ISO 8601) | Timestamp when the Order was cancelled |
| `cancellationReason` | enum | Reason for cancellation. Currently only: `customer_requested` |
***
## Key Concepts & Business Rules
#### Idempotency on order placement
Every `POST /v3/orders` request must include an `idempotencyKey` (1-120 characters). If a request with the same key has already succeeded for this Company, the original order is returned - no duplicate is created. Use stable, caller-generated keys (e.g. your internal order ID).
See [Duplicate Detection](/pages/duplicate-gifts-detection) for the full idempotency reference.
#### Variants are required - not Products
When placing an order, you must always specify the `variantId`, not the `productId`. Every Product has at least one Variant, even if it has no variations. See [Products & Variants Overview](/modules/api/v2/products/overview).
#### Tags vs metadata
Two separate fields with different purposes:
* **`tags`** - array of strings for grouping and filtering orders in reports (e.g. `["q4-campaign", "vip"]`).
* **`metadata`** - key-value object for arbitrary passthrough data (e.g. `{"externalRecipientId": "crm-user-987"}`). Up to 50 pairs, keys up to 40 chars, values up to 500 chars.
Use `tags` for categorization; use `metadata` for data you want to round-trip.
#### Status vs fulfillment status
The Order has two distinct status fields that capture different concerns:
* **`status`** tracks the **commercial** lifecycle: `active` → `completed`, or `active` → `cancelled` / `refunded`
* **`fulfillmentStatus`** tracks the **physical** lifecycle aggregated across line items: `unfulfilled` → `fulfilled` (or `cancelled`)
Per-shipment carrier progress lives inside `fulfillments[].status`.
#### Cancellation rules
Orders can only be cancelled before they are picked up by the fulfillment partner. Once an Order is in transit, cancellation returns `422`. The `cancellationReason` enum currently only supports `customer_requested`; additional reasons may be added in future releases.
#### Address validation reduces fulfillment failures
Invalid or incomplete shipping addresses are a common cause of fulfillment failures. We recommend validating the recipient's address via `POST /orders/addresses/validate` before calling `POST /v3/orders`, especially when addresses are entered by end users in your platform UI.
#### Billing is triggered at order creation
The Billing Method (referenced via `billingMethodId`) is debited when an Order is successfully placed. If the Billing Method has insufficient funds at the time of the request, the order will not be processed. See [Billing Methods Overview](/modules/api/v3/billing-methods/overview) to check remaining balance before placing high-value orders.
#### Pagination
The V3 list endpoint uses **page-number pagination** (`page[number]` and `page[size]`, max 300, default 100). Responses include a top-level `links` object with `first`, `next`, and `prev` URLs.
See [Request & Response Standards](/pages/request-response-standards) for the full V3 pagination contract.
#### Account scoping
Pass the optional `Snappy-Account-Id` header to scope queries to a specific Account. For `POST /v3/orders`, `accountId` may alternatively be supplied in the request body - the header takes precedence when both are provided.
#### Permissions
The V3 Orders endpoints require different scopes depending on the operation:
| Endpoint | Required scope |
| :--------------------------------- | :--------------------------------------------- |
| `POST /v3/orders` | `orders:create` |
| `GET /v3/orders` | `orders:read:masked` or `orders:read:unmasked` |
| `GET /v3/orders/{orderId}` | `orders:read:masked` or `orders:read:unmasked` |
| `POST /v3/orders/{orderId}/cancel` | `orders:cancel` |
PII fields (recipient name, tracking info) are masked under `orders:read:masked` and returned in full under `orders:read:unmasked`.
***
## How to Work with Orders
**Place an order** *(Direct Fulfillment)*
```text theme={null} theme={null}
POST /v3/orders
```
Creates a new Order for a single variant on behalf of a recipient. Returns a minimal create result with the Order ID, initial status (`active`), and a tracking link. Required: `billingMethodId`, `variantId`, `recipient`, `shippingAddress`, `idempotencyKey`. Optional: `accountId`, `tags`, `metadata`.
**List orders**
```text theme={null} theme={null}
GET /v3/orders
```
Returns a paginated list of Orders matching the supplied filters. Filter by `status` (`active`, `cancelled`), `idempotencyKey` (comma-separated array), or `createdAt` range. Sort by `createdAt` (newest first by default).
**Get an order by ID**
```text theme={null} theme={null}
GET /v3/orders/{orderId}
```
Retrieves a single Order with full detail - line items, recipient, shipping address, fulfillments, and tracking info.
**Cancel an order**
```text theme={null} theme={null}
POST /v3/orders/{orderId}/cancel
```
Cancels an Order that has not yet been fulfilled. Returns the Order in its post-cancellation state with `status: "cancelled"` and a populated `cancellationDetails` object. Returns `422` if the Order has already been picked up by the fulfillment partner.
***
# Place order
Source: https://docs.snappy.com/modules/api/v3/orders/place-order
post /v3/orders
Use this endpoint to create a new order for a specific product variant on behalf of a recipient via the Direct Fulfillment integration. Use this when you already know the recipient's shipping address and the exact variant to ship - V3 collapses gift creation and order placement into a single idempotent call.
Use this endpoint to create a new order for a specific product variant on behalf of a recipient via the Direct Fulfillment integration. Use this when you already know the recipient's shipping address and the exact variant to ship - V3 collapses gift creation and order placement into a single idempotent call.
###### Required fields:
* `billingMethodId` - the Billing Method that will pay for the order. Must belong to the specified Account. Use GET /v3/billing-methods to discover available methods.
* `variantId` - the specific product variant to order. Use GET /v3/products to browse variants.
* `recipient` - recipient contact information. Requires firstName, lastName, email, and phone (E.164 format).
* `shippingAddress` - the delivery address. Only `countryCode` (ISO 3166-1 alpha-2, uppercase) is required at the schema level. For physical variants, the full address is also required - see Behavior notes below.
* `idempotencyKey` - a stable, caller-generated key (1-120 characters). Replaying the same request with the same key returns the original order without creating a duplicate.
###### Optional fields:
* `accountId` - the Account placing the order. May be supplied here in the body or via the Snappy-Account-Id header (header takes precedence when both are provided).
* `tags` - array of strings for grouping orders in reports (e.g. \["q4-campaign", "vip"]).
* `metadata` - key-value object for caller-supplied passthrough data. Up to 50 pairs; keys up to 40 chars; values up to 500 chars.
* `Snappy-Company-Id` header - optional company scoping.
###### Behavior Notes:
* The response is minimal: . Use GET /v3/orders/ to retrieve the full Order with line items, fulfillments, and tracking detail.
* `status` is always active immediately after a successful placement.
* The Billing Method is debited on successful order creation. If it has insufficient funds at the time of the request, the order is not processed (422).
* Returns `422` for business-rule violations (insufficient funds, variant unavailable in the recipient's country, variant not found, etc.).
* Returns `404` if the referenced `accountId`, `billingMethodId`, or `variantId` does not exist or is not accessible to the calling Company.
* **Shipping address requirements depend on the variant type.** For **physical variants** (variants where `shippingRequired: true`), the full address is required: `address1`, `city`, `provinceCode`, `postalCode`, and `countryCode`. `address2` is optional but recommended. For **digital variants** (`shippingRequired: false`, e.g. gift cards and e-vouchers), `countryCode` alone is sufficient - no street/city/postal fields are needed since there's no physical delivery.
* Missing required address fields on a physical variant return a validation error.
* Incomplete addresses on physical orders are a leading cause of fulfillment failures - we recommend validating with `POST /orders/addresses/validate` before placing the order.
#### Permissions
* Requires: `gifts:create` or `orders:create`
# Validate order address
Source: https://docs.snappy.com/modules/api/v3/orders/validate-order-address
post /v3/orders/addresses/validate
Use this endpoint to validate a physical shipping address before placing an order. Use this when end users are entering shipping addresses in your platform UI - validating up front catches errors early and reduces fulfillment failures. This endpoint is only relevant for physical variants (variants where `shippingRequired` is `true`); digital variants (e.g. gift cards) require only `countryCode` and skip address validation.
###### Required fields:
- `address` - the address object to validate. Contains:
- `address1` - street address (validated as required)
- `address2` - apartment, suite, floor (optional)
- `city` - city name (validated as required)
- `provinceCode` - state or province code (1-3 uppercase alphanumeric characters)
- `postalCode` - postal/ZIP code (alphanumeric, 3-10 characters)
- `countryCode` - two-letter uppercase country code
###### Optional parameters:
- `Request-Source` header - source of the request (api_native, api_zapier, api_salesforce, api_ftp, api_make)
###### Please note:
- Successful validation returns one of two `result` values:
- `verified` - the address is correct and deliverable
- `ambiguous` - the address was found but with ambiguity (e.g. multiple matches). Consider surfacing the response message to the end user to confirm before proceeding.
- Returns `400` with a per-field errors array when address validation fails (e.g. invalid address format, missing required fields). Each entry includes the offending path, a human-readable message, and the errorCode.
- Returns `422` when the address is well-formed but cannot be found by the validation service.
- This endpoint does not place an order - it's a pre-flight check. Always follow up with placing an order once validation succeeds.
- Do not call this endpoint for digital orders. Digital variants (`shippingRequired: false`) don't have a physical shipping address to validate - `countryCode` alone is sufficient at order placement.
#### Permissions
- Requires: `orders:read:masked` or `orders:read:unmasked`
# V3 API Overview
Source: https://docs.snappy.com/modules/api/v3/overview
Bring Snappy's curated catalog into your own platform. Your users browse and select; your system places orders directly through the API.
V3 is designed for integrations that embed the Snappy catalog directly in your product. It introduces a standardised request/response convention (JSON:API-aligned) across all endpoints.
## What's in V3
`POST /v3/orders` - one call to place an order.
Products, variants, collections, and tags.
Read balances and funding sources.
## What's new vs V2
| Convention | V2 | V3 |
| :------------------ | :--------------------- | :--------------------------------------- |
| **Pagination** | `skip` / `limit` | `page[number]` / `page[cursor]` |
| **Filtering** | Bespoke per endpoint | `filter[field]` JSON:API style |
| **Error codes** | Symbolic (`NOT_FOUND`) | Structured (`404_PROD_001`) |
| **Scoping headers** | - | `Snappy-Account-Id`, `Snappy-Company-Id` |
Still using V2? It's fully supported. See the [V2 reference](/modules/api/v2/overview).
# Get product tags
Source: https://docs.snappy.com/modules/api/v3/product-tags/get-product-tags
get /v3/product-tags
Use this endpoint to retrieve a paginated list of all available product tags. Use tags to categorize and filter products when building your catalog UI.
###### Filtering options
- `filter[name]` - search string to filter tags by name. **Minimum 3 characters** when provided. Omit to return all tags.
- `page[number]` - 1-indexed page number (default `1`)
- `page[size]` - number of tags per page (max `100`, default `100`)
- `Snappy-Account-Id` / `Snappy-Company-Id` headers - optional scoping
#### Permissions
- Requires: `products:read`
# Get product by ID
Source: https://docs.snappy.com/modules/api/v3/products/get-product-by-id
get /v3/products/{productId}
Use this endpoint to retrieve a single product by its stable product ID. Returns product-level fields only.
###### Optional parameters
- `include=brand,tags` - return related entities in the response
- `fields=options,priceRange,variantsCount` - return additional product fields. On this endpoint, `fields=options` returns the aggregated variant options array.
- `filter[price][gte]` / `filter[price][lte]` - scope the variants used when computing `priceRange` and `variantsCount`
- `location` - ISO 3166-1 alpha-2 country code (default `US`)
- `Snappy-Account-Id` / `Snappy-Company-Id` headers - optional scoping
###### Please note
- This endpoint returns product-level data only. Use `GET /v3/products/{productId}/variants` to retrieve the full paginated variants list.
- `filter[price][gte]` and `filter[price][lte]` scope the *variants* used when computing `priceRange` and `variantsCount` - they do not filter whether the product itself is returned.
#### Permissions
- Requires: `products:read`
# Get product recommendations
Source: https://docs.snappy.com/modules/api/v3/products/get-product-recommendations
get /v3/products/{productId}/recommendations
Use this endpoint to retrieve products related to a given source product.
###### Optional parameters
- `filter[price][gte]` / `filter[price][lte]` - restrict recommendations to a price range. Both bounds must be supplied together.
- `collectionId` - restrict recommendations to products in a given collection.
- `page[limit]` - control the size of the returned set (0-20).
- `include=brand,tags` - return related entities in each recommended product.
###### Please note
- At most `page[limit]` products are returned.
- This endpoint is not paginated.
- Returns `404` (`404_PROD_001`) when the source `productId` does not exist.
- Returns `422` (`422_PBLC_003`) when `collectionId` references a collection that does not exist or is not accessible.
#### Permissions
- Requires: `products:read`
# Get product variants
Source: https://docs.snappy.com/modules/api/v3/products/get-product-variants
get /v3/products/{productId}/variants
Use this endpoint to retrieve a paginated list of variants for a specific product.
###### Filtering options:
- Selected options (e.g., size, color) - exact match, multiple options are ANDed together
- Price range (min and max)
###### Optional parameters
- `include=brand` - include the variant's `brand` object (same shape as the product-level brand, nullable)
- `fields=pricing,details` - include variant pricing (`price` and `priceBreakdown`) and the structured `details` wrapper
- `filter[price][gte]` / `filter[price][lte]` - inclusive min/max variant price filter
- `location` - ISO 3166-1 alpha-2 country code (default `US`) to scope pricing
- `page[number]` / `page[size]` - page-number pagination (1-indexed; max 300 per page, default 100)
- `Snappy-Account-Id` / `Snappy-Company-Id` headers - optional scoping
###### Please note
When `fields` is omitted, `price`, `priceBreakdown`, and `details` are not returned in the public response. Unknown filter keys will return a 400 error.
#### Permissions
- Requires: `products:read`
# Get products
Source: https://docs.snappy.com/modules/api/v3/products/get-products
get /v3/products
Use this endpoint to retrieve a paginated list of products across all collections. Returns the same response shape as the collection-scoped endpoint, without requiring a collectionId.
###### Filtering options
- `filter[catalog]` - `marketplace`, `swag`, `giftCards`, or `donations` (defaults to `marketplace`)
- `filter[search]` - free-text search across product title, category, and brand
- `filter[brandId]` - comma-separated list of brand IDs (OR semantics)
- `filter[brandName]` - case-insensitive substring search on brand name
- `filter[tagId]` - comma-separated list of tag IDs (OR semantics)
- `filter[price][gte]` / `filter[price][lte]` - inclusive minimum / maximum price filter
- `location` - ISO 3166-1 alpha-2 country code (default `US`) to scope pricing
- `Snappy-Account-Id` / `Snappy-Company-Id` headers - optional account/company scoping
###### Please note
- Variants are not returned on this endpoint. Use GET /v3/products/{productId}/variants to retrieve the variants list for a specific product.
#### Permissions
- Requires: `products:read`
# Products API: Browse Snappy's Catalog
Source: https://docs.snappy.com/modules/api/v3/products/overview
Retrieve products, search and filter the catalog, browse product tags, and access the variants belonging to each product.
A **Product** is a single specific item available in the Snappy catalog - a physical gift, branded swag, a digital item, a gift card, or a donation.
The V3 Products API lets you browse and retrieve products, search the catalog, and discover the tags used to categorize them. Each Product is the display-level entity; the actual orderable units are **Variants**, retrievable via the product's variants endpoint or through the [Variants API](/modules/api/v3/variants/overview).
Want to understand how **Products** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page.
Products belong to one of four catalogs: `marketplace` (Snappy's curated gift catalog), `swag` (branded merchandise templates), `giftCards`, or `donations`. Use the `filter[catalog]` parameter to choose which catalog to query.
***
## The Product Object
| Field | Type | Description |
| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------- |
| `id` | string | Stable product identifier |
| `title` | string | Display name of the product |
| `createdAt` | string (ISO 8601) | When the product was added to the catalog |
| `media` | array | Product media items (images, video). Each item contains `type` and `src`. |
| `category` | object | Category taxonomy. Contains `fullName` - full path separated by `/`. |
| `catalog` | enum | `marketplace`, `swag`, `giftCards`, or `donations` |
| `brand` | object | Returned only when `include=brand`. Contains `id`, `name`, `description`. Nullable. |
| `tags` | array | Returned only when `include=tags`. Each tag has `id` and `name`. |
| `options` | array | Aggregated variant options (e.g. `color`, `size`). Returned only when `fields` includes `options`. |
| `priceRange` | object | Min/max pricing across the product's variants. Returned only when `fields` includes `priceRange`. |
| `variantsCount` | integer | Number of variants. Returned only when `fields` includes `variantsCount`. |
***
## Key Concepts & Business Rules
#### Catalog selection
Every Product belongs to one of four catalogs:
| Catalog | What it contains |
| :---------------------- | :----------------------------------------------------------------- |
| `marketplace` (default) | Curated gifts from third-party brands - physical and digital items |
| `swag` | Branded merchandise templates |
| `giftCards` | Gift cards |
| `donations` | Charitable donations |
The `filter[catalog]` parameter is required on list endpoints (defaults to `marketplace`). Use `swag`, `giftCards`, or `donations` to retrieve those catalogs instead.
#### Always order by Variant - not Product
Products are the display-level entity. Variants are the orderable units. When placing an order, always use the `variantId`, not the `productId`. Retrieve a product's variants using `GET /v3/products/{productId}/variants`.
#### Expanding products with `include` and `fields`
V3 uses JSON:API conventions to keep responses lean by default and expand only what you need:
* **`include`** - return related entities as full objects. Supported values: `brand`, `tags`.
* **`fields`** - return additional computed/expanded fields. On the list endpoint: `priceRange`, `variantsCount`. On the single-product endpoint: `options`, `priceRange`, `variantsCount`.
Combine them in a single request:
```text theme={null} theme={null}
GET /v3/products?include=brand,tags&fields=priceRange,variantsCount
```
#### Filtering
V3 follows JSON:API filtering conventions on the list endpoint:
* `filter[catalog]` - `marketplace`, `swag`, `giftCards`, or `donations` (defaults to `marketplace`)
* `filter[search]` - free-text search across title, category, and brand
* `filter[brandId]` - comma-separated list of brand IDs (OR semantics)
* `filter[brandName]` - case-insensitive substring search on brand name
* `filter[tagId]` - comma-separated list of tag IDs (OR semantics)
* `filter[price][gte]` / `filter[price][lte]` - inclusive price range
Multiple `filter[...]` expressions are ANDed. Unknown filter keys return `400`.
#### Pagination and sorting
The list endpoint uses **cursor pagination** (`page[cursor]`, `page[size]`, max 300, default 100). The variants-by-product endpoint uses **page-number pagination** (`page[number]`, `page[size]`). See [Request & Response Standards](/pages/request-response-standards) for the full pagination contract.
Sortable fields on the list endpoint: `minPrice`, `createdAt`. Prefix with `-` for descending order.
#### Location scoping
Pass `location` (ISO 3166-1 alpha-2 country code) to scope pricing and availability to a specific recipient country. Defaults to `US`.
#### Account scoping
The optional `Snappy-Account-Id` header scopes queries to a specific Account - used primarily for swag validation and filtering.
#### Permissions
All V3 Products endpoints require the `products:read` scope.
***
## How to Work with Products
**List products**
```text theme={null} theme={null}
GET /v3/products
```
Returns a paginated list of products across all collections. Filter by catalog, type, brand, tag, price, or free-text search.
**Get a single product**
```text theme={null} theme={null}
GET /v3/products/{productId}
```
Returns product-level fields only. Use the variants endpoint below to retrieve the product's variants.
**List variants for a product**
```text theme={null} theme={null}
GET /v3/products/{productId}/variants
```
Returns the product's variants as a paginated list. Use `fields=pricing` to include variant `price` and `priceBreakdown`, and `fields=details` to expand structured details.
**List product tags**
```text theme={null} theme={null}
GET /v3/product-tags
```
Returns a paginated list of available product tags. Use `title` to filter by name (minimum 3 characters).
***
Looking for **branded swag templates** instead of marketplace products? See the [Swag](/modules/api/v3/base-products/overview) page.
# Get variant availability
Source: https://docs.snappy.com/modules/api/v3/variants/get-variant-availability
get /v3/variants/{variantId}/availability
Use this endpoint to retrieve a variant's availability across countries. Returns an availability map keyed by ISO 3166-1 alpha-2 country code (e.g., "US", "DE").
###### Each entry includes:
- `isAvailable` - whether the variant ships to that country
- `price` - variant price for that country (null if not available)
- `priceBreakdown` - full pricing detail (null if not available)
###### Please note
- Country codes not present in the response should be treated as `isAvailable: false` - only supported countries are returned.
- Unlike other variant endpoints, this endpoint does **not** accept a `location` parameter. It returns availability for every supported country in a single response.
#### Permissions
- Requires: `products:read`
# Get variant by ID
Source: https://docs.snappy.com/modules/api/v3/variants/get-variant-by-id
get /v3/variants/{variantId}
Use this endpoint to retrieve a single product variant by its ID. Returns the same `Variant` object shape as items in the Get product variants list response.
#### Optional parameters
- `fields=pricing` - include `price` and `priceBreakdown`
- `fields=details` - include the `details` wrapper
- `include=brand` - include the variant's `brand` object
#### Please note
When `fields` is omitted, `price`, `priceBreakdown`, and `details` are not returned in the public response.
#### Permissions
- Requires: `products:read`
# Variants API: The Orderable Units of the Catalog
Source: https://docs.snappy.com/modules/api/v3/variants/overview
Retrieve product variants, check per-country availability and pricing, and access the variant-level details required to place an order.
A **Variant** is the orderable unit in the Snappy catalog - the specific version of a Product with concrete pricing, size, color, or other distinguishing attributes. When placing an order, you always specify a `variantId` (never just a `productId`).
The V3 Variants API lets you retrieve individual variants by ID and check their availability across countries.
Want to understand how **Variants** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page.
To list all variants for a specific product, use [`GET /v3/products/{productId}/variants`](/modules/api/v3/products/get-product-variants) - this is part of the [Products API](/modules/api/v3/products/overview) since it's scoped to a product.
***
## The Variant Object
| Field | Type | Description |
| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Unique identifier of the variant - required when placing an order |
| `title` | string | Display name of the variant |
| `productId` | string | Parent product identifier |
| `selectedOptions` | object | Key-value option values for this variant (e.g. `{"color": "Black", "size": "M"}`) |
| `taxable` | boolean | Whether the variant is taxable |
| `media` | array | Variant-specific media items |
| `personalization` | object | Personalization metadata (whether personalization is supported, available template fields). Nullable when unavailable. |
| `descriptionHtml` | string | Full narrative HTML description |
| `price` | object | Variant price in `{amount, currency}`. Returned only when `fields` includes `pricing` (or `price`). |
| `priceBreakdown` | object | Detailed pricing breakdown. Returned only when `fields` includes `pricing` (or `priceBreakdown`). |
| `details` | object | Structured details wrapper (description, features, specifications). Returned only when `fields` includes `details`. |
| `brand` | object | Returned only when `include=brand`. Nullable. |
***
## Key Concepts & Business Rules
#### Default response is lean
By default, variant responses omit `price`, `priceBreakdown`, and `details`. Use `fields` to request the gated fields when you need them:
* `fields=pricing` - adds `price`, `priceBreakdown`
* `fields=details` - adds the structured `details` wrapper
This keeps responses small for browse and search use cases while letting checkout flows pull the full data they need.
#### Pricing is per country
Variant pricing depends on the recipient's shipping country. Two ways to retrieve it:
* **For a specific country** - use `GET /v3/variants/{variantId}` with `location` (default `US`) and `fields=pricing`
* **Across all countries** - use [`GET /v3/variants/{variantId}/availability`](#get-variant-availability) to retrieve the full per-country availability map
#### Personalization data
The `personalization` object indicates whether the variant supports custom personalization (e.g. a printed name or message) and what template fields are configurable. It is `null` when personalization isn't available for the variant.
#### Availability vs pricing
The availability endpoint returns a map of countries the variant ships to, with country-specific pricing inline. Country codes **not present** in the response should be treated as `isAvailable: false` - only supported countries are returned.
For per-recipient validation, the typical pattern is: check availability once via `GET /v3/variants/{variantId}/availability`, then place an order with the appropriate `location` parameter on `POST /v3/orders`.
#### Account scoping
The optional `Snappy-Account-Id` header scopes queries to a specific Account - primarily for swag validation and filtering.
#### Permissions
All V3 Variants endpoints require the `products:read` scope. (The Variants API shares the `products:read` scope with the Products API since they're part of the same catalog domain.)
***
## How to Work with Variants
**Get a single variant**
```text theme={null} theme={null}
GET /v3/variants/{variantId}
```
Returns a single variant by its ID. Use `fields` to expand pricing and details, and `include=brand` to inline the brand object.
**Get variant availability**
```text theme={null} theme={null}
GET /v3/variants/{variantId}/availability
```
Returns an availability map keyed by ISO 3166-1 alpha-2 country code. Each entry indicates whether the variant ships to that country and includes the country-specific pricing.
***
Looking for **swag base variants** (variations of branded merchandise templates) instead of marketplace variants? See the [Swag](/modules/api/v3/base-products/overview) page.
# Snappy API Authentication: API Keys, Scopes & mTLS
Source: https://docs.snappy.com/pages/authentication-and-security
Generate and rotate scoped API keys, manage granular permissions, scope requests by account or company, and configure Mutual TLS for enterprise integrations.
Snappy APIs are authenticated using scoped API keys passed in the `X-Api-Key` header. OAuth2 is not used. Enterprise customers may optionally enable Mutual TLS (mTLS) for additional network-level security.
**The same API key works for both V2 and V3.** Pick the API version that matches the endpoint path (`/v2/...` vs `/v3/...`) - the authentication header is identical.
***
## Authentication at a glance
| Surface | Auth mode | Header | Notes |
| :----------------------------------------------------------------------------------------------- | :--------------------------- | :------------------------ | :--------------------------------------------------------------------------------------------------- |
| V3 APIs (Products, Variants, Collections, Orders, Accounts, Billing Methods, Swag, Export, etc.) | API key | `X-Api-Key: YOUR_API_KEY` | Same key as V2. |
| V2 APIs | API key | `X-Api-Key: YOUR_API_KEY` | Same key as V3. |
| API Keys Management (`/v2/authentication/apiKeys`, `/v3/authentication/api-keys`) | API key | `X-Api-Key: YOUR_API_KEY` | Manage keys programmatically - see [Managing API Keys](#managing-api-keys). |
| Enterprise mTLS (optional) | API key + client certificate | `X-Api-Key` + TLS cert | Use the mTLS base URL - see [Enterprise Security: Mutual TLS](#enterprise-security-mutual-tls-mtls). |
***
## Authenticating Requests
To communicate with the Snappy API, you need an API key. You must include it in the header of **every** request using the `X-Api-Key` header:
```text theme={null}
X-Api-Key: YOUR_API_KEY
```
**Example request against a V3 endpoint:**
```bash theme={null}
curl --request GET \
--url https://api.snappy.com/public-api/v3/accounts \
--header 'X-Api-Key: YOUR_API_KEY' \
--header 'accept: application/json'
```
**The same key against a V2 endpoint:**
```bash theme={null}
curl --request GET \
--url https://api.snappy.com/public-api/v2/accounts \
--header 'X-Api-Key: YOUR_API_KEY' \
--header 'accept: application/json'
```
Create your first API key in the Snappy dashboard on the **Sharing & Access** page. After that you can manage keys in the dashboard or programmatically via the [**API Keys endpoints**](/modules/api/v3/api-keys/overview) - see [Managing API Keys](#managing-api-keys) below.
### Base URLs
| Purpose | Base URL |
| :----------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| Standard | `https://api.snappy.com/public-api` |
| mTLS (enterprise) | `https://mtls-api.snappy.com/public-api` |
| Append `/v2/...` or `/v3/...` to address the version you want. Both versions are served from both base URLs. | |
***
## Optional Scoping Header (V3)
V3 endpoints accept an optional header that narrows a request to a specific sub-entity inside your organization. It is not required for getting started - omit it and the request runs against the full org reachable by the API key.
| Header | Description |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
| `Snappy-Account-Id` | Scope the request to a specific Account within your Company. |
| Header names are RFC 6648 compliant - no `X-` prefix. Headers themselves are case-insensitive in transit; the casing shown here is the canonical documentation form. | |
A small number of V3 endpoints - currently the Collections list and by-ID endpoints - **require** `Snappy-Account-Id` because the response depends on account-level visibility. Each endpoint's reference page notes when this applies.
***
## Managing API Keys
There are two ways to manage your Company's API keys:
* **Dashboard (UI)** - create and manage keys on the **Sharing & Access** page under Company Settings. This is where you create your first key.
* **API** - list, create, and delete keys programmatically by authenticating with an existing API key (`X-Api-Key`). A key can only create or revoke other keys with permissions equal to or more restrictive than its own, which prevents privilege escalation.
See [API Keys V3](/modules/api/v3/api-keys/overview) for the full management reference.
***
## API Permissions & Key Management
### Overview
To help you meet modern enterprise security standards and enforce the **Principle of Least Privilege**, Snappy uses scoped API keys. You can restrict exactly what each key is allowed to do, minimizing security risks.
When generating a key in the dashboard, you can assign specific permissions based on the integration's exact needs:
* **Read-only access:** Allow an integration to retrieve data (gift statuses, catalog items, order tracking) without the ability to spend budget or place orders.
* **Full access:** Allow an integration to create orders, manage recipients, and run campaigns.
* **Account-level scoping:** Restrict a key so it can only operate within a specific sub-account rather than your entire Company.
Each environment (Testing and Production) has its own set of API keys. Never use a Production key in your test environment or vice versa.
Always assign the minimum required permissions necessary for your integration to function.
### Available Scopes
Scopes are common across V2 and V3 - assigning `products:read` to a key, for example, grants access to both `/v2/products` and `/v3/products` endpoints. The table below lists every available scope and which endpoints it unlocks.
| Permission | Scope | Description | Notes |
| :----------------------------- | :--------------------------------------------------- | :---------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
| **Create & Update Gifts** | `gifts:create`, `gifts:update` | Create or update gifts for recipients and notify them. | V2 Gifts API. V3 uses Orders instead - see `orders:*`. |
| **Read Gifts** | `gifts:read:masked`, `gifts:read:unmasked` | Retrieve gift information, including recipients. | Sensitive Information. V2 only. |
| **Create a Demo Gift** | `gifts:create:demo` | Create a demo gift that can be shared. | `POST /v2/gifts/demo`. V2 only. |
| **Create an Order** | `orders:create` | Place an order. | Billable action. Covers V2 `POST /orders` and V3 `POST /v3/orders`. |
| **Read Orders** | `orders:read:masked`, `orders:read:unmasked` | Retrieve order information, including recipient and delivery details. | Sensitive Information. V2 and V3. |
| **Cancel Order** | `orders:cancel` | Cancel orders that have not yet been picked up by fulfillment. | V2 and V3. |
| **Create & Update Campaigns** | `campaigns:create`, `campaigns:update` | Create and update Campaigns (a template for sending gifts). | V2 only. |
| **Read Campaigns** | `campaigns:read` | Retrieve Campaigns with filtering and pagination. | V2 only. |
| **Read Collections** | `collections:read` | Retrieve Collections and their budget ranges. | V2: list + budgets. V3: list and by-ID Collection resources. Products within a Collection require `products:read`. |
| **Read Products** | `products:read` | Read products, variants, brands, and tags. | Covers V2 Products & Variants, V3 Products & Variants, and V3 Base Products (Swag catalog). |
| **Create Recipients** | `recipients:create` | Add new recipients to the account roster. | `POST /v2/recipients`. |
| **Update / Delete Recipients** | `recipients:update`, `recipients:delete` | Update, override, or delete recipients in the account roster. | |
| **Read Recipients** | `recipients:read:masked`, `recipients:read:unmasked` | Retrieve recipient information. | Sensitive Information. |
| **Create Account** | `accounts:create` | Create accounts. | V2 and V3. |
| **Read Account** | `accounts:read` | Retrieve account information. | V2 and V3. |
| **Read Billing Methods** | `billingMethods:read` | Retrieve Billing Method details: remaining balance, status, expiration. | V3 only - introduced with the V3 Billing Methods API. |
### Data Privacy & PII Masking
To protect employee and recipient privacy, Snappy masks Personally Identifiable Information (PII) in API responses by default.
If a key does not have explicit permission to view sensitive data, fields are returned partially redacted. For example:
* **Email:** `j*******@e*****.com`
* **Name:** `J*** D***`
* **Phone:** `(***) ***-1234`
* **IDs:** `3****`
To retrieve unmasked data, toggle the **"Expose Sensitive Information"** setting when generating the API key in the dashboard. This determines whether reads against PII-bearing endpoints resolve to the `:read:masked` or `:read:unmasked` permission tier.
### Creating an API Key
Snappy supports up to **100 active API keys** per Company.
1. Log in to your Snappy Dashboard at [https://login.snappy.com/login](https://login.snappy.com/login).
2. Navigate to **Sharing & Access** under **Company Settings** ([https://login.snappy.com/company-settings/general](https://login.snappy.com/company-settings/general)).
3. Scroll to **API Access** and enable API access for your organization if not already enabled.
4. Click **Generate Key**.
5. **Name your key.**
6. **Set Expiration:** Select your key rotation policy (keys can be set to expire in up to one year).
7. Check the mTLS checkbox if you are an Enterprise customer using enhanced network security.
8. **Assign Permissions:** Select the specific scopes this key will have access to.
9. **Configure Privacy & Security:** Toggle sensitive information access on or off depending on your PII requirements.
10. Click **Generate Key**.
11. **Copy the key immediately.** For security reasons, the secret key is never displayed again.
### Rotating a Key
To reset a compromised key or comply with your company's security policies, rotate keys without integration downtime:
1. Follow the **Creating an API Key** steps above to generate a new scoped key.
2. Update your application's environment variables with the new key.
3. Verify the new key is working in production.
4. Delete the old key from the Snappy Dashboard.
This pattern lets you cut over with zero downtime - both keys remain valid until you delete the old one.
### The API Key Object
When you retrieve your API keys via the management endpoint, each key is returned as an object with the following fields:
| Field | Type | Description |
| :--------------- | :---------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Unique identifier for the API key. |
| `name` | string | Display name assigned to the key at creation. |
| `expirationDate` | string (ISO 8601) | When the key will expire. |
| `enforceMtls` | boolean | Whether Mutual TLS is enforced for requests using this key. See [Enterprise Security: Mutual TLS](#enterprise-security-mutual-tls-mtls). |
| `createdAt` | string (ISO 8601) | When the key was created. |
For security reasons, the secret key value itself is returned **only once** - at the moment of creation. It is never included in subsequent `GET` responses. If you lose your key, you'll need to rotate it.
***
## Enterprise Security: Mutual TLS (mTLS)
For environments requiring strict network security (such as financial institutions or highly regulated microservices), Snappy offers Mutual TLS (mTLS).
In a standard API request, the client verifies the server's identity. With mTLS, the authentication goes both ways: Snappy verifies the client's SSL certificate, and the client verifies Snappy's SSL certificate. This guarantees a secure, encrypted communication channel and actively prevents man-in-the-middle attacks.
### mTLS Base URL
mTLS requests go to a dedicated base URL with a separate certificate-validating endpoint:
```text theme={null}
https://mtls-api.snappy.com/public-api
```
All V2 and V3 endpoints are accessible at this base URL - append `/v2/...` or `/v3/...` as you would on the standard URL.
### Setting up mTLS
1. Contact your Snappy account representative to request mTLS provisioning for your organization.
2. Snappy issues your client SSL certificate.
3. When generating an API key in the dashboard, check the **mTLS** checkbox to enforce mutual authentication for requests using that key.
4. Configure your HTTP client to present the issued certificate when making requests to `mtls-api.snappy.com`.
Static API keys cover all standard integrations. Enable mTLS in addition to your API key only if your security policy requires certificate-based mutual authentication.
# Before You Begin: Prerequisites for the Snappy API
Source: https://docs.snappy.com/pages/before-you-begin
Account access, API key generation and Testing vs. Production environments - everything you need before integrating Snappy.
Before making your first API call, you'll need a Snappy environment set up for your organization. This will be done by our team in order to make sure your gifting programs are properly structured from day one.
## How to Get Set Up
Reach out to your Snappy account manager or [contact us](https://www.snappy.com/book-meeting) to request API access.
Our team will provision two separate Companies for your organization:
* **Testing Company** - a demo environment with a test Account. Use this to build and test your integration safely without affecting real recipients or budgets.
* **Production Company** - your live environment, used once your integration is ready to go live.
Always make sure you are using the correct Company and API key for your environment. Test API keys and production API keys are not interchangeable.
Our team will assist in setting up your billing method as part of the onboarding process.
Once your environments are ready, you'll receive access to the Snappy Dashboard at [https://login.snappy.com/workspace](https://login.snappy.com/workspace). Each Company has its own Dashboard - make sure you're logged into the correct one when generating API keys.
In the Dashboard, navigate to **Company Settings → Sharing & Access → API Access** and generate your API keys. You'll need a separate key for each environment - one for Testing and one for Production.
You're now ready to make your first API call. Head straight to the [Quickstart guide](/pages/quickstart) to make your first API call.
# Changelog
Source: https://docs.snappy.com/pages/changelog
Recent updates, new features, and breaking changes. Track API versions and migrate between releases.
All notable changes to the Snappy API are documented here. We follow Semantic Versioning.
**Breaking changes are marked with ⚠️.** We recommend subscribing to release notifications to stay informed of upcoming changes.
***
## ⚠️ v3.1 - July 2026
| Endpoint | Changes |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v3/accounts` | Removed `companyId`, `createdAt`, `updatedAt`, and `full` from `fields`. Response now only returns `id` and `name`. |
| `GET /v3/accounts/{accountId}` | Response now only returns `id` and `name`, removing `companyId`, `createdAt`, and `updatedAt`. |
| `POST /v3/accounts` | `billingMethod` is now optional. When present, all billing method fields are required. Renamed `billingMethod.amount` to `billingMethod.spendingLimit.amount`. Response now only returns `id` and `name`. |
| `GET /v3/authentication/api-keys` | Removed `filter[accountId]` query parameter and related filtering/link logic. |
| `POST /v3/orders/addresses/validate` | Request body now matches create-order address shape: `address.address1`, `address.address2`, `address.city`, `address.provinceCode`, `address.postalCode`, `address.countryCode`. Removed top-level `country`. |
| `GET /v3/orders/addresses/autocomplete` | `filter[country]` is required and normalized to uppercase two-letter country code. Response now matches create-order address shape: `address1`, `address2`, `city`, `provinceCode`, `postalCode`, `countryCode`. |
| `GET /v3/product-tags` | Renamed query parameter `title` to `filter[name]`. Pagination links now preserve `filter[name]`. |
## v3.0 - June 2026
V3 introduces a parallel API line for Marketplace, Orders, and Export, alongside refreshed Billing Methods and Accounts surfaces. **V2 is not deprecated** - V3 lives alongside V2 at `https://api.snappy.com/public-api/v3` and uses the same `X-Api-Key` authentication. Pick the version that matches your endpoint path; the API key works for both.
### 🆕 New APIs
* **V3 Orders** - single-call order placement replaces the old Campaign → Gift → Order chain. Orders are first-class resources, retrievable, listable, and cancellable independent of Gifts. See [Orders V3 Overview](/modules/api/v3/orders/overview).
* **V3 Marketplace** - Products, Variants, and Collections with **85% lower catalog latency**, **static product and variant IDs** that no longer change with real-time availability, default sorting by popularity, and enhanced semantic search. See [Products V3 Overview](/modules/api/v3/products/overview) and [Variants V3 Overview](/modules/api/v3/variants/overview).
* **V3 Swag (Base Products)** - branded swag templates and base variants exposed via the public API, replacing the standalone Covver integration. See [Swag](/pages/swag-overview).
* **V3 Export** - asynchronous, NDJSON-based bulk catalog export for partners maintaining a local product mirror. Pair with `stock-availability-updates` webhooks for incremental refresh. See [Export API](/modules/api/v3/exports/overview).
* **V3 Billing Methods** - retrieve funding sources, check remaining balance, and view expiration. See [Billing Methods Overview](/pages/billing-methods).
* **V3 Accounts** - list, retrieve, and create sub-accounts under your Company. See [Accounts V3 Overview](/modules/api/v3/accounts/overview).
* **V3 API Keys Management** - programmatically create, rotate, and revoke API keys using an existing `X-Api-Key`. See [API Keys V3](/modules/api/v3/api-keys/overview).
* **Product Recommendations** - new endpoint surfaces related products to drive engagement.
### 🔄 New conventions in V3
* **JSON:API-style query syntax** - `filter[field]`, `include`, `fields`, `sort`, and `page[number]` / `page[size]` pagination (cursor pagination on product list endpoints). See [Request & Response Standards](/pages/request-response-standards).
* **Standardized error envelope** - every error response returns `{ message, errorCode, errors[] }` with structured `{status}_{DOMAIN}_{sequence}` error codes. See [Request & Response Standards](/pages/request-response-standards).
* **camelCase field names** across all V3 endpoints, replacing the mixed casing in V2.
* **New scoping header** - `Snappy-Account-Id` narrows a request to a specific sub-entity (RFC 6648 compliant; no `X-` prefix). See [Authentication & Security](/pages/authentication-and-security).
* **PII masking via explicit scopes** - `:read:masked` returns masked PII; `:read:unmasked` returns full PII. See [Authentication & Security](/pages/authentication-and-security).
### ⚠️ Migration notes
* **V2 remains fully supported.** No deprecation timeline. V3 is purely additive - the version is in the URL path (`/v2/...` vs `/v3/...`), and the same `X-Api-Key` authenticates both.
* **The Order entity is now independent of the Gift entity.** In V2, orders are retrieved via their parent Gift. In V3:
* Orders are accessible directly via `/v3/orders`.
* Order-level webhooks fire alongside gift-level webhooks for V3 orders. See [Webhook Event Types](/pages/webhook-event-types).
* **Pagination conventions differ between V2 and V3.** V2 uses `skip` / `limit`; V3 uses `page[number]` / `page[size]` on most endpoints, with cursor pagination on product list endpoints.
***
## v2.0 - February 2025
### 🆕 New
* **Granular API Permissions** - API keys can now be scoped to specific endpoints and actions. See [Authentication & Security](/pages/authentication-and-security).
* **PII Masking** - Personally Identifiable Information is now masked by default in API responses. Keys must explicitly enable sensitive data access. See [Authentication & Security](/pages/authentication-and-security).
### 🔄 Changed
* Base URL updated to `https://api.snappy.com/public-api/v2`.
* Pagination now uses `skip` and `limit` parameters. See [Request & Response Standards](/pages/request-response-standards).
# Duplicate Gift Detection
Source: https://docs.snappy.com/pages/duplicate-gifts-detection
Prevent accidental double-billing with idempotency keys. How to assign keys, reuse them safely, and handle conflict responses.
To protect the recipient experience and maintain billing accuracy, Snappy uses a unique `key` system to prevent redundant gift deliveries. This idempotency system ensures that your integration remains reliable even in the event of network timeouts, automated retries, or accidental double-clicks.
Including a unique key is not mandatory, but it is **strongly recommended** for all production integrations to ensure billing accuracy and a seamless recipient experience.
### How it Works
When you include a `key` in your gift request, Snappy checks if that specific key has been used before.
* **If it's a new key:** We process the gift as usual.
* **If the key exists:** We reject the duplicate and return a specific error, ensuring no additional gift is sent or billed.
### Implementing Unique Keys
We recommend generating a unique key for every gift intent. You can use two primary strategies:
1. **UUID (Recommended):** Generate a random version 4 UUID for every gift object.
2. **Deterministic Logic:** Create a string based on your internal business rules (e.g., `user_123_anniversary_2024`). This is perfect for ensuring a recipient only receives **one** gift for a specific event.
**Keys do not expire with their associated gift.** Even if a gift expires without being claimed, its key remains permanently reserved in the system. Reusing an expired gift's key for a new send will trigger a duplicate detection error. Always generate a fresh unique key for every new gift intent, regardless of the outcome of previous sends.
### Usage Example
Add the `key` field to the individual recipient objects in your payload:
```json theme={null}
{
"campaignId": "cmp_12345",
"recipients": [
{
"firstname": "John",
"lastname": "Doe",
"email": "john@example.com",
"key": "unique-uuid-string-001"
},
{
"firstname": "Jane",
"lastname": "Doe",
"email": "jane@example.com",
"key": "unique-uuid-string-002"
}
]
}
```
### Handling Errors & Partial Success
If a duplicate key is detected, the API will return `errorCode: 41008`.
**Partial Success Scenarios**
Because Snappy processes gift batches, a single request may result in a "Partial Success." This happens if some keys in your list are new while others are duplicates.
**Sample Partial Success Response:**
```json theme={null}
{
"results": [
{
"success": false,
"message": "Duplicate gift detected based on the provided key.",
"errorCode": 41008
},
{
"success": true,
"id": "gft_789abc",
"link": "https://gift.snappy.com/gft_789abc"
}
],
"message": "1 gift out of 2 sent successfully."
}
```
# Error Handling: Snappy API Error Codes & Recovery
Source: https://docs.snappy.com/pages/error-handling
HTTP status codes, error payload shape, idempotency, and recommended retry patterns for the Snappy gifting API.
Snappy uses standard HTTP response codes to indicate the success or failure of an API request. If a request fails, we return a descriptive error object to help you identify and resolve the issue. **The error shape is the same in V2 and V3.**
***
## The Error Object
Every error response follows a consistent JSON structure. The structure differs slightly between validation errors (HTTP `400`) and all other errors.
**Validation errors (HTTP 400):**
```json theme={null}
{
"path": "recipients[0].email",
"errorCode": "INVALID_REQUEST",
"message": "Invalid email format."
}
```
**All other errors (401, 403, 404, 409, 422, 5xx):**
```json theme={null}
{
"status": 401,
"errorCode": "UNAUTHORIZED",
"message": "Authorization key is invalid."
}
```
| Field | Type | Description |
| :---------- | :----- | :--------------------------------------------------------------------------------------------- |
| `path` | string | *(400 only)* Dot-separated path to the request parameter or body field that failed validation. |
| `status` | number | *(non-400 only)* The HTTP status code. |
| `errorCode` | string | Granular code identifying the specific business-logic error. |
| `message` | string | Human-readable description of the error. |
**Error codes are stable; changing a code is considered a breaking change.** Error messages, however, may be updated over time to provide better clarity. Your integration should switch on `errorCode` for programmatic handling - never on the `message` text.
### V3 error code anatomy
V3 endpoints use structured error codes in the format `{STATUS}_{DOMAIN}_{SEQUENCE}`:
| Segment | Description | Example |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- | :-------------------------------------------------------------- |
| `{STATUS}` | The HTTP status code | `404` |
| `{DOMAIN}` | Short uppercase domain tag | `PROD` (products), `ORDS` (orders), `PBLC` (public-API generic) |
| `{SEQUENCE}` | 3-digit sequence number scoped to that domain | `001` |
| Example V3 codes: `404_PROD_001`, `400_PBLC_001`, `422_ORDS_003`. | | |
| V2 endpoints use shorter symbolic codes (e.g., `INVALID_REQUEST`, `NOT_FOUND`, `UNAUTHORIZED`). Both formats are stable; switch on the full string regardless of format. | | |
***
## HTTP Status Codes
### 400 – Bad Request
The server could not process the request, usually due to a syntax error in the URL or the JSON body.
**How to fix**
* Verify your request body matches the expected schema.
* Ensure all required fields are present (e.g. `variantId`, `recipient.email`, `idempotencyKey`).
* Check that field values are the correct type (e.g. strings vs. integers).
* Inspect `path` in the response body - it points to the specific field that failed validation.
### 401 – Unauthorized
There is an issue with your API key credentials.
**How to fix**
* **Missing header.** Ensure you are passing the `X-Api-Key` header on every request.
* **Invalid key.** Double-check that the key hasn't been mistyped or deleted from the Dashboard.
* **Expired key.** Check whether the key has reached its expiration date and rotate it if needed. See [Authentication & Security](/pages/authentication-and-security).
### 403 – Forbidden
Your key is valid but does not have permission to access the requested resource.
**How to fix**
* **Wrong scope.** Check that your API key was created with the correct scope for this action (e.g. `orders:create` to place an order, `products:read` to read the catalog). See [Authentication & Security](/pages/authentication-and-security).
* **Wrong environment.** Make sure you are not using a Testing API key against a Production resource or vice versa. See [Before You Begin](/pages/before-you-begin).
* **Insufficient masking scope.** Reading PII fields requires `:read:unmasked` rather than `:read:masked`. See [PII Masking](/pages/authentication-and-security#data-privacy--pii-masking).
### 404 – Not Found
The requested resource does not exist, or it exists but is not visible to the calling Account.
**How to fix**
* Double-check the ID in your request path - copy it directly from a previous API response to avoid typos.
* Verify the resource was successfully created before attempting to retrieve or update it.
* If you're scoping with `Snappy-Account-Id`, verify the resource belongs to that account.
Snappy intentionally returns `404` (rather than `403`) in some cases where a resource exists but is not visible to your key - for example, a Collection belonging to a different Account. This prevents exposing the existence of resources your key shouldn't know about. If you're confident the resource exists and you should have access, check your key's scope and account scoping headers.
### 422 – Unprocessable Entity
The request was syntactically valid, but Snappy can't fulfill it due to a business rule or current resource state.
**How to fix**
* Read the `errorCode` and `message` to understand the specific rule that was violated.
* Common 422 scenarios on V3:
* **Order already in transit** - `POST /v3/orders/{orderId}/cancel` returns `422` once the fulfillment partner has picked up the shipment.
* **Variant not shippable to country** - `POST /v3/orders` returns `422` if the shipping address country is outside the variant's supported locations.
* **Insufficient billing balance** - `POST /v3/orders` returns `422` when the Billing Method has insufficient funds.
* **Idempotency conflict** - replaying an `idempotencyKey` with a *different* request body returns `422`. Use the same key only with identical bodies.
* Re-issue the request once the underlying state changes (e.g., refunded balance, address updated, order cancelled in time).
### 429 – Too Many Requests
Your integration has exceeded the API rate limit.
**How to fix**
* Pause your requests immediately.
* Respect the `Retry-After` response header - it tells you how long to wait before retrying.
* Implement an exponential backoff strategy - wait a short period before retrying, and increase the wait time with each subsequent retry.
* Review your request frequency against the published [Rate Limits](/pages/rate-limits).
### 500 – Internal Server Error
Something went wrong on Snappy's end.
**How to fix**
* Check the [Snappy Status Page](https://status.snappy.com/) to see if there is an ongoing incident.
* Retry once with a short delay - many `500`s are transient.
* If no incident is reported and the issue persists, contact support with:
* Request URL and method
* The `errorCode` returned in the response body
* A timestamp of when the error occurred
* The `idempotencyKey` you used (for writes), so support can correlate without risking a duplicate
### 502 – Bad Gateway
An upstream service returned an invalid response while processing your request. Almost always transient.
**How to fix**
* Retry with exponential backoff. For writes, only retry if you supplied an `idempotencyKey` - otherwise you risk duplicate side effects.
### 503 – Service Unavailable
The service is temporarily unavailable, typically during a deployment or under unusual load.
**How to fix**
* Retry with exponential backoff. Respect the `Retry-After` header if present.
* If the issue persists across several minutes, check the [Snappy Status Page](https://status.snappy.com/).
### 504 – Gateway Timeout
The request took too long to process and timed out.
**How to fix**
* Retry with a short delay; most `504`s are transient.
* For writes, only retry if you supplied an `idempotencyKey` (the original request may have actually succeeded before the timeout - the idempotency key lets you safely re-issue without risking a duplicate).
* If a specific endpoint times out consistently, contact support with the request details.
***
## Retry & Idempotency
Not all errors are safe to retry. The matrix below summarizes when retrying is the right move.
| Status | Safe to retry? | Strategy |
| :-------------------- | :----------------- | :--------------------------------------------------------------------- |
| `400` | No | Fix the request and re-issue. |
| `401` | No | Fix credentials. |
| `403` | No | Fix scope or environment. |
| `404` | No | Fix the resource ID or scoping. |
| `422` | Conditionally | Only after the underlying state changes (balance, availability, etc.). |
| `429` | Yes | Exponential backoff; respect `Retry-After`. |
| `500` | Yes (with caution) | Backoff; for writes, only retry with an idempotency key. |
| `502` / `503` / `504` | Yes | Backoff; for writes, only retry with an idempotency key. |
### Idempotency keys make retries safe
Many Snappy write endpoints accept an idempotency key. Sending the same key twice returns the *same* result - no duplicate side effect, no double-charge, no second gift sent.
| Endpoint | Idempotency field | Notes |
| :--------------------- | :----------------------------------- | :---------------------------------------------------- |
| `POST /v3/orders` | `idempotencyKey` in the request body | Required. 1–120 characters. Stable per logical order. |
| `POST /v2/orders` | `recipient.key` in the request body | Same semantics, different field location. |
| **Rules of the road:** | | |
* **Generate one stable key per logical operation** - e.g., `user-{userId}-order-{cartId}`. Reuse it on every retry of that operation.
* **Never generate a fresh random key on each retry** - that defeats the protection and will result in duplicates.
* **Replaying with a different body returns `422`** (idempotency conflict). Either reuse the original body or pick a new key.
* See [Duplicate Detection](/pages/duplicate-gifts-detection) for the full reference.
### Exponential backoff
For all retryable error codes, use exponential backoff with jitter:
1. First retry: wait \~1 second.
2. Each subsequent retry: double the wait, with random jitter.
3. Cap at \~60 seconds between retries.
4. Give up after 5–7 attempts unless the operation is critical and idempotent.
Always respect a `Retry-After` header if the response includes one - it overrides your local backoff calculation.
# Snappy + LogicBroker Integration Guide
Source: https://docs.snappy.com/pages/logicbroker-integration
Connect Snappy to LogicBroker for advanced order routing and supplier networks. Setup, mapping, and troubleshooting.
This integration is coming soon.
Snappy is building a native integration with LogicBroker, a connected commerce platform that enables seamless drop-ship and marketplace operations. Once available, this integration will allow businesses using LogicBroker to incorporate Snappy's gifting capabilities directly into their commerce workflows - without custom API development.
## What to Expect
The LogicBroker integration will enable you to:
* Trigger gift sends directly from LogicBroker order and fulfillment events
* Sync recipient and order data between platforms automatically
## Get Notified
If you're interested in early access or want to learn more, contact your Snappy account manager or [reach out](https://www.snappy.com/book-meeting).
# Embedded Marketplace with Snappy as the Fulfillment Engine
Source: https://docs.snappy.com/pages/marketplace-overview
Bring Snappy's curated catalog into your own platform. Your users browse and select; your system places orders directly through the API. Build rewards marketplaces, swag stores, and procurement portals.
*Your platform displays the catalog and collects the order, Snappy handles the fulfillment.*
Bring Snappy's curated catalog of gifts and swag into your own platform. Build a dedicated gifting store from scratch, or seamlessly add Snappy's global products to your existing marketplace. You control the UI - whether it's a new redemption center or an existing e-commerce shop - and Snappy acts as the invisible fulfillment engine.
Want recipients to choose their own gift from a Snappy-hosted claim page instead? See [Triggered Gifting](/pages/triggered-gifting).
***
## Common Use Cases
### **Embedded Rewards Marketplace**
Allow employees or customers to redeem loyalty points for physical products directly within your own portal. You control the branding, point values, and display; Snappy handles the logistics and shipping.
### **Procurement & Swag Stores**
Build an internal company store where office managers can order branded swag or equipment for their teams directly, with instant order placement.
***
## How it works
Pull product data via the [V3 Catalog API](/modules/api/v3/products/overview) to display items in your own UI. There are two integration patterns - pick the one that fits your traffic and UX:
* **Real-time queries** - hit `GET /v3/products`, `GET /v3/variants`, and `GET /v3/collections/{collectionId}/products` on demand to browse, filter, and paginate directly against Snappy. Best for low-volume integrations or browse-as-you-go experiences where the catalog is rendered fresh per request.
* **Bulk catalog mirror (async export)** - kick off a background job via `POST /v3/products/exports` (or `POST /v3/collections/exports` for a single collection), poll `GET /v3/products/exports/{exportId}` for completion, and download the full result as a single NDJSON file. Best for high-volume integrations, local search and filtering, or partners maintaining their own product database. See the [Export API](/modules/api/v3/exports/overview) for the full reference.
**Recommended pattern for production partners:** run a nightly async export to refresh your catalog mirror, and subscribe to the [`stock-availability-updates` webhook](/pages/webhook-event-types#stock-availability-events) for incremental inventory changes between exports.
The user chooses a product and variant in your UI. Your system captures the shipping address (from the user's profile or input form) and optionally validates per-country availability using `GET /v3/variants/{variantId}/availability`.
Pass the chosen variant and recipient details to [`POST /v3/orders`](/modules/api/v3/orders/place-order) in a single idempotent call. Snappy returns an order with a tracking link and emits webhooks for every status change.
***
## Key Features
* **Comprehensive catalog access** - browse, filter, expand, and paginate Snappy's full catalog via the V3 Catalog API in real time.
* **Bulk catalog ingestion** - mirror the entire catalog (or any filtered subset) into your own database via the async [Export API](/modules/api/v3/exports/overview). Export jobs return signed NDJSON download URLs, valid for 48 hours. Pair with webhooks for incremental refresh between full snapshots.
* **Single-call order placement** - variant + recipient → order in one API call. Built-in idempotency via `idempotencyKey` prevents duplicates on replay.
* **Full order management** - retrieve, list, and cancel orders programmatically. Tag orders for reporting and attach metadata to round-trip your internal IDs.
***
## Core Platform Capabilities
The following capabilities apply to all Snappy integration models.
**Global Reach** Send gifts to recipients in over 30 countries. Snappy handles currency conversion, local sourcing, and international logistics automatically.
**Real-Time Tracking** Track the full lifecycle of every order - from order received to delivered - using Snappy's comprehensive [Webhooks](/pages/overview-and-setup) system. Order-level webhooks fire alongside gift-level webhooks for V3 orders.
**Enterprise Security** Scoped API keys, granular per-endpoint permissions, PII masking on order reads, and optional mTLS for enterprise integrations. See [Authentication & Security](/pages/authentication-and-security).
**Standards-Based Integration** Connect through standardized RESTful API endpoints with JSON:API conventions for filtering, expansion, pagination, and sorting on V3.
***
See the step-by-step Embedded Marketplace walkthrough with code samples (JavaScript, Python, cURL) in the API Recipes guide.
# Webhooks Setup
Source: https://docs.snappy.com/pages/overview-and-setup
Receive real-time gift, order, and delivery events. Configure endpoints, verify signatures, and test locally before going live.
## Overview
Webhooks allow your application to receive real-time, asynchronous notifications when specific events occur within the Snappy system. Instead of constantly polling the API for updates, Snappy will push data to your server as soon as a gift is sent, viewed, or redeemed.
***
## Setup
Set up a public endpoint in your application that can accept incoming `POST` requests with a JSON payload.
**Performance & Response:** Your endpoint must quickly return a `200 OK` response. If your server does not respond within **10 seconds**, Snappy will assume a delivery failure and will continue to retry sending the same event based on our retry policy.
To activate webhooks and start receiving events:
1. Log in to your Snappy Dashboard [https://login.snappy.com/login](https://login.snappy.com/login)
2. Navigate to **Sharing & Access** tab under the **Company Settings** page in the Snappy Dashboard ([https://login.snappy.com/company-settings/sharing-access](https://login.snappy.com/company-settings/sharing-access)).
3. Scroll down to the 'Webhooks' section and toggle **Enable webhooks** for your organization.
4. Click **Add Webhook**.
5. Specify your **Destination URL**, choose the relevant **Event Types**, and click **Add**.
Once your webhook is saved, send a test event from the dashboard to confirm Snappy can reach your endpoint:
1. In the **Webhooks** section of **Sharing & Access**, hover over the webhook you just added.
2. Click **Test**.
3. Snappy will send a template `POST` request to your destination URL containing a verification token.
4. Confirm your endpoint received the request and returned a `200 OK`.
If the test event doesn't arrive, double-check your firewall settings, your endpoint URL, and that your server is publicly reachable. You can re-run this test at any time to verify endpoint health.
To ensure that incoming requests are legitimately from Snappy and have not been tampered with, you must verify the **X-Snappy-Signature** header.
Under your webhook configurations, you will find a **Security Token**. This token is unique to your organization and can be regenerated if it is ever compromised.
**The Signature Logic:** The signature is a **SHA-256 hash**, where the key is your **UTF-8 encoded token** and the **raw request body** serves as the data.
```javascript theme={null}
function digest(parameters: {
clientSecret: string;
requestBody: unknown;
}) {
return crypto
.createHmac("sha256", encodeURI(parameters.clientSecret))
.update(JSON.stringify(parameters.requestBody))
.digest("hex");
}
const signature = digest({
clientSecret,
requestBody: request.body,
});
const headerSignature = request.headers["x-snappy-signature"];
const validSignature = signature === headerSignature;
```
***
## Delivery & Retries
If your endpoint is unavailable or returns a non-2xx status code, Snappy will attempt to redeliver the event.
* **Retry Strategy:** We use exponential backoff over a 24-hour period.
* **Timeout:** Requests time out after 10 seconds.
* **Manual Test:** Re-run the dashboard Test action at any time to verify endpoint health.
# Product Images
Source: https://docs.snappy.com/pages/product-images
Resize, pad, and format-convert product images from Snappy's CDN by appending query parameters to any image URL.
Product images in Snappy are served from **`https://image.snappy.com`**. You can request resized, padded, or format-converted versions of any product image by appending query parameters to the image URL.
No API key is required to load these URLs - they are public CDN links suitable for use directly in `
` tags or your mobile app.
## Getting the image URL
Every product and variant in the Snappy catalog includes image metadata.
### V3 products
V3 returns images in the `media` array on products and variants. Each entry includes a `src` URL that already points at the image CDN:
```json theme={null}
{
"id": "655277e68e0719000d6c3fd5",
"title": "NFL 25-Layer StadiumView Wall Art",
"media": [
{
"type": "image",
"src": "https://image.snappy.com/o1xc17wfbda6cl91hm0r6"
}
]
}
```
You can use the `src` value from the API as-is, or add query parameters to match the size and format your UI needs.
## URL format
```text theme={null}
https://image.snappy.com/{imageId}?width={px}&height={px}&format={format}&background={color}
```
All query parameters are optional. Omit any parameter you do not need.
**Example - square thumbnail with white padding:**
```text theme={null}
https://image.snappy.com/bnxjv94tmzq1w8eplk73ua?width=400&height=400&format=webp&background=white
```
**Example - scale to a fixed width:**
```text theme={null}
https://image.snappy.com/bnxjv94tmzq1w8eplk73ua?width=600&format=auto
```
***
## Query parameters
| Parameter | Type | Allowed values | Default | Required |
| :----------- | :------ | :-------------------------------------------------------------------------- | :------ | :------- |
| `width` | integer | Any positive integer (snapped - see below) | - | No |
| `height` | integer | Any positive integer (snapped - see below) | - | No |
| `format` | string | `jpg`, `png`, `webp`, `auto` | `auto` | No |
| `background` | string | `white`, `black`, `transparent`, or hex as `rgb:rrggbb` (e.g. `rgb:ff0000`) | `white` | No |
**`background` is only applied when both `width` and `height` are provided.** In all other cases it is ignored.
### Allowed sizes (snapping)
You can pass any positive integer for `width` and `height`. The CDN snaps your value to the allowed size before serving the image:
`100`, `200`, `300`, `400`, `500`, `600`, `800`, `1000`
For example, `width=99` is served as `100`, and `width=350` is served as `400`. This keeps cache efficiency high across all integrations using the same CDN.
If no transformation parameters are provided, the original image is returned unchanged.
***
## How transformations work
### Both `width` and `height` provided - pad to fit
When both dimensions are given, the image is **padded** (not cropped, not stretched) to fit the requested box. The original aspect ratio is preserved and empty space is filled with the `background` color.
This is the recommended mode for product cards and grids where the full product must remain visible.
```text theme={null}
width=400&height=400&background=white
```
### Only `width` or `height` provided - scale preserving ratio
When only one dimension is given, the image is scaled proportionally. The other dimension adjusts automatically. No padding, no cropping.
```text theme={null}
width=600
```
```text theme={null}
height=300
```
### Neither provided - format only
When no dimensions are given, only format conversion is applied (if `format` is set).
```text theme={null}
format=jpg
```
***
## Examples
| Use case | URL |
| :-------------------------------------------- | :-------------------------------------------------------------------------------------------------------- |
| Square card thumbnail (400 px, white padding) | `https://image.snappy.com/bnxjv94tmzq1w8eplk73ua?width=400&height=400&format=webp&background=white` |
| Square with transparent padding | `https://image.snappy.com/bnxjv94tmzq1w8eplk73ua?width=400&height=400&background=transparent&format=auto` |
| Fixed width, auto height | `https://image.snappy.com/bnxjv94tmzq1w8eplk73ua?width=600&format=auto` |
| Fixed height, auto width | `https://image.snappy.com/bnxjv94tmzq1w8eplk73ua?height=300&format=auto` |
| Convert to JPEG, no resize | `https://image.snappy.com/bnxjv94tmzq1w8eplk73ua?format=jpg` |
***
## Recommended sizes by use case
| Size (px) | Typical use |
| :-------- | :-------------- |
| 100 | Micro thumbnail |
| 200 | Small list item |
| 300 | Card |
| 400 | Medium card |
| 500 | Large card |
| 600 | Hero thumbnail |
| 800 | Grid image |
| 1000 | Large display |
Pick the size closest to your layout need - the snapping behavior ensures you land on one of these values automatically.
***
## Best practices
* **Use the first `media` image as the card thumbnail** when rendering product lists.
* **Prefer `format=webp` or `format=auto`** for web UIs to reduce payload size.
* **Use both `width` and `height` with `background`** when you need a consistent square or fixed-aspect container without cropping the product.
* **Reuse the same URL** across your app for the same size - identical URLs are served from CDN cache.
# Snappy API Quickstart: Send Your First Gift in Minutes
Source: https://docs.snappy.com/pages/quickstart
Authenticate, call POST /gifts, and deliver your first Snappy gift in under 5 minutes. Step-by-step quickstart with copy-paste examples.
## Getting Started with the Snappy API
Welcome to the developer quickstart! This guide will help you authenticate and make your first successful API request in under 5 minutes.
Make sure you are using your **Testing** API key while building your integration. See [Before You Begin](/pages/before-you-begin) for details on your two environments.
## Step 1: Get Your API Key
The Snappy API uses keys to authenticate requests. To ensure the highest level of security, Snappy uses **Granular API Permissions**, meaning you should only grant the exact permissions your application needs.
1. Log in to your Snappy Dashboard [https://login.snappy.com/login](https://login.snappy.com/login)
2. Navigate to **Sharing & Access** tab under the **Company Settings** page in the Snappy Dashboard.
3. Scroll down to the 'API Access' section and enable API access for your organization (if not already enabled).
4. Click '**Create API Key'**.
5. Define the **Scopes** (e.g., Select 'Read' , 'Create' or 'Update' etc. based on our needs).
6. Copy your new secret key. *Note: For security reasons, you will only be able to see this key once.*
## Step 2: Authentication
Snappy uses the `X-Api-Key` header to authenticate requests. You must include your API key in every request like so:
```text theme={null}
X-Api-Key: YOUR_API_KEY
```
For example, using cURL:
```text theme={null}
curl --request GET
--url https://api.snappy.com/public-api/v2/authentication/apiKeys
--header 'X-Api-Key: YOUR_API_KEY'
--header 'accept: application/json'
```
**Keep your key secure.** Never expose it in client-side code or public repositories. If a key is compromised, rotate it immediately from the Snappy Dashboard.
## Step 3: Make Your First Request
Test your connection by retrieving the API key you just created. The base URL for the Snappy API is [https://api.snappy.com/public-api/v2](https://api.snappy.com/public-api/v2).
You can use the following cURL command in your terminal (just replace YOUR\_API\_KEY with your actual key):
```text theme={null}
curl --request GET
--url https://api.snappy.com/public-api/v2/authentication/apiKeys
--header 'X-Api-Key: YOUR_API_KEY'
--header 'accept: application/json'
```
**Expected Response (200 OK):** If successful, you will receive a JSON payload containing the API key you generated in step 1:
```json theme={null}
{
"results": [
{
"id": "abc123456",
"expirationDate": "2022-12-06T09:50:38.536Z",
"createdAt": "2022-12-06T09:50:38.536Z",
"enforceMtls": false,
"name": "My API"
}
]
}
```
***
## Next Steps
Now that you have successfully authenticated, you can start building out your integration. Choose the flow that matches your use case:
**Triggered Gifting**
Send a gift invitation and let the recipient choose their own item:
* Set your first campaign. See [Create Campaign](/modules/api/v2/campaigns/create-campaign).
* Send a demo gift to preview the full Snappy Recipient Experience. See [Create Demo Gift](/modules/api/v2/gifts/create-demo-gift).
**Embedded Marketplace**
Display the catalog in your own platform and place orders directly:
* Retrieve the product catalog and browse available products and variants to display in your UI using. See [Get products](/modules/api/v3/products/get-products).
# Rate Limits
Source: https://docs.snappy.com/pages/rate-limits
Global, API-specific, and endpoint-specific rate limits for the Snappy API. Response codes, backoff guidance, and best practices.
To maintain consistent performance and availability, the APIs apply global, API-specific, and endpoint-specific rate limits.
All limits are measured per Company. Requests made with different API keys belonging to the same Company share the Company's limits. The most restrictive applicable limit takes precedence.
## How rate limiting works
Snappy uses a **token bucket** model. Each bucket has two parameters:
* **Sustained rate** - the rate at which tokens are added to the bucket, measured in requests per second (**RPS**). Each request, read or write, consumes one token.
* **Burst capacity** - the maximum number of tokens the bucket can hold, determining how many requests can be handled in a short burst.
When traffic stays below the sustained rate, unused tokens accumulate up to the burst capacity. These saved tokens provide temporary headroom for traffic spikes. After they are consumed, requests must follow the sustained rate until the bucket refills.
## API rate limits
| API | Rate limits |
| ------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Global API rate limit** | • **25 RPS** across all APIs
• Burst capacity: **200 requests** |
| **Marketplace API** | • Read: **25 RPS,** burst capacity: **200 requests**
• Write: **10 RPS,** burst capacity: **30 requests** |
| **Administration API** | • Read: **10 RPS,** burst capacity: **30 requests**
• Write: **5 RPS,** burst capacity: **20 requests** |
| **Authentication API** | • Read: **5 RPS,** burst capacity: **30 requests**
• Write: **5 RPS,** burst capacity: **10 requests** |
## Endpoint-specific limits
### Endpoint-specific export limits
| Endpoint | Write limits |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| `POST /v3/products/exports` | **1 concurrent export job**, shared across all export types |
| `POST /v3/collections/exports` | **1 concurrent export job**, shared across all export types |
| Creating another export while any export job is active returns `409 Conflict`. | |
## Rate-limit responses
When a rate limit is exceeded, the API returns a `429 Too Many Requests` response:
```json theme={null}
{
"status": 429,
"message": "Request rate limit exceeded. You can learn more here: https://docs.snappy.com/pages/rate-limits"
}
```
Clients should throttle requests according to the documented rate and burst limits. After receiving a `429 Too Many Requests` response, clients should reduce their request rate before retrying. Requests that continue to exceed the applicable limit may receive additional `429` responses.
## Fair-use protection
Additional temporary safeguards may be applied when traffic patterns threaten platform stability, including unusually large bursts, excessive polling, repeated failures, or automated abuse. Clients affected by these safeguards receive a `429 Too Many Requests` response.
Higher limits may be approved for verified integrations with demonstrated business requirements.
# Request & Response Standards
Source: https://docs.snappy.com/pages/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.
**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.
***
## 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
* **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.
## 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**. | | | |
**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.
### 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)**. | | |
# See V2
Source: https://docs.snappy.com/pages/see-v2
# See V3
Source: https://docs.snappy.com/pages/see-v3
# Core Concepts & Data Models
Source: https://docs.snappy.com/pages/snappy-core-concepts-and-data-models
How Companies, Accounts, Campaigns, Gifts, Orders, Recipients, and the marketplace and swag catalogs fit together in the Snappy platform.
Before diving into individual API endpoints, it helps to understand how Snappy's core objects relate to one another. Most API calls either create one of these objects or retrieve its current state - so a clear mental model will save you time when designing your integration.
## Account Structure
### Company
Your top-level account in Snappy. It holds your API keys, global configuration, and your recipient list. All accounts, campaigns, and gifts are ultimately scoped to a Company.
### Account
An Account lives within a Company and lets you separate and organize gifting activity for different teams, departments, or budget owners - each with its own campaigns and billing method.
If your organization has multiple departments sending gifts independently, each should operate through its own Account.
→ See the full schema in [Accounts Overview](/modules/api/v3/accounts/overview).
### Billing Method
A Billing Method is the financial funding source used to pay for gifts and associated fees. It must be specified when creating a campaign and is verified when sending a gift - it must have sufficient funds or credit at the time of the request, otherwise the gift will not be processed.
Billing Methods are set at the **Account** level. One Billing Method per Account can be set as the default. This default is applied automatically to any Campaign created via the API. Campaigns created through the Dashboard allow you to select a Billing Method explicitly at the time of creation.
Snappy supports several billing method types. Note that **credit card (EXP) cannot currently be used to send gifts via the API**.
Billing Methods can currently be defined and managed through the Snappy Dashboard.
***
## The Gift Pipeline
### Campaign
A Campaign is an **organizational object** for configuring and sending gifts. It represents a single gifting activity or occasion and acts as a template for all settings that apply to it - including the selected Collection or Product, budget, branding, and notification messages.
Think of it as: a reusable send configuration. Campaigns can be created via the Dashboard or directly through the API. When created via the API, the Account's default Billing Method is applied automatically.
For **Triggered Gifting** (`POST /gifts`), a Campaign ID is required. For **Embedded Marketplace** (`POST /orders`), Snappy auto-selects or auto-creates a Campaign based on the supplied `fundingSourceId` when `campaignId` is omitted.
→ See the full schema in [Campaigns Overview](/modules/api/v2/campaigns/overview).
### Gift
A Gift represents the entire gifting experience for a single recipient within a Campaign - from creation through to final delivery. The Gift is the **primary integration object for Triggered Gifting**: you create it, Snappy notifies the recipient, they claim it, and an Order is generated downstream.
The Gift goes through the following lifecycle:
| Stage | What happens |
| :------------------- | :---------------------------------------------------------------------------- |
| **Creation** | Gift is initiated and linked to a recipient and Campaign |
| **Notification** | Recipient is notified via email or other channels |
| **Selection** | Recipient chooses their item and variation, and enters their shipping address |
| **Order Generation** | An Order is created based on the selected variant and shipping address |
| **Delivery** | The physical product is shipped and tracked to completion |
Once the Order is generated, its delivery progress is reflected back in the Gift's status - webhook events like `gift-delivery-status-changed` cover the full lifecycle through to `delivered`, so you can continue tracking via the Gift you originally created.
Use Webhooks to track Gift status changes in real time rather than polling.
→ See the full schema in [Gifts Overview](/modules/api/v2/gifts/overview).
### Order
An Order represents the physical fulfillment event.
* For **Embedded Marketplace** an Order is the **primary integration object**.
* For **Triggered Gifting** the Order is the point at which a gift becomes a shipment.
The Order is the **primary integration object for Embedded Marketplace**: you create it via a single call to `POST /orders` with the recipient and variant details.
It is created either:
* **Automatically** by Snappy once a recipient selects their item and variant and enters their shipping address in the Snappy Recipient Experience (**Triggered Gifting model**) - downstream of the Gift.
* **Directly** by your system using the V3 `POST /orders` endpoint, passing the selected variant ID and recipient details in a single idempotent call (**Embedded Marketplace model**).
Each Order carries:
* **Line items** - what was ordered (variant, quantity, title)
* **Fulfillments** - shipments with carrier, tracking number, tracking URL, and status (`confirmed`, `processing`, `in_transit`, `out_for_delivery`, `delivered`)
* **Shipping address** - delivery address for the order. For **physical** variants (`shippingRequired: true`), the full address is required (`address1`, `city`, `provinceCode`, `postalCode`, `countryCode`). For **digital** variants (gift cards, e-vouchers, `shippingRequired: false`), only `countryCode` is required - the delivery is by email, not physical shipment.
* **`tags`** - caller-supplied labels for grouping orders in reports
* **`metadata`** - key-value passthrough for caller data (for example, your internal order ID or campaign reference)
* **`idempotencyKey`** - caller-supplied stable key that prevents duplicate orders on replay (Embedded Marketplace only)
Orders can be retrieved, listed, and cancelled programmatically via the [V3 Orders API](/modules/api/v3/orders/overview).
Use Webhooks to track fulfillment and delivery status in real time rather than polling.
→ See the full schema in [Orders Overview](/modules/api/v3/orders/overview).
### Recipient
A Recipient is a person in your Snappy contact list. Once created, they can be referenced across multiple gift sends without re-submitting their details each time.
Each Recipient can carry an **`externalId`** - your own identifier for the same person in your CRM, HRIS, or other system. Snappy stores and returns it on every gift and order, so you can join Snappy data back to your records without maintaining a separate mapping.
For one-off sends, you can pass contact details inline when creating a Gift. However, for recurring use cases - employee anniversaries, loyalty rewards - managing Recipients via the API keeps your integration clean and avoids duplicate contacts.
→ See the full schema in [Recipients Overview](/modules/api/v2/recipients/overview).
***
## The Gift Catalog
Snappy maintains **two parallel catalogs**:
* **Marketplace catalog** - curated gifts from third-party brands (physical items, digital items, gift cards, donations)
* **Swag catalog** - branded merchandise templates that you customize (t-shirts, mugs, notebooks, etc.)
Across both catalogs, the orderable unit is always the **variant** - never the product or base product.
### **Collection**
A Collection is a curated catalog of marketplace items tailored to a specific theme, budget range, and audience (e.g. "Wellness Gifts Under \$50"). Assign a Collection to a Campaign if you want the recipient to select their preferred item.
→ See the full schema in [Collections Overview](/modules/api/v3/collections/overview).
### **Product**
A Product is a single specific marketplace item - a curated gift, digital item, gift card, or donation. Assign a specific Product to a Campaign when you have a specific item in mind. If that Product has variants, the recipient will need to select the specific one (size, color, etc.).
→ See the full schema in [Products & Variants Overview](/modules/api/v3/products/overview).
### **Product Variant**
Many Products come in multiple variations - for example, a hoodie in different sizes and colors. Each variation is represented as a distinct **Variant**.
When placing a marketplace Order you must specify the Variant ID, not the Product ID.
→ See the full schema in [Products & Variants Overview](/modules/api/v3/products/overview).
### **Swag Base Product**
A Base Product is a **swag template** - an unbranded item from which customized swag is derived (e.g. "standard cotton t-shirt", "ceramic mug"). Base Products are the swag-catalog counterpart to standard Products and are browsed via the V3 `/v3/base-products` endpoints.
→ See the full schema in the [Swag](/modules/api/v3/base-products/overview) page.
### **Swag Base Variant**
A Base Variant is a specific orderable version of a Base Product (e.g. "standard cotton t-shirt, Medium, Navy"). Base Variants are the swag-catalog counterpart to standard Variants.
When placing a swag Order you must specify the Base Variant ID, not the Base Product ID.
→ See the full schema in the [Swag](/modules/api/v3/base-products/overview) page.
**Where do Collections, Products, and Base Products come from?**
* **Collections** can be curated in the Snappy Dashboard, letting you assemble themed gift sets from Snappy's catalog.
* **Products** are available from Snappy's curated marketplace catalog.
* **Base Products** are swag templates browsable via the V3 `/v3/base-products` endpoints. For custom-branded swag setup (logos, designs, mockups), contact your Snappy account manager.
***
## Gift Customization
Gift Customization is not a standalone entity you create independently - it is a **configuration layer** that controls how a gift looks and behaves for the recipient. It is unique in that it can be defined at multiple levels of your account hierarchy and is **inherited downward**, with each level able to override the one above it.
### The Three Configuration Areas
#### **Gift Properties**
The core characteristics of the gift: type (Collection or specific Product), budget, expiration period, and similar settings.
#### **Notification Policy**
Controls how and when recipients are notified - through which channels, with what content, and at what timing.
#### **Recipient Experience**
Defines the interactive journey recipients go through when claiming their gift: the unwrapping animation, greeting message, and address collection flow.
You can explore recipient experience options and generate an API-ready payload at [https://login.snappy.com/api/gift-customization](https://login.snappy.com/api/gift-customization).
### Inheritance & Overrides
Gift Customization follows a top-down inheritance model. Defaults set at a higher level flow down automatically, but can be overridden at any level below:
```text theme={null}
Company defaults → Account defaults → Campaign settings → Individual Gift
```
**Example:** Your Account has a default gift expiration of 30 days. You create a Campaign that overrides this to 14 days. When creating an individual Gift, you can override it again - for example, to give a high-value recipient more time:
```json theme={null}
{
"campaignId": "cmp_12345",
"recipients": [
{
"firstname": "Jane",
"lastname": "Doe",
"email": "jane@example.com",
"key": "jane-vip-2026"
}
],
"customization": {
"giftProperties": {
"expiration": {
"type": "daysFromSend",
"numberOfDays": 60
}
}
}
}
```
In this example, even though the Campaign default is 14 days, this specific Gift will expire after 60 days. The Campaign default is not affected - all other gifts created under this Campaign will still use the 14-day expiration.
Overrides at the Gift level apply only to **that specific Gift**. They do not modify the Campaign, Account, or Company defaults.
Via the API, Gift Customization can only be set at the **Campaign level and below**. Company and Account level defaults must be configured through the Snappy Dashboard.
***
## Quick Reference
| Entity | Lives inside | Created via | Notes |
| :-------------------------- | :---------------------------------- | :--------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- |
| Company | - | Snappy onboarding | Root of everything |
| Account | Company | Dashboard or API | Organizes campaigns by team or department |
| Billing Method | Account | Dashboard | Must be funded; debited on gift creation. Referenced in V3 APIs as `fundingSourceId`. |
| Campaign | Account | Dashboard or API | Inherits the Account's default Billing Method when created via API. Required for Triggered Gifting; auto-managed for Embedded Marketplace. |
| Collection | Account | Snappy catalog or Dashboard | Recipient chooses from it |
| Product / Variant | Account | Snappy catalog or Dashboard | Marketplace catalog; Variant required for marketplace orders |
| Base Product / Base Variant | Account | Snappy catalog | Swag catalog (templates); Base Variant required for swag orders |
| Recipient | Company | API or Dashboard | Can be passed inline for one-off sends; can carry an `externalId` |
| Gift | Campaign | API or Dashboard | **Primary integration object for Triggered Gifting**; one per recipient per send |
| Order | Gift | Automatically or direct API call | **Primary integration object for Embedded Marketplace**; created on recipient selection (Triggered) or via single-call placeOrder (DF) |
| Gift Customization | Company / Account / Campaign / Gift | Dashboard or API (Campaign & Gift) | Inherited and overridable at each level |
# Snappy MCP Server: Connect Claude & ChatGPT
Source: https://docs.snappy.com/pages/snappy-mcp-server
Plug Snappy into Claude, ChatGPT, and other MCP-compatible AI clients. Setup, supported tools, and security model.
Snappy's MCP (Model Context Protocol) Server allows AI systems - such as Microsoft Copilot, custom AI agents, or any MCP-compatible client - to send gifts through natural language interactions. Instead of building API integrations manually, your AI agent can call Snappy's MCP tools directly to manage campaigns and send gifts on behalf of users.
This integration path is designed for AI-driven environments. If you're building a standard programmatic integration, use the [Snappy REST API](/pages/quickstart) instead.
## Prerequisites
Before connecting an AI system to the MCP server, make sure the following are configured in your Snappy Dashboard:
* A valid Snappy user account (username, password, and optional TOTP)
* At least one Company and Account
* At least one Collection configured for your Account
* Recipients set up, or ready to be managed via API
The MCP server endpoint is provisioned by Snappy. Contact your account manager to get the URL for your environment.
***
## How It Works
Your AI system connects to the Snappy MCP server and authenticates using user credentials. Once authenticated, it can call a set of structured tools to retrieve collections, generate personalized content, create campaigns, and send gifts - all through a standardized protocol.
### Authentication
The MCP server uses session-based authentication. Call the `sign-in` tool with your Snappy credentials to establish a session:
```json theme={null}
{
"method": "tools/call",
"params": {
"name": "sign-in",
"arguments": {
"username": "your@email.com",
"password": "your_password",
"totp": "123456"
}
}
}
```
**Successful response:**
```json theme={null}
{
"content": [
{
"type": "text",
"text": "Successfully signed in. Available accounts: [Account data]"
}
]
}
```
For **HTTP transport**, each request must include authentication. For **SSE transport**, the session is maintained automatically after sign-in.
Never hardcode usernames, passwords, or TOTP codes. Use environment variables or a secrets manager. See [Best Practices](#best-practices) below.
***
## Available Tools
| Tool | Purpose | Required auth | Key parameters |
| :------------------------ | :--------------------------------------- | :------------ | :--------------------------------------------------------------------------- |
| `sign-in` | Authenticate with the MCP server | No | `username`, `password`, `totp?` |
| `check-sign-in` | Verify current authentication status | No | None |
| `get-account-collections` | List collections available to an Account | Yes | `accountId` |
| `get-collection-products` | Retrieve products within a collection | Yes | `collectionId`, `maxBudget?`, `country?` |
| `create-email` | Generate AI-powered email content | Yes | `occasion`, `tone?`, `primaryColor?` |
| `create-greeting` | Generate a personalized greeting card | Yes | `occasion`, `tone?`, `primaryColor?` |
| `choose-reveal` | Select the gift reveal experience | Yes | `occasion`, `brandColors?` |
| `create-campaign` | Create a new gift campaign | Yes | `companyId`, `accountId`, `collectionId`, `name`, `budgets`, `recipientsIds` |
| `send-gifts` | Send gifts within a campaign | Yes | `campaignId`, `recipients`, `budget`, `productCollectionId` |
### Tool examples
#### `get-account-collections`
```json theme={null}
{
"method": "tools/call",
"params": {
"name": "get-account-collections",
"arguments": {
"accountId": "account_12345"
}
}
}
```
**Response:**
```json theme={null}
{
"content": [{
"type": "text",
"text": "[{\"_id\":\"coll_123\",\"name\":\"Premium Business Collection\",\"rank\":1},{\"_id\":\"coll_456\",\"name\":\"Holiday Special Collection\",\"rank\":2}]"
}]
}
```
#### `create-campaign`
```json theme={null}
{
"method": "tools/call",
"params": {
"name": "create-campaign",
"arguments": {
"companyId": "comp_789",
"accountId": "acc_123",
"collectionId": "coll_456",
"name": "Q4 Employee Appreciation Campaign",
"maxBudget": 75,
"minBudget": 25,
"recipientsIds": ["recipient_1", "recipient_2"],
"email": {
"mailSubject": "A special gift for you!",
"mailGreeting": "Dear {receiver_first_name}",
"mailBody": "We appreciate your hard work this quarter.",
"mailSignature": "Best regards, The Team"
}
}
}
}
```
***
## The Gift Sending Flow
Regardless of which AI system you're connecting, the gifting flow follows the same sequence:
1. `sign-in` - authenticate and establish a session
2. `get-account-collections` - retrieve available collections
3. `create-email` + `create-greeting` + `choose-reveal` - generate personalized content (these three can run in parallel)
4. `create-campaign` - set up the campaign with recipients and content
5. `send-gifts` - trigger the gift send
***
## Quick Start: Adding to Your AI Agent (MCP Config)
To add Snappy to any MCP-compatible AI environment, point your MCP configuration to the Snappy server URL provided by your account manager:
```javascript theme={null}
const mcpConfig = {
serverUrl: "",
transport: "http",
authentication: {
type: "user_credentials",
endpoint: "/auth/sign-in"
}
};
```
***
## Worked Example: Node.js Bot
A complete Node.js example that handles a `/sendgift` chat command, authenticates, generates personalized content in parallel, and creates and sends a campaign.
```javascript theme={null}
const MCPClient = require('./mcp-client');
class SnappyGiftBot {
constructor() {
this.mcpClient = new MCPClient(process.env.SNAPPY_MCP_URL);
}
async sendGift(params) {
// 1. Authenticate (use cached session in production)
await this.mcpClient.signIn(
process.env.SNAPPY_USERNAME,
process.env.SNAPPY_PASSWORD
);
// 2. Get collections and pick one for the occasion
const collections = await this.mcpClient.getAccountCollections(params.accountId);
const selectedCollection = this.selectBestCollection(collections, params.occasion);
// 3. Generate personalized content in parallel
const [email, greeting, reveal] = await Promise.all([
this.mcpClient.createEmail({ occasion: params.occasion, tone: 'professional' }),
this.mcpClient.createGreeting({ occasion: params.occasion }),
this.mcpClient.chooseReveal({ occasion: params.occasion })
]);
// 4. Create campaign
const campaign = await this.mcpClient.createCampaign({
companyId: params.companyId,
accountId: params.accountId,
collectionId: selectedCollection._id,
name: `${params.occasion} Gift - ${new Date().toLocaleDateString()}`,
maxBudget: params.maxBudget,
minBudget: params.minBudget,
recipientsIds: [params.recipientId],
email: JSON.parse(email.content)
});
// 5. Send gifts
await this.mcpClient.sendGifts({
campaignId: campaign.created._id,
recipients: { type: 'ids', recipientsList: [params.recipientId] },
budget: { plan: params.budget, max: params.maxBudget, min: params.minBudget },
productCollectionId: selectedCollection._id
});
return { campaignName: campaign.name, collectionName: selectedCollection.name };
}
}
```
***
## Error Handling
### Common error scenarios
**Authentication failed**
```json theme={null}
{
"error": {
"code": "AUTH_FAILED",
"message": "Login failed",
"details": "Invalid username or password"
}
}
```
**TOTP required**
```json theme={null}
{
"content": [{
"type": "text",
"text": "Please enter your TOTP code from your authenticator app"
}]
}
```
When you see this response, prompt the user (or your secret store) for the current TOTP and re-call `sign-in` with the `totp` argument populated.
**Resource not found**
```json theme={null}
{
"error": {
"code": "NOT_FOUND",
"message": "Collection not found",
"collectionId": "invalid_collection_id"
}
}
```
**No products available for criteria**
```json theme={null}
{
"error": {
"code": "NO_PRODUCTS_AVAILABLE",
"message": "No products available for the specified criteria",
"details": {
"budget": 25,
"country": "US",
"collectionId": "coll_123"
}
}
}
```
### Retry strategy
Use exponential backoff for transient failures (network errors, timeouts, 5xx). Do not retry `AUTH_FAILED` or `NOT_FOUND`.
```javascript theme={null}
class MCPErrorHandler {
static async withRetry(operation, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries || !this.isRetryableError(error)) {
throw error;
}
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
static isRetryableError(error) {
return ['NETWORK_ERROR', 'TIMEOUT', 'SERVER_ERROR'].includes(error.code);
}
}
```
***
## Best Practices
**Secure credentials.** Never hardcode username, password, or TOTP. Pull them from environment variables or a secrets manager. When logging tool calls for observability, redact `password` and `totp` before writing.
**Reuse sessions.** Don't call `sign-in` on every request. Cache the session and refresh only when expired:
```javascript theme={null}
class MCPSessionManager {
constructor() {
this.session = null;
this.lastAuth = null;
}
async getValidSession() {
if (this.isSessionExpired()) {
this.session = await this.authenticate();
this.lastAuth = Date.now();
}
return this.session;
}
}
```
**Run content generation in parallel.** `create-email`, `create-greeting`, and `choose-reveal` are independent - call them concurrently to cut latency.
**Cache collections.** Collection data changes infrequently. A 5-minute in-memory cache keyed by `accountId` is usually sufficient.
***
## Troubleshooting
**Check authentication status** before debugging tool calls:
```javascript theme={null}
const authStatus = await mcpClient.checkSignIn();
console.log('Auth Status:', authStatus);
```
**Confirm account setup** if collections come back empty:
```javascript theme={null}
const collections = await mcpClient.getAccountCollections(accountId);
if (collections.length === 0) {
throw new Error('No collections available. Set up collections in the Snappy Dashboard.');
}
```
**Test individual tools** in isolation when narrowing down a failing flow - call `get-account-collections` and `get-collection-products` directly to verify the data exists before calling `create-campaign`.
For additional support, contact your Snappy account manager.
# Send Event-Driven Gifts with Recipient Choice
Source: https://docs.snappy.com/pages/triggered-gifting-overview
Send personalized gifts triggered by events in your system. Snappy delivers a claim link, recipients pick their favorite, and Snappy handles fulfillment.
***Your system triggers the gift, Snappy handles the experience, the recipient chooses what they want.***
This is the classic Snappy experience. Use this model when you want to offer the recipient a choice of gifts, or when you don't have their physical shipping address upfront.
Looking for full control over the catalog and ordering UX inside your own platform? See [Embedded Marketplace](/pages/direct-fulfillment).
***
## Common Use Cases
### **Employee Recognition & Retention**
Automatically send gifts for work anniversaries, birthdays, or performance milestones via integration with HR systems like Workday or BambooHR.
### **Sales Lead Nurturing**
Improve conversion rates by sending gift offers at key moments in the sales funnel - after product demos, discovery calls, or proposal stages - using CRM triggers.
### **Client Onboarding & Appreciation**
Welcome new clients with thoughtful gifts once onboarding milestones are completed, creating a memorable first impression without needing to ask for their home address.
***
## How it works
Your system calls `POST /gifts` to create a Gift. The response includes a Gift object with a unique link to the Snappy *Claim Gift* experience.
Snappy notifies the recipient automatically via email or SMS with a magic link, or you can use the link from the response to trigger your own notification.
The recipient opens the magic link, browses the curated catalog, chooses their preferred gift, and enters their shipping address.
Snappy generates the order, dispatches it through the right fulfillment partner, and emits webhooks for every status change.
***
## Key Features
* **Personalized gifting experience** - deliver a curated collection of gifts and let the recipient choose their favorite item and provide their own address.
* **Automated campaigns** - trigger gifts automatically based on predefined business rules (birthdays, anniversaries) or external system events.
* **Recipient management** - programmatically manage your recipient lists and groups directly through the API.
***
See the step-by-step Triggered Gifting walkthrough with code samples (JavaScript, Python, cURL) in the API Recipes guide.
# Webhook Event Types: Real-Time Gift Lifecycle
Source: https://docs.snappy.com/pages/webhook-event-types
Reference for Snappy webhook events - gift claimed, order fulfilled, address validated, and more. Payload examples included.
## Event Structure
Every payload received at your endpoint follows this standard structure:
| **Field** | **Type** | **Description** |
| :------------ | :------- | :------------------------------------------------------------------------------- |
| `webhookData` | Object | Metadata about the webhook delivery (ID, type, timestamp). |
| `eventData` | Object | The core payload containing entity-specific information (Gift ID, Status, etc.). |
Use these example payloads to build and test your webhook listener locally before receiving real events. Tools like [Ngrok](https://ngrok.com) or [Webhook.site](http://Webhook.site) let you simulate incoming webhook calls by sending these payloads directly to your local endpoint - no real gift sends required.
***
## Webhooks and Events Types
Snappy currently supports the following:
### Gift Status Events
These events track the core lifecycle of a gift.
| **Event Name** | **Description** |
| :-------------------- | :---------------------------------------------------------------------- |
| `gift-status-changed` | Triggered whenever a gift moves to a new stage in the recipient journey |
**Optional statuses:**
| **Status** | **Description** |
| :---------- | :-------------------------------------------------------------------------- |
| `unopened` | The gift has been sent but the recipient has not clicked the link. |
| `unwrapped` | The recipient has clicked the link but has not yet viewed the gift options. |
| `opened` | The recipient has viewed the available gift options. |
| `claimed` | The recipient has selected a gift and provided their details. |
| `expired` | The gift reached its expiration date without being claimed. |
**Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "gift-status-changed",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-15T14:44:00Z"
},
"eventData": {
"companyId": "com_12345",
"giftId": "gft_12345",
"status": "claimed",
"metadata": {
"internalReferenceId": "REF-ABC-123"
}
}
}
```
***
### Order Lifecycle Events
These events expose the order behind a claimed gift using the **v3 Orders** vocabulary. Use them when you integrate at the order level rather than the recipient-facing gift level.
| **Event Name** | **Description** |
| :------------------------------ | :------------------------------------------------------------------- |
| `order-status-changed` | Triggered when an order's status changes (e.g. placed or cancelled). |
| `order-delivery-status-changed` | Triggered as an order moves through the full delivery lifecycle. |
**`order-status-changed` statuses:**
| **Field** | **Values** |
| :------------------ | :------------------------- |
| `status` | `active`, `cancelled` |
| `fulfillmentStatus` | `unfulfilled`, `cancelled` |
**`order-delivery-status-changed` statuses:**
| **Delivery Status** | **Description** |
| :------------------ | :------------------------------------------ |
| `confirmed` | The fulfillment request has been received. |
| `processing` | The item is being prepared for shipment. |
| `in_transit` | The item has been picked up by the carrier. |
| `out_for_delivery` | The item is expected to be delivered today. |
| `delivered` | The item has reached its final destination. |
Order delivery statuses use the public v3 `snake_case` vocabulary, whereas the `gift-delivery-status-changed` event (under **Delivery & Fulfillment Events**) reports the recipient-facing `camelCase` milestones.
**`order-status-changed` Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "order-status-changed",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-16T12:50:40.313Z"
},
"eventData": {
"companyId": "com_12345",
"orderId": "ord_12345",
"status": "active",
"fulfillmentStatus": "unfulfilled",
"metadata": {
"internalReferenceId": "REF-ABC-123"
}
}
}
```
**`order-delivery-status-changed` Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "order-delivery-status-changed",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-16T12:50:40.313Z"
},
"eventData": {
"companyId": "com_12345",
"orderId": "ord_12345",
"deliveryStatus": "out_for_delivery",
"triggerEvent": "updated",
"outForDeliveryDate": "2025-01-16T08:00:00.000Z",
"estimatedDeliveryDate": "2025-01-17T00:00:00.000Z"
}
}
```
***
### Delivery & Fulfillment Events
These events track the physical movement of a gift after it has been claimed.
| **Event Name** | **Description** |
| :----------------------------- | :----------------------------------------------------------------- |
| `gift-delivery-status-changed` | Triggered as the gift moves through the physical shipping process. |
**Delivery statuses:**
| **Delivery Status** | **Description** |
| :------------------ | :------------------------------------------ |
| `inTransit` | The item has been picked up by the carrier. |
| `outForDelivery` | The item is expected to be delivered today. |
| `delivered` | The item has reached its final destination. |
This event only fires for the `inTransit`, `outForDelivery`, and `delivered` milestones. For the full order-level delivery lifecycle (including `confirmed` and `processing`), use the `order-delivery-status-changed` event under **Order Lifecycle Events** instead.
**Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "gift-delivery-status-changed",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-16T12:50:40.313Z"
},
"eventData": {
"companyId": "com_12345",
"giftId": "gft_12345",
"deliveryStatus": "outForDelivery",
"triggerEvent": "updated",
"outForDeliveryDate": "2025-01-16T08:00:00.000Z",
"estimatedDeliveryDate": "2025-01-17T00:00:00.000Z"
}
}
```
`deliveredAt` is included when `deliveryStatus` is `delivered`; `outForDeliveryDate` when `outForDelivery`. `triggerEvent` is `created` for the first tracking update and `updated` for subsequent ones.
***
### Recipient Notification Events
Use these events to track the communications Snappy sends to your recipients.
| **Event Name** | **Description** |
| :----------------------------- | :------------------------------------------------------------------------ |
| `gift-notification-initial` | The first gift notification was sent. |
| `gift-notification-reminder` | An automated reminder was sent to a recipient who hasn't claimed yet. |
| `gift-notification-resend` | A notification was manually resent via the dashboard or API. |
| `gift-notification-expiration` | The final "last chance" notification was sent before the gift expiration. |
**Event Data fields:**
| **Field** | **Type** | **Notes** |
| :--------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------- |
| `companyId` | string | The company that owns the gift. |
| `giftId` | string | The gift the notification relates to. |
| `sendingChannel` | string | The channel used to deliver the notification (e.g. `email`, `sms`). |
| `giftLink` | string | The recipient-facing link included in the notification. |
| `reminderType` | string | The reminder variant. Present on `gift-notification-reminder` only. |
| `cadence` | number | Sequence number of the reminder/expiration notice. Present on `gift-notification-reminder` and `gift-notification-expiration`. |
| `metadata` | object | Any custom metadata attached at gift creation. |
**Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "gift-notification-reminder",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-19T10:30:00.000Z"
},
"eventData": {
"companyId": "com_12345",
"giftId": "gft_12345",
"cadence": 1,
"sendingChannel": "email",
"reminderType": "first-reminder",
"giftLink": "https://gift.snappy.com/g/abc123",
"metadata": {
"internalReferenceId": "REF123456",
"customCategory": "employee-recognition",
"recipientContext": "anniversary-gift"
}
}
}
```
***
### Catalog & Stock Events
Use these events to keep your local catalog in sync.
| **Event Name** | **Description** |
| :--------------------------- | :------------------------------------------------------------------------------------------------------------- |
| `stock-availability-updates` | Triggered when a product's inventory status changes. This is essential for keeping your local catalog in sync. |
**Optional statuses:**
| Inventory **Status** | **Description** |
| :------------------- | :------------------------------------------------------------- |
| `in_stock` | The product is available for ordering. |
| `stocked_on_demand` | The product is available but may require additional lead time. |
| `discontinued` | The product is no longer available and will not be restocked. |
| `out_of_stock` | The product is temporarily unavailable. |
**Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "string",
"eventType": "stock-availability-updates",
"target": "string",
"triggeredAt": "2025-01-22T11:45:00.000Z"
},
"eventData": {
"id": "string",
"title": "Premium Wireless Headphones",
"description": "High-quality wireless headphones with noise cancellation",
"category": "Electronics / Audio / Headphones / Wireless",
"status": "in_stock",
"brand": {
"id": "string",
"name": "Brand Name"
},
"types": [
{
"type": "physicalGift"
}
]
}
}
```
***
### Recipient Engagement Events
Triggered when a recipient interacts with the platform after claiming their gift, such as sending a message to the gift sender.
| **Event Name** | **Description** |
| :----------------------------- | :--------------------------------------------------------- |
| `gift-thank-you-note-received` | The recipient has written a thank-you note for the sender. |
**Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "gift-thank-you-note-received",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-18T10:30:00.000Z"
},
"eventData": {
"companyId": "com_12345",
"giftId": "gft_12345",
"thankYouNote": "Thank you so much for the thoughtful gift!"
}
}
```
### Exceptions & Operational Events
Use these events to track **critical edge cases** in your integration.
| **Event Name** | **Description** |
| :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
| `order-canceled` | Triggered if an order is canceled by the sender, the system, or due to a fulfillment issue. |
| `order-out-of-stock` | Triggered if a selected product becomes unavailable before fulfillment. This allows you to proactively notify the sender or offer a replacement. |
The `eventData` for operational events always contains the relevant `orderId` and `companyId` so you can map the failure back to the specific order and recipient.
**`order-canceled` Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "order-canceled",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-20T15:00:00.000Z"
},
"eventData": {
"companyId": "com_12345",
"orderId": "ord_12345",
"cancellationReason": "customer_requested"
}
}
```
**`order-out-of-stock` Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "order-out-of-stock",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-20T15:00:00.000Z"
},
"eventData": {
"companyId": "com_12345",
"orderId": "ord_12345"
}
}
```
***
### Billing Events
Use these events to react to invoicing changes.
| **Event Name** | **Description** |
| :------------------ | :------------------------------------------- |
| `gift-invoice-sent` | Triggered when a gift invoice has been sent. |
**`gift-invoice-sent` Example Payload:**
```json theme={null}
{
"webhookData": {
"id": "wh_12345",
"eventType": "gift-invoice-sent",
"target": "https://your-domain.com/webhooks",
"triggeredAt": "2025-01-21T09:15:00.000Z"
},
"eventData": {
"companyId": "com_12345",
"invoiceId": "inv_12345"
}
}
```
***
## Metadata in Webhooks
As discussed in the **API Standards** section, any `metadata` you attach during gift creation is echoed back in the `eventData` of the gift-lifecycle, notification, and shipping events - specifically `gift-status-changed`, `order-status-changed`, the `gift-notification-*` events, `gift-delivery-status-changed`, and `order-delivery-status-changed`. This ensures you can always map a Snappy event back to your internal IDs (e.g., `internalReferenceId`).
Billing, catalog, and operational events (such as `order-canceled` and `order-out-of-stock`) do **not** carry gift `metadata`. Map these back to your records using the `orderId` or `companyId` included in the payload.
**Proactive Support:** By listening for `order-out-of-stock` or `expired` events, your system can automatically trigger follow-up actions, ensuring a high-quality experience even when things don't go as planned.
# Welcome to the Snappy API
Source: https://docs.snappy.com/pages/welcome-to-snappy-api
Build rewards marketplaces and swag stores with Snappy's catalog, or trigger gifts recipients claim on Snappy - one API, two integration paths.
Snappy empowers organizations to bring gifting and marketplace experiences directly into their platforms, portals, and workflows. Our API provides the flexible infrastructure to handle both use cases at scale - built with enterprise-grade security, and designed to get you from planning to live deployment in weeks.
## Two ways to integrate
We support two primary integration models. Pick the one that fits your use case - or combine both if your platform needs them:
Full control over the user experience within your own platform. Your team renders the catalog UI and your platform places orders directly.
**Best for:** embedded marketplaces, branded shopping flows, full UX ownership.
A link-based experience where recipients choose their own gift. Snappy handles the catalog UI, claim flow, and address collection.
**Best for:** fast time-to-launch, no recipient address required upfront, marketing or HR-driven gifting.
## How to choose
Ask yourself: **what are you trying to achieve?**
* **Enrich your marketplace with Snappy's curated gifts and swag** → [Embedded Marketplace](/pages/marketplace-overview)
* **Create event-driven gifting moments** → [Triggered Gifting](/pages/triggered-gifting-overview)
Both models share the same underlying infrastructure and reporting.
## Next steps
Authenticate and make your first request in under 5 minutes with copy-paste examples.
Understand how Companies, Accounts, Gifts, Orders and other entities fit together.