# Introduction Source: https://docs.snappy.com/guides/introduction Introduction to Snappy's guides - foundational patterns, vertical recipes, and migration references for building on the Snappy API. Snappy's Guides are organized in two tiers: **Patterns** — focused, reusable building blocks for a single capability. Each pattern covers one thing in depth: how to access the catalog, how to place an order, how to track fulfillment. They're designed to be combined. **Recipes** — end-to-end vertical walkthroughs that assemble patterns into complete integration guides. A recipe walks you from zero to working integration for a specific use case. ## Patterns ### Catalog Access * [Real-Time Catalog Access](/guides/patterns/real-time-catalog-access) — Serve Snappy's V3 catalog on demand with live API calls (and optional edge caching). No local mirror, no sync jobs. * [Bulk Catalog Export](/guides/patterns/bulk-catalog-export) — Export the full catalog asynchronously and maintain a local copy. Queue a job, poll for completion, download the result. * [Swag Products Access](/guides/patterns/swag-products-access) — Access Snappy's swag catalog. Requires the `Snappy-Account-Id` header and swag-specific filters. ### Orders * [Place Orders](/guides/patterns/place-orders) — Place direct-fulfillment orders via `POST /v3/orders`. Covers address validation, idempotency, bulk placement, and the Swag variant. * [Track Order Fulfillment](/guides/patterns/track-order-fulfillment) — Subscribe to order webhooks and respond to fulfillment lifecycle events. Covers all four event types and delivery status transitions. ### Triggered Gifting * [Send Triggered Gifts](/guides/patterns/send-triggered-gifts) — Create gifts programmatically with `POST /v2/gifts`. Covers bulk chunking, idempotency keys, and partial failure handling. * [Auto-claim Gifts as Fallback](/guides/patterns/auto-claim-gifts-as-fallback) — Auto-claim a gift on the recipient's behalf when they don't claim it themselves. Covers the fallback flow, webhook cancellation, and 409 handling. ## Recipes * [Build a Rewards Experience](/guides/recipes/marketplace) — An embedded marketplace: fetch the catalog, personalize a "For You" page, present products with variant selection, validate addresses, place orders, and track fulfillment. * [Triggered Gifting](/guides/recipes/triggered-gifting) — Send gifts at scale programmatically: create gifts in bulk, handle idempotency, and manage the recipient experience through webhooks. ## Migration * [Migrating V2 to V3](/guides/migrating-v2-to-v3) — Key differences between the V2 and V3 APIs: endpoint changes, pagination, authentication, and entity model updates. Before you start, make sure you have: 1. A valid API key with the scopes each guide requires (see [Quickstart](/quickstart) and [Authentication & Security](/authentication-and-security)). 2. At least one Account with a configured Billing Method. 3. For Triggered Gifting patterns: at least one Campaign configured in the Snappy Dashboard. # Migrating from V2 to V3 Source: https://docs.snappy.com/guides/migrating-v2-to-v3 Why and how to move your Snappy integration from the V2 API to the V3 API - authentication, JSON:API conventions, the new first-class Orders model, the refreshed Marketplace catalog, and a full endpoint mapping. This guide explains **why** you'd move an existing integration from V2 to V3, and **how** to do it endpoint by endpoint. V3 is the recommended standard for all new work. *** ## Why migrate V3 is a ground-up refresh that standardizes conventions across every endpoint and unlocks surfaces that V2 never exposed. | Area | What V3 gives you | | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Catalog performance** | \~85% lower catalog latency and **static product/variant IDs** that no longer change with real-time availability - so you can cache and store IDs safely. | | **Orders** | Orders are **first-class resources**. Place an order in a **single idempotent call** instead of the V2 Campaign → Gift → claim chain, then retrieve, list, and cancel by `orderId`. | | **Consistency** | One **JSON:API** convention set across all endpoints: `filter[...]`, `include`, `sort`, `page[...]`, and `camelCase` fields everywhere. | | **Discovery** | Default sorting by popularity, enhanced **semantic search**, and a `marketplace` vs `swag` catalog split. | | **New surfaces** | Billing Methods, asynchronous bulk **Export**, Swag base products, and Accounts - all introduced in V3. | You don't have to migrate everything at once. A common path is: keep V2 gifting running, adopt **V3 Marketplace** for catalog reads first (biggest performance win, lowest risk), then move order placement to **V3 Orders**. *** ## Before you begin ### Your existing API key already works The **same `X-Api-Key`** authenticates both V2 and V3. The version lives in the URL path - there is nothing new to provision to start calling V3. ```bash theme={null} # V2 - existing call curl --request GET \ --url https://api.snappy.com/public-api/v2/accounts \ --header 'X-Api-Key: YOUR_API_KEY' # V3 - same key, just the version in the path changes curl --request GET \ --url https://api.snappy.com/public-api/v3/accounts \ --header 'X-Api-Key: YOUR_API_KEY' ``` Scopes are shared too: a key with `products:read` already reaches both `/v2/products` and `/v3/products`. See [Authentication & Security](/pages/authentication-and-security) for the full scope table. ### Generating an API key (recap) If you're starting fresh or want a dedicated key for your V3 rollout: Log in at [login.snappy.com](https://login.snappy.com/login) and go to **Sharing & Access** under **Company Settings**. Under **API Access**, click **Generate Key**, name it, and set an expiration (up to one year). Grant the minimum scopes your integration needs (e.g. `products:read`, `orders:create`, `orders:read:masked`). The same scopes apply to V2 and V3. Toggle **Expose Sensitive Information** to decide whether reads resolve to `:read:masked` or `:read:unmasked`. The secret is shown **once**. Store it securely - if you lose it, rotate the key. **Two ways to manage API keys.** Create and manage keys in the Snappy dashboard on the **Sharing & Access** page, or manage them programmatically with an existing `X-Api-Key` via the key-management endpoints (`/v2/authentication/apiKeys` or `/v3/authentication/api-keys`). See [API Keys (V3)](/modules/api/v3/api-keys/overview). ### New optional scoping header (V3) V3 endpoints accept an optional header that narrows a request to a sub-entity. Omit it to run against the full org reachable by the key. | Header | Purpose | | :------------------ | :--------------------------------------- | | `Snappy-Account-Id` | Scope the request to a specific Account. | A few endpoints - currently the V3 Collections list and by-ID endpoints - **require** `Snappy-Account-Id`. Each reference page notes when it applies. *** ## What changes at a glance Most of the migration work is mechanical: swap the version in the path and adopt V3's JSON:API query conventions. This table is your cheat sheet. | Convention | V2 | V3 | | :--------------- | :----------------------------------------------- | :----------------------------------------------------------------------------------------- | | Base path | `/public-api/v2/...` | `/public-api/v3/...` | | Field casing | Mixed (`firstname`, `orderRecipient`) | Consistent `camelCase` | | Pagination | `skip` / `limit` (offset) | `page[number]` / `page[size]` - or `page[cursor]` / `page[size]` on product list endpoints | | Related entities | `expand[]=product` (array) | `include=brand,tags` (comma-separated) | | Filtering | Bespoke per endpoint (`minBudget`, `brandName`…) | `filter[field]=...`, ranges via `filter[field][gte]` / `[lte]` | | Sorting | Bespoke per endpoint | `sort=field` / `sort=-field` (single field) | | Field selection | `fields=id,name` | `fields=id,name` (unchanged) | | Dates | ISO 8601 UTC | ISO 8601 UTC (unchanged) | | Metadata | Up to 50 pairs | Up to 50 pairs (unchanged) | ### Pagination: offset → page/cursor ```bash theme={null} # V2 - offset pagination GET /v2/orders?skip=200&limit=100 # V3 - page-number pagination (most list endpoints) GET /v3/orders?page[number]=3&page[size]=100 ``` V3 responses include a top-level `links` object - use `links.next` **verbatim** to fetch the next page rather than building URLs yourself: ```json theme={null} { "data": [ /* items */ ], "links": { "first": "/v3/orders?page[number]=1&page[size]=100", "next": "/v3/orders?page[number]=4&page[size]=100", "prev": "/v3/orders?page[number]=2&page[size]=100" } } ``` High-volume product list endpoints (`GET /v3/products`, `GET /v3/collections/{id}/products`, `GET /v3/products/{id}/variants`) use **cursor** pagination instead: `page[cursor]` + `page[size]`. `links.prev` is always `null` (no backward navigation). Never parse or construct cursors manually - follow `links.next`. ### Filtering & sorting: bespoke → JSON:API ```bash theme={null} # V2 - bespoke params GET /v2/products?minBudget=25&maxBudget=200&brandName=acme # V3 - JSON:API filter[] with range brackets, plus sort GET /v3/products?filter[price][gte]=25&filter[price][lte]=200&filter[brandName]=acme&sort=minPrice ``` Multiple `filter[...]` expressions are ANDed. Unknown filter keys return `400`. ### Hydration: `expand[]` → `include` ```bash theme={null} # V2 GET /v2/gifts/gft_abcd123?expand[]=product&expand[]=recipient # V3 GET /v3/products/prd_98765?include=brand,tags ``` *** ## Deep dive - Orders This is the biggest behavioral change in V3. In V2, an Order is a **sub-entity of a Gift**: even Direct Fulfillment goes through a two-step gift-and-claim flow, and you retrieve order state by reading the parent Gift. In V3, **Orders are first-class** - created, retrieved, listed, and cancelled directly. ### Placing an order **V2 - two steps (create gift, then claim):** ```bash theme={null} # 1. Create a gift under a campaign curl --request POST \ --url https://api.snappy.com/public-api/v2/gifts \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "campaignId": "cmp_12345", "recipients": [ { "firstname": "Jordan", "lastname": "Lee", "email": "jordan@example.com", "key": "order-9982" } ] }' # 2. Claim the gift to place the order curl --request POST \ --url https://api.snappy.com/public-api/v2/gifts/gft_abcd123/claim \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "variantId": "var_55555", "orderRecipient": { "firstName": "Jordan", "lastName": "Lee", "email": "jordan@example.com", "phone": "+15125550123", "country": "US", "address": { "addressLine1": "123 Main St", "city": "Austin", "state": "TX", "zipcode": "78701" } } }' ``` **V3 - one idempotent call:** ```bash theme={null} curl --request POST \ --url https://api.snappy.com/public-api/v3/orders \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "idempotencyKey": "order-9982", "billingMethodId": "bm_abc123", "variantId": "var_55555", "recipient": { "firstName": "Jordan", "lastName": "Lee", "email": "jordan@example.com", "phone": "+15125550123" }, "shippingAddress": { "address1": "123 Main St", "city": "Austin", "provinceCode": "TX", "postalCode": "78701", "countryCode": "US" }, "tags": ["q4-campaign"], "metadata": { "externalRecipientId": "crm-user-987" } }' ``` In V3 the recipient's contact details and the **shipping address are separate objects** (`recipient` + `shippingAddress`), whereas V2's claim payload merged them. The `idempotencyKey` (1–120 chars) is **required** in V3 and replaces the V2 recipient `key` for double-billing protection. ### Retrieving, listing, and cancelling | Operation | V2 | V3 | | :------------------- | :------------------------------------------------- | :------------------------------------------------------------------- | | Get order | `GET /v2/gifts/{giftId}` (read order off the Gift) | `GET /v3/orders/{orderId}` | | List orders | - *(no direct list; iterate Gifts)* | `GET /v3/orders` (filter by `status`, `idempotencyKey`, `createdAt`) | | Cancel order | `POST /v2/gifts/{giftId}/cancel` | `POST /v3/orders/{orderId}/cancel` | | Validate address | `POST /v2/orders/addresses/validate` | `POST /v3/orders/addresses/validate` | | Autocomplete address | `GET /v2/orders/addresses/autocomplete` | `GET /v3/orders/addresses/autocomplete` | ### Order field mapping | V2 (on the Gift / Order) | V3 (on the Order) | Notes | | :-------------------------------------------- | :------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `orders[].id` | `id` | Order is now top-level, not nested under a Gift. | | `orders[].status` (`active`, `cancelled`) | `status` (`active`, `completed`, `cancelled`, `refunded`) | V3 adds commercial lifecycle states. | | - | `fulfillmentStatus` (`unfulfilled`, `fulfilled`, `cancelled`) | New: physical status, separate from commercial `status`. | | `orderRecipient` | `recipient` | `{ firstName, lastName, email, phone }`. | | - | `shippingAddress` | Address is now its own object. | | `orderedProducts[].selectedProduct.variantId` | `lineItems[].variantId` | | | `orderedProducts[].selectedProduct.title` | `lineItems[].title` | | | `orderedProducts[].deliveryDetails` | `fulfillments[]` | Many-to-many between line items and shipments; carrier name is in `fulfillments[].trackingCompany`; tracking number/URL in `fulfillments[].trackingInfo`. | | `deliveryDetails.status` | `fulfillments[].status` | Delivery statuses renamed and switched to snake\_case (e.g. `orderReceived` → `confirmed`, `inTransit` → `in_transit`, `outForDelivery` → `out_for_delivery`). | Order-level webhooks fire alongside gift-level webhooks for V3 orders, so you can track delivery state without polling. See [Webhook Event Types](/pages/webhook-event-types). *** ## Deep dive - Marketplace (Products, Variants & Collections) V2 modeled the catalog as a single Product object with an embedded `variants` array. V3 splits **Products** (display-level) and **Variants** (orderable units) into separate resources with their own endpoints, and adds the `marketplace` vs `swag` catalog split. ### Listing products **V2 - budget is required, `types` is an array, offset pagination:** ```bash theme={null} curl --request GET \ --url 'https://api.snappy.com/public-api/v2/products?minBudget=25&maxBudget=200&brandName=acme&limit=100&skip=0' \ --header 'X-Api-Key: YOUR_API_KEY' ``` **V3 - JSON:API filters, cursor pagination:** ```bash theme={null} curl --request GET \ --url 'https://api.snappy.com/public-api/v3/products?filter[catalog]=marketplace&filter[price][gte]=25&filter[price][lte]=200&filter[brandName]=acme&include=brand,tags&sort=minPrice&page[size]=100' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ### Getting variants In V2, both `GET /v2/products/{id}` and `GET /v2/variants/{id}` returned the **full Product object with a `variants` array**. In V3 the entities are cleanly separated: ```bash theme={null} # V2 - product carries its variants inline GET /v2/products/prd_98765 # returns product + variants[] # V3 - product and variants are separate resources GET /v3/products/prd_98765 # product-level fields only GET /v3/products/prd_98765/variants # paginated list of variants GET /v3/variants/var_55555 # a single variant on its own ``` ### Key catalog differences | V2 | V3 | What to change | | :----------------------------------- | :-------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | | `minBudget` / `maxBudget` (required) | `filter[price][gte]` / `filter[price][lte]` (optional) | Budget is no longer mandatory; use price range filters when needed. | | `types: ["physical", ...]` (array) | No direct equivalent | Product type filtering is no longer supported. Use `filter[catalog]` to scope by catalog. | | Variants embedded in product | `GET /v3/products/{id}/variants` | Fetch variants from the dedicated endpoint. | | `expand[]` | `include=brand,tags` | Switch hydration syntax. | | `GET /v2/products/tags` | `GET /v3/product-tags` | Path renamed. | | IDs could change with availability | **Static IDs** | Safe to cache/store product and variant IDs. | | No catalog split | `filter[catalog]=marketplace` \| `swag` \| `giftCards` \| `donations` | Required on list endpoints (defaults to `marketplace`). | Maintaining a local mirror of the catalog? V3 adds an asynchronous, NDJSON-based **[Export API](/modules/api/v3/exports/overview)** - pair it with `stock-availability-updates` webhooks for incremental refresh instead of repeatedly paging the list endpoints. *** ## Everything else: endpoint mapping The remaining domains keep **identical functionality** in V3 - the same operations and data, relocated to `/v3/...` and re-skinned in V3 conventions (`camelCase`, `filter[...]`, `page[...]`, `include`). Migrating these is the mechanical swap described in [What changes at a glance](#what-changes-at-a-glance): change the path, update pagination/filter params, and adjust field casing. | Domain | V2 | V3 | Migration notes | | :----------------------- | :--------------------------------------------- | :-------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Campaigns** | `/v2/campaigns` | `/v3/campaigns` | Same create/read/update/estimate operations. Swap `skip`/`limit` → `page[...]` and bespoke filters → `filter[...]`. | | **Gifts** | `/v2/gifts` | `/v3/gifts` | Same lifecycle (create, retrieve, update, expire). Recipient fields become `camelCase` (`firstName`/`lastName`). | | **Recipients** | `/v2/recipients` | `/v3/recipients` | Same roster CRUD. JSON:API pagination and filtering. | | **Collections** | `/v2/collections` (+ budgets, products, count) | `/v3/collections` (+ products) | List and by-ID Collection resources; products within a Collection use cursor pagination and `products:read`. List/by-ID **require** `Snappy-Account-Id`. | | **Accounts** | `/v2/accounts` | `/v3/accounts` | Same list/retrieve/create. | | **Billing Methods** | - | `/v3/billing-methods` | New in V3: check remaining balance/expiration. Use before high-value orders. | | **Address validate** | `POST /v2/orders/addresses/validate` | `POST /v3/orders/addresses/validate` | Identical behavior. | | **Address autocomplete** | `GET /v2/orders/addresses/autocomplete` | `GET /v3/orders/addresses/autocomplete` | Identical behavior. | Scopes are unchanged across versions - e.g. `campaigns:create`, `recipients:read:masked`, `accounts:read` work for both `/v2` and `/v3`. You do **not** need to regenerate keys to call the V3 equivalents. *** ## Error handling The error object stays a **flat shape** in V3, with the same split between validation (`400`) and everything else: ```json theme={null} // Validation error (400) { "path": "campaignId", "errorCode": "INVALID_REQUEST", "message": "The campaignId provided does not exist." } // All other errors (401, 403, 404, 409, 422, 5xx) { "status": 404, "errorCode": "NOT_FOUND", "message": "The resource you requested could not be found." } ``` The main change is `errorCode`: V3 uses the **structured format** `{STATUS}_{DOMAIN}_{SEQUENCE}` (e.g. `404_PROD_001`, `403_PBLC_001`), while V2 uses symbolic codes (e.g. `NOT_FOUND`). **Switch on `errorCode`, never on `message`.** See [Request & Response Standards](/pages/request-response-standards#error-responses) and [Error Handling](/pages/error-handling). *** ## Migration checklist Repeat one existing V2 read against the `/v3/...` path with the same `X-Api-Key`. If it returns data, you're ready - no new credentials needed. Update your client's pagination (`page[...]`), filtering (`filter[...]`), sorting (`sort=`), and hydration (`include=`) helpers. Follow `links.next` verbatim. Move Products/Variants/Collections to V3 - lowest risk, biggest performance win. Split your product-and-variants reads into the separate V3 endpoints and start caching the now-static IDs. Replace the gift-create → claim flow with a single `POST /v3/orders`. Generate a stable `idempotencyKey` per order and split recipient vs `shippingAddress`. Read order state from `GET /v3/orders/{orderId}` instead of the parent Gift. Switch your error branching to the structured `errorCode` format. Apply the mechanical path/convention swap to Campaigns, Gifts, Recipients, Accounts, and address endpoints. Adopt Billing Methods to pre-check funds before high-value orders. Because the same key serves both, you can ship V3 per domain behind a flag, verify, and retire the V2 calls when you're confident. Need a hand planning your migration? [Reach out](https://www.snappy.com/book-meeting) to your Snappy contact. # Auto-claim Gifts as Fallback Source: https://docs.snappy.com/guides/patterns/auto-claim-gifts-as-fallback Auto-claim a gift on the recipient's behalf when they don't claim it themselves. Covers the fallback flow, webhook cancellation, and 409 handling. The Auto-claim as Fallback pattern handles the case where a recipient receives a gift but doesn't claim it — perhaps they missed the notification, the email went to spam, or the deadline passed. Instead of letting the gift expire, you claim it on their behalf and fulfill it directly. ## Prerequisites * API key with `gifts:create` (to create the gift) and `orders:create` (to claim it) scopes * A plan for what product to order on the recipient's behalf (store this at gift creation time) * A mechanism to watch for `gift-status-changed` webhooks ## The flow 1. **Create the gift** with `POST /v2/gifts` and store your fallback plan (product/variant + recipient address) in your own database, keyed by `giftId`. 2. **Watch `gift-status-changed` webhooks**. If the recipient claims the gift themselves, cancel your fallback plan — the gift is handled. 3. **Trigger auto-claim** after your deadline (e.g., N days after creation) if the gift is still unclaimed. ## Store the fallback plan at creation time When you create the gift, immediately record what you'll order if the recipient doesn't claim it: ```javascript JavaScript theme={null} async function createGiftWithFallback(campaignId, recipient, fallback) { const result = await sendGift(campaignId, recipient); const giftId = result.results[0].id; // Store in your DB: giftId → fallback plan await db.giftFallbacks.upsert({ giftId, variantId: fallback.variantId, shippingAddress: fallback.shippingAddress, scheduledFor: new Date(Date.now() + fallback.daysToWait * 86400000), status: "pending", }); return giftId; } ``` ## Cancel the plan if the recipient claims Subscribe to `gift-status-changed` webhooks. When a gift's status changes to `claimed` (or any terminal state that means the recipient acted), mark your fallback plan as cancelled: ```javascript JavaScript theme={null} app.post("/webhooks/snappy", async (req, res) => { const { webhookData, eventData } = req.body; if (webhookData.eventType === "gift-status-changed") { const { giftId, status } = eventData; if (["claimed", "expired", "canceled"].includes(status)) { await db.giftFallbacks.cancel(giftId); } } res.sendStatus(200); }); ``` ## Trigger auto-claim After your deadline, if the fallback plan is still `pending`, claim the gift: ```text theme={null} POST /v2/gifts/{giftId}/claim ``` ```javascript JavaScript theme={null} async function autoClaim(giftId, { variantId, shippingAddress, recipient }) { const res = await fetch(`${SNAPPY}/v2/gifts/${giftId}/claim`, { method: "POST", headers, body: JSON.stringify({ variantId, orderRecipient: { firstName: recipient.firstName, lastName: recipient.lastName, email: recipient.email, phone: recipient.phone, // E.164, required country: shippingAddress.countryCode, address: { addressLine1: shippingAddress.address1, addressLine2: shippingAddress.address2, city: shippingAddress.city, state: shippingAddress.provinceCode, zipcode: shippingAddress.postalCode, }, }, }), }); if (res.status === 409) { const error = await res.json(); return handleClaimConflict(error); } if (!res.ok) throw new Error(`Claim failed: ${res.status}`); return res.json(); } ``` ```python Python theme={null} def auto_claim(gift_id, variant_id, shipping_address, recipient): payload = { "variantId": variant_id, "orderRecipient": { "firstName": recipient["first_name"], "lastName": recipient["last_name"], "email": recipient["email"], "phone": recipient["phone"], "country": shipping_address["country_code"], "address": { "addressLine1": shipping_address["address1"], "addressLine2": shipping_address.get("address2"), "city": shipping_address["city"], "state": shipping_address["province_code"], "zipcode": shipping_address["postal_code"], }, }, } res = requests.post(f"{SNAPPY}/v2/gifts/{gift_id}/claim", headers=headers, json=payload) if res.status_code == 409: return handle_claim_conflict(res.json()) res.raise_for_status() return res.json() ``` ## 409 handling The claim endpoint returns `409 Conflict` in two distinct situations: | `errorCode` | Meaning | Action | | :------------- | :------------------------------------------------------ | :---------------------------------------------------- | | `409_PBLC_001` | Gift already has an active order — recipient claimed it | Cancel your fallback plan; order is already in flight | | `409_ORDS_001` | Gift already expired | Log and stop; gift is no longer claimable | ```javascript JavaScript theme={null} function handleClaimConflict(error) { if (error.errorCode === "409_PBLC_001") { console.log("Gift already claimed by recipient"); return { alreadyClaimed: true }; } if (error.errorCode === "409_ORDS_001") { console.log("Gift already expired"); return { expired: true }; } throw new Error(`Unexpected 409: ${JSON.stringify(error)}`); } ``` Both 409 cases are terminal — retrying will not succeed. Handle them gracefully without alerting as errors. ## Related * [Send Triggered Gifts](/guides/patterns/send-triggered-gifts) — creating the gift that this pattern wraps * [Track Order Fulfillment](/guides/patterns/track-order-fulfillment) — tracking the order after a successful claim # Bulk Catalog Export Source: https://docs.snappy.com/guides/patterns/bulk-catalog-export Export Snappy's full catalog asynchronously and maintain a local copy. Queue a job, poll for completion, download the result. The Bulk Catalog Export pattern pulls Snappy's catalog into your own data store asynchronously. You queue a job, poll until it completes, then download a compressed JSON file. This is the right pattern when you need a local copy of the catalog — for blending with other data sources, powering a search index, or serving from your own infrastructure. ## When to use Use bulk export when: * You already have local catalog infrastructure (search index, database, product schema) * You want to blend Snappy products with other sources * Your rendering layer cannot tolerate per-request API latency Use [Real-Time Catalog Access](/guides/patterns/real-time-catalog-access) instead for simpler integrations that don't need a local copy. ## The three-phase lifecycle Every export goes through **Queue → Poll → Download**: 1. **Queue** — POST to an export endpoint to start a job. You get back a job ID. 2. **Poll** — GET the job status until `status` is `completed` (or `failed`). 3. **Download** — fetch the file URL from the completed job response. ## Two export paths ### Filtered products export Export products from the full catalog with optional filters. ```text theme={null} POST /v3/products/exports ``` ```json theme={null} { "filter": { "collectionId": "col_abc123", "location": "US" } } ``` ### Collection-scoped export Export all products in a specific collection. ```text theme={null} POST /v3/collections/exports ``` ```json theme={null} { "collectionId": "col_abc123", "location": "US" } ``` Only **1 concurrent export job** is allowed at a time, shared across all export types. Creating another export while a job is active returns `409 Conflict`. Poll for completion before starting a new job. ## Poll and download Both export types share the same poll endpoint: ```text theme={null} GET /v3/exports/{exportId} ``` ```javascript JavaScript theme={null} async function waitForExport(exportId, { pollIntervalMs = 5000, timeoutMs = 300000 } = {}) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const res = await fetch(`${SNAPPY}/v3/exports/${exportId}`, { headers }); const job = await res.json(); if (job.status === "completed") return job; if (job.status === "failed") throw new Error(`Export failed: ${job.error}`); await new Promise(r => setTimeout(r, pollIntervalMs)); } throw new Error("Export timed out"); } async function downloadExport(job) { const res = await fetch(job.fileUrl); // pre-signed URL, no auth needed return res.json(); // { data: [Product, ...] } } // Full flow: async function runExport(collectionId, location = "US") { const startRes = await fetch(`${SNAPPY}/v3/products/exports`, { method: "POST", headers, body: JSON.stringify({ filter: { collectionId, location } }), }); const { id: exportId } = await startRes.json(); const job = await waitForExport(exportId); return downloadExport(job); } ``` ```python Python theme={null} import time, requests def wait_for_export(export_id, poll_interval=5, timeout=300): start = time.time() while time.time() - start < timeout: res = requests.get(f"{SNAPPY}/v3/exports/{export_id}", headers=headers) job = res.json() if job["status"] == "completed": return job if job["status"] == "failed": raise RuntimeError(f"Export failed: {job.get('error')}") time.sleep(poll_interval) raise TimeoutError("Export timed out") def download_export(job): res = requests.get(job["fileUrl"]) # pre-signed URL, no auth needed return res.json() def run_export(collection_id, location="US"): start_res = requests.post( f"{SNAPPY}/v3/products/exports", headers=headers, json={"filter": {"collectionId": collection_id, "location": location}}, ) export_id = start_res.json()["id"] job = wait_for_export(export_id) return download_export(job) ``` ## Keeping your local copy fresh After the initial bulk export, use the `stock-availability-updates` webhook to receive incremental updates instead of re-exporting the full catalog on a timer. ```text theme={null} stock-availability-updates ``` Subscribe to this event in your [Webhook](/pages/overview-and-setup) configuration. Each event carries the products whose availability or pricing changed. Update only those records in your local store. Re-run a full bulk export periodically (daily or weekly) as a safety net, and rely on webhooks for real-time freshness in between. This covers any webhook delivery failures. ## Common pitfalls | Pitfall | Fix | | :-------------------------------------------------- | :------------------------------------------------------------------------------ | | Starting a second export before the first completes | Check for active jobs before queuing; handle `409 Conflict` | | Polling too aggressively | Use 5-second intervals minimum; the export rarely completes in under 30 seconds | | Ignoring the file URL expiry | Download the file immediately after the job completes; pre-signed URLs expire | | Not subscribing to stock webhooks | Your local copy will drift; pair bulk export with `stock-availability-updates` | ## Related * [Real-Time Catalog Access](/guides/patterns/real-time-catalog-access) — simpler alternative without local infrastructure * [Swag Products Access](/guides/patterns/swag-products-access) — swag-specific catalog access # Place Orders Source: https://docs.snappy.com/guides/patterns/place-orders Place direct-fulfillment orders via POST /v3/orders. Covers address validation, idempotency, bulk placement, and the Swag variant. The Place Orders pattern creates direct-fulfillment orders through the V3 orders API. A single `POST /v3/orders` call creates an order that Snappy fulfills and ships to the recipient — no two-step gift/claim flow required. ## Prerequisites * API key with `orders:create` scope * A configured billing method under the ordering account * `accountId`, `billingMethodId`, and `variantId` ready ## Physical vs digital variants The shipping address requirement depends on the variant type: * **Physical variants** (`variant.shippingRequired: true`) — physical merchandise, apparel, experiences. Require the full shipping address: `address1`, `city`, `provinceCode`, `postalCode`, and `countryCode`. `address2` is optional. * **Digital variants** (`variant.shippingRequired: false`) — gift cards, e-vouchers, and other card-based rewards. Require only `countryCode`, used for pricing and localization. No street/city/postal fields needed since there's no physical delivery. Snappy validates the address at runtime against the variant's `shippingRequired` value — if you send only `countryCode` for a physical variant, the request is rejected. ## Pre-flight: validate the address For physical variants, run the recipient's address through Snappy's validation endpoint before placing the order. This catches undeliverable addresses before the order is created. ```text theme={null} POST /v3/orders/addresses/validate ``` ```javascript JavaScript theme={null} async function validateAddress(address) { const res = await fetch(`${SNAPPY}/v3/orders/addresses/validate`, { method: "POST", headers, body: JSON.stringify({ address }), }); const result = await res.json(); if (!result.valid) throw new Error(`Invalid address: ${result.message}`); return result.normalizedAddress; } ``` For address autocomplete as the user types, use `GET /v3/orders/addresses/autocomplete?filter[address]=...&filter[country]=US`. Skip address validation for digital variants — there's no physical address to validate. `countryCode` alone is sufficient. ## Place the order ```text theme={null} POST /v3/orders ``` The account is scoped via header, not body: ```javascript JavaScript theme={null} async function placeOrder({ variantId, recipient, shippingAddress, idempotencyKey, tags, metadata }) { const res = await fetch(`${SNAPPY}/v3/orders`, { method: "POST", headers: { ...headers, "Snappy-Account-Id": process.env.SNAPPY_ACCOUNT_ID, }, body: JSON.stringify({ billingMethodId: process.env.SNAPPY_BILLING_METHOD_ID, variantId, recipient: { firstName: recipient.firstName, lastName: recipient.lastName, email: recipient.email, phone: recipient.phone, // E.164, required }, shippingAddress: { // For physical variants, include the full address: address1: shippingAddress.address1, address2: shippingAddress.address2, // optional city: shippingAddress.city, provinceCode: shippingAddress.provinceCode, postalCode: shippingAddress.postalCode, countryCode: shippingAddress.countryCode, // For digital variants, `countryCode` alone is enough. }, idempotencyKey, // top-level, 1–120 chars, stable per order attempt tags, // optional string[]: for your reporting and filtering metadata, // optional object: roundtrips on status-changed webhooks }), }); if (!res.ok) throw new Error(`Order failed ${res.status}: ${await res.text()}`); const { data } = await res.json(); return data; // { id, status: "active", trackingLink } } ``` ```python Python theme={null} def place_order(variant_id, recipient, shipping_address, idempotency_key, tags=None, metadata=None): payload = { "billingMethodId": os.environ["SNAPPY_BILLING_METHOD_ID"], "variantId": variant_id, "recipient": { "firstName": recipient["first_name"], "lastName": recipient["last_name"], "email": recipient["email"], "phone": recipient["phone"], }, # For physical variants, include the full address. # For digital variants (e.g. gift cards), send just {"countryCode": ...}. "shippingAddress": { "address1": shipping_address["address1"], "address2": shipping_address.get("address2"), "city": shipping_address["city"], "provinceCode": shipping_address["province_code"], "postalCode": shipping_address["postal_code"], "countryCode": shipping_address["country_code"], }, "idempotencyKey": idempotency_key, } if tags: payload["tags"] = tags if metadata: payload["metadata"] = metadata order_headers = {**headers, "Snappy-Account-Id": os.environ["SNAPPY_ACCOUNT_ID"]} res = requests.post(f"{SNAPPY}/v3/orders", headers=order_headers, json=payload) res.raise_for_status() return res.json()["data"] ``` ## Idempotency The `idempotencyKey` (top-level field, 1–120 chars) makes retries safe. Sending the same key twice returns the original order — no duplicate, no double charge. **Derive from stable identifiers:** ```javascript JavaScript theme={null} // Good: stable per user per redemption event const key = `user-${userId}-redemption-${redemptionId}`; // Bad: changes on retry const key = crypto.randomUUID(); ``` Reuse the same key on all retry attempts for the same order. ## Cancel an order ```text theme={null} DELETE /v3/orders/{orderId} ``` Orders can only be canceled before fulfillment begins. Check `status` on the order before attempting cancellation. ## Bulk placement For placing multiple orders (e.g., a batch send), use bounded concurrency to stay within rate limits: ```javascript JavaScript theme={null} async function placeOrdersBatch(orderRequests, { concurrency = 5 } = {}) { const results = []; for (let i = 0; i < orderRequests.length; i += concurrency) { const batch = orderRequests.slice(i, i + concurrency); const batchResults = await Promise.all(batch.map(req => placeOrder(req))); results.push(...batchResults); } return results; } ``` ## Swag orders Swag orders use the same `POST /v3/orders` endpoint but require: * `Snappy-Account-Id` header (same as standard orders) * A `variantId` from a swag product (fetched with `filter[catalog]=swag`) Swag variants are always physical, so send the full shipping address. ## Failure table | HTTP | Meaning | Action | | :---- | :----------------------------------------------------- | :---------------------------------------------------------------------- | | `422` | Insufficient funds | Alert your ops team; this is a billing issue | | `422` | Variant not available in country | Offer alternatives via recommendations endpoint | | `404` | Variant, billing method, or account not found | Config bug on your side — log and fix | | `400` | Physical variant missing required address fields | Ensure the full address is sent when `variant.shippingRequired` is true | | `400` | Invalid body (bad phone, malformed address) | Validate at form entry; catch before reaching this call | | `409` | Duplicate order (same idempotency key, different body) | Check your key derivation — keys must be stable | ## Related * [Real-Time Catalog Access](/guides/patterns/real-time-catalog-access) — fetching the variant ID and its `shippingRequired` before placing the order * [Track Order Fulfillment](/guides/patterns/track-order-fulfillment) — subscribe to webhooks after placing * [Build a Rewards Experience](/guides/recipes/marketplace) — end-to-end recipe using this pattern # Real-Time Catalog Access Source: https://docs.snappy.com/guides/patterns/real-time-catalog-access Serve Snappy's catalog live from the V3 API on demand, with optional edge caching. The Real-Time Catalog Access pattern serves Snappy's V3 catalog API on demand, with data fetched live from Snappy (and optionally cached at your edge). No local mirror, no nightly sync. Snappy is the source of truth. ## When to use **Use real-time when:** * You want live pricing and availability without sync lag * You don't have local catalog infrastructure to maintain * You want the simplest possible integration path **Use [Bulk Catalog Export](/guides/patterns/bulk-catalog-export) instead when:** * You already have a local catalog or search index you want to blend Snappy products into * You need offline access or want to blend with other data sources * Your rendering layer can't tolerate API latency on every request ## Two paths ### Collection-based (recommended) Fetch products scoped to a specific collection — the usual pattern for partners who have a dedicated collection for their program. ```text theme={null} GET /v3/collections/{collectionId}/products ``` This endpoint uses **cursor pagination**. Follow `links.next` until it returns `null`. ```javascript JavaScript theme={null} async function fetchCollection(collectionId, { location = "US" } = {}) { const products = []; let cursor = null; do { const url = new URL(`${SNAPPY}/v3/collections/${collectionId}/products`); url.searchParams.set("location", location); // ISO 3166-1 alpha-2, required url.searchParams.set("include", "brand,tags"); url.searchParams.set("fields", "priceRange,variantsCount"); url.searchParams.set("page[size]", "100"); // max 300 if (cursor) url.searchParams.set("page[cursor]", cursor); const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Snappy ${res.status}: ${await res.text()}`); const body = await res.json(); products.push(...body.data); cursor = body.links?.next ? new URL(body.links.next).searchParams.get("page[cursor]") : null; } while (cursor); return products; } ``` ```python Python theme={null} from urllib.parse import urlparse, parse_qs import requests def fetch_collection(collection_id, location="US"): products = [] cursor = None while True: params = { "location": location, "include": "brand,tags", "fields": "priceRange,variantsCount", "page[size]": 100, } if cursor: params["page[cursor]"] = cursor res = requests.get( f"{SNAPPY}/v3/collections/{collection_id}/products", headers=headers, params=params, ) res.raise_for_status() body = res.json() products.extend(body["data"]) next_link = (body.get("links") or {}).get("next") if not next_link: break cursor = parse_qs(urlparse(next_link).query)["page[cursor]"][0] return products ``` ### Filter-based Fetch from the full catalog with filters, without scoping to a collection. ```text theme={null} GET /v3/products?filter[collectionId]=...&location=US ``` Useful when you want to merge multiple collections, apply tag or price filters globally, or access the catalog without a fixed `collectionId`. ## After picking a product Once a user selects a product, fetch the full detail and its variants: ```javascript JavaScript theme={null} // Product detail (options matrix for variant pickers) const product = await fetch(`${SNAPPY}/v3/products/${productId}?fields=options,priceRange,variantsCount&include=brand,tags`, { headers }).then(r => r.json()); // Orderable variants const { data: variants } = await fetch(`${SNAPPY}/v3/products/${productId}/variants?fields=price`, { headers }).then(r => r.json()); ``` The product detail endpoint returns the object directly (no `data` wrapper). The variants endpoint returns `{ data, links }` with page-number pagination. ### Resolve the selected variant at checkout ```javascript JavaScript theme={null} // `selection` maps option name → chosen value, e.g. { color: "black", size: "l" } function resolveVariant(variants, selection) { return variants.find((v) => Object.entries(selection).every( ([level, value]) => v.selectedOptions[level] === value ) ); } const variant = resolveVariant(variants, { color: "black", size: "l" }); // variant.id goes into POST /v3/orders ``` Pre-select each option's `firstSelectableVariant` on page load to avoid dead-end combinations. ## Caching guidance Fetching live on every render is fine for most traffic levels. Add a short cache at your edge if you need to reduce latency or API call volume: | Data | Recommended TTL | | :--------------------------------------------------------- | :-------------- | | Product list (collection) | 5–15 minutes | | Product detail + variants | 5–15 minutes | | Product tags | 1 hour | | Availability (`GET /v3/variants/{variantId}/availability`) | 2–5 minutes | Cache by `(collectionId, location, page[cursor])` for list pages, and by `(productId, location)` for detail pages. ## Common pitfalls | Pitfall | Fix | | :--------------------------------------------- | :---------------------------------------------------------------------------------- | | Serving stale out-of-stock products | Cache with short TTL; check `GET /v3/variants/{variantId}/availability` at checkout | | Missing `location` param | Always pass `location` (ISO 3166-1 alpha-2) — it affects availability and pricing | | Rendering a product with no selectable variant | Pre-select `firstSelectableVariant`; never show a picker with no valid default | | Exceeding rate limits on page load | Cache aggressively, batch requests, use `page[size]=300` to minimize pages | ## Related * [Bulk Catalog Export](/guides/patterns/bulk-catalog-export) — for local catalog infrastructure * [Place Orders](/guides/patterns/place-orders) — the next step after catalog browsing * [Build a Rewards Experience](/guides/recipes/marketplace) — end-to-end recipe using this pattern # Send Triggered Gifts Source: https://docs.snappy.com/guides/patterns/send-triggered-gifts Create gifts programmatically with POST /v2/gifts. Covers bulk chunking, idempotency keys, and partial failure handling. The Send Triggered Gifts pattern creates gifts programmatically for a list of recipients — triggered by events like hiring, anniversaries, or milestone completions. Gifts are created in the V2 gifting API and sent to recipients who choose their own product from a curated selection. ## Prerequisites * API key with `gifts:create` scope * At least one Campaign configured in the Snappy Dashboard * The `campaignId` for the campaign you're sending under ## Create gifts ```text theme={null} POST /v2/gifts ``` Pass a `recipients` array — each entry creates one gift for one recipient. The V2 gifting API uses lowercase field names for recipient names: `firstname` and `lastname` (not camelCase). This differs from the V3 orders API which uses `firstName` / `lastName`. ```javascript JavaScript theme={null} async function sendGifts(campaignId, recipients) { const res = await fetch(`${SNAPPY}/v2/gifts`, { method: "POST", headers, body: JSON.stringify({ campaignId, recipients: recipients.map(r => ({ firstname: r.firstName, // lowercase - V2 API lastname: r.lastName, // lowercase - V2 API email: r.email, key: r.stableId, // idempotency key - see below })), }), }); return res.json(); } ``` ```python Python theme={null} def send_gifts(campaign_id, recipients): payload = { "campaignId": campaign_id, "recipients": [ { "firstname": r["first_name"], # lowercase - V2 API "lastname": r["last_name"], # lowercase - V2 API "email": r["email"], "key": r["stable_id"], # idempotency key - see below } for r in recipients ], } res = requests.post(f"{SNAPPY}/v2/gifts", headers=headers, json=payload) return res.json() ``` ## Chunking for large lists The V2 gifts endpoint has a maximum recipients per request. For large sends, split into chunks of 100 and send sequentially (or with bounded concurrency): ```javascript JavaScript theme={null} const CHUNK_SIZE = 100; async function sendGiftsInBulk(campaignId, allRecipients) { const results = []; for (let i = 0; i < allRecipients.length; i += CHUNK_SIZE) { const chunk = allRecipients.slice(i, i + CHUNK_SIZE); const result = await sendGifts(campaignId, chunk); results.push(result); } return results; } ``` ```python Python theme={null} CHUNK_SIZE = 100 def send_gifts_in_bulk(campaign_id, all_recipients): results = [] for i in range(0, len(all_recipients), CHUNK_SIZE): chunk = all_recipients[i:i + CHUNK_SIZE] result = send_gifts(campaign_id, chunk) results.append(result) return results ``` ## Idempotency Each recipient entry accepts a `key` field — a stable, permanent identifier for that recipient in the context of this send. Snappy uses this to deduplicate: sending the same `key` twice does not create a second gift. **Derive `key` from stable identifiers**, not from random values or timestamps: ```javascript JavaScript theme={null} // Good: stable per recipient per campaign const key = `campaign-${campaignId}-user-${userId}`; // Bad: changes on retry, defeats idempotency const key = `${Date.now()}-${Math.random()}`; ``` Keys must be unique within a campaign. A good pattern: `{campaignId}-{userId}` or `{campaignId}-{employeeId}`. ## Partial failure handling The API may succeed for some recipients and fail for others in the same request. The response includes per-recipient status: | Outcome | Field | Action | | :----------------- | :---------------------------------- | :-------------------------------------- | | Success | `gifts[].id` present | Record gift ID for tracking | | Validation error | `errors[].recipient` | Fix the data and re-send that recipient | | Duplicate key | `errors[].code === "duplicate_key"` | Already sent — no action needed | | Campaign not found | Top-level `404` | Check `campaignId` | Always inspect `errors` in the response and re-run the failed recipients rather than the full list. Since each recipient has a `key`, re-running is safe — duplicates are silently ignored. ```javascript JavaScript theme={null} function extractFailures(result) { return (result.errors ?? []).filter(e => e.code !== "duplicate_key"); } ``` ## Related * [Auto-claim Gifts as Fallback](/guides/patterns/auto-claim-gifts-as-fallback) — what to do when a recipient doesn't claim their gift * [Track Order Fulfillment](/guides/patterns/track-order-fulfillment) — subscribe to webhooks once the gift is claimed # Swag Products Access Source: https://docs.snappy.com/guides/patterns/swag-products-access Access Snappy's swag catalog. Requires the Snappy-Account-Id header and swag-specific filters. The Swag Products Access pattern covers how to browse and filter Snappy's swag catalog — branded merchandise that ships to recipients. Swag access differs from the standard marketplace in one key way: all calls require the `Snappy-Account-Id` header, scoping requests to a specific account's swag configuration. ## Prerequisites * A Snappy account with swag enabled * An API key with `products:read` and `collections:read` scopes * The `accountId` for the account whose swag you're accessing ## The Snappy-Account-Id header Every swag API call must include: ```http theme={null} Snappy-Account-Id: ``` Without it, swag products will not appear in results. This header scopes the request to an account's swag catalog and pricing configuration. ## Three endpoints ### 1. List Swag collections Retrieve collections tagged as swag to discover what's available for your account. ```text theme={null} GET /v3/collections?filter[tag]=swag ``` ```javascript JavaScript theme={null} const res = await fetch(`${SNAPPY}/v3/collections?filter[tag]=swag`, { headers: { ...headers, "Snappy-Account-Id": accountId, }, }); const { data: swagCollections } = await res.json(); ``` ### 2. List products in a Swag collection Once you have a collection ID, fetch its products with the swag catalog filter: ```text theme={null} GET /v3/collections/{collectionId}/products?filter[catalog]=swag ``` ```javascript JavaScript theme={null} const url = new URL(`${SNAPPY}/v3/collections/${collectionId}/products`); url.searchParams.set("filter[catalog]", "swag"); url.searchParams.set("location", "US"); url.searchParams.set("include", "brand,tags"); url.searchParams.set("page[size]", "100"); const res = await fetch(url, { headers: { ...headers, "Snappy-Account-Id": accountId, }, }); const { data: products } = await res.json(); ``` ### 3. List all Swag products (across collections) To fetch all swag products without filtering by collection: ```text theme={null} GET /v3/products?filter[catalog]=swag ``` ```python Python theme={null} params = { "filter[catalog]": "swag", "location": "US", "include": "brand,tags", "page[size]": 100, } res = requests.get( f"{SNAPPY}/v3/products", headers={**headers, "Snappy-Account-Id": account_id}, params=params, ) products = res.json()["data"] ``` ## Swag base products (coming soon) Snappy is adding a Base Products API for swag that exposes the underlying product catalog before account-specific configuration is applied. This is useful for building swag configurators and admin tooling. See the [API Reference](/modules/api/v3/base-products/overview) for the current spec. ## Common pitfalls | Pitfall | Fix | | :----------------------------------------- | :------------------------------------------------------------------------------------- | | Missing `Snappy-Account-Id` header | All swag calls require this header; requests without it return empty results or errors | | Omitting `filter[catalog]=swag` | Without this filter, swag products may not appear in standard product queries | | Using swag products with the wrong account | Swag pricing and availability is account-scoped; always pass the correct `accountId` | ## Related * [Real-Time Catalog Access](/guides/patterns/real-time-catalog-access) — standard marketplace catalog pattern * [Place Orders](/guides/patterns/place-orders) — placing swag orders (also requires `Snappy-Account-Id`) # Track Order Fulfillment Source: https://docs.snappy.com/guides/patterns/track-order-fulfillment Subscribe to order webhooks and respond to fulfillment lifecycle events. Covers all four event types and delivery status transitions. The Track Order Fulfillment pattern subscribes to Snappy's order webhooks and keeps your system — and the recipient — up to date as the order moves from confirmed to delivered. This is push-based: Snappy calls your endpoint on each status change. ## The four order events | Event | When it fires | | :------------------------------ | :-------------------------------------------------------------- | | `order-status-changed` | Order status changes (e.g., `active` → `fulfilled`) | | `order-delivery-status-changed` | Delivery status transitions (e.g., `processing` → `in_transit`) | | `order-canceled` | Order is canceled before fulfillment | | `order-out-of-stock` | A line item is out of stock and cannot be fulfilled | Subscribe to these in your [Webhook configuration](/pages/overview-and-setup). ## Delivery status values V3 delivery statuses use **snake\_case**, not camelCase. Use `in_transit` and `out_for_delivery` — not `inTransit` / `outForDelivery`. | Status | Meaning | | :----------------- | :---------------------------------------- | | `confirmed` | Order received by the vendor | | `processing` | Being prepared for shipment | | `in_transit` | Shipped and in transit — tracking is live | | `out_for_delivery` | Out for delivery today | | `delivered` | Arrived at the recipient's address | ## Metadata roundtrips `metadata` (the arbitrary object you pass at order creation) roundtrips on `order-status-changed` and `order-delivery-status-changed` events, but **not** on `order-canceled` or `order-out-of-stock`. If you need to correlate those events with your internal records, store the mapping in your own database by `orderId`. ## Webhook handler ```javascript JavaScript theme={null} // Express handler app.post("/webhooks/snappy", express.json(), async (req, res) => { // Acknowledge immediately — process async res.sendStatus(200); const { webhookData, eventData } = req.body; switch (webhookData.eventType) { case "order-delivery-status-changed": { await db.orders.updateDeliveryStatus(eventData.orderId, eventData.status); if (eventData.status === "in_transit") { await notifyRecipient(eventData.orderId, { message: "Your order is on its way!", trackingLink: eventData.trackingLink, }); } if (eventData.status === "delivered") { await notifyRecipient(eventData.orderId, { message: "Your order has arrived!" }); } break; } case "order-status-changed": { await db.orders.updateStatus(eventData.orderId, eventData.status, eventData.metadata); break; } case "order-canceled": { await db.orders.markCanceled(eventData.orderId); await notifyRecipient(eventData.orderId, { message: "Your order was canceled." }); break; } case "order-out-of-stock": { await db.orders.markOutOfStock(eventData.orderId); await alertOpsTeam(eventData.orderId, "Out of stock"); break; } } }); ``` ```python Python theme={null} # Flask handler @app.post("/webhooks/snappy") def snappy_webhook(): # Acknowledge immediately body = request.get_json() process_webhook.delay(body) # push to background task queue return "", 200 def handle_webhook(body): webhook_data = body["webhookData"] event_data = body["eventData"] event_type = webhook_data["eventType"] if event_type == "order-delivery-status-changed": db.update_delivery_status(event_data["orderId"], event_data["status"]) if event_data["status"] == "in_transit": notify_recipient(event_data["orderId"], tracking_link=event_data.get("trackingLink")) elif event_data["status"] == "delivered": notify_recipient(event_data["orderId"], message="Your order has arrived!") elif event_type == "order-status-changed": db.update_order_status(event_data["orderId"], event_data["status"], event_data.get("metadata")) elif event_type == "order-canceled": db.mark_canceled(event_data["orderId"]) elif event_type == "order-out-of-stock": alert_ops_team(event_data["orderId"]) ``` ## On-demand order fetch You can also pull order state at any time without relying on webhooks: ```text theme={null} GET /v3/orders/{orderId} ``` This is useful for reconciliation, initial page loads, or as a fallback if your webhook handler missed an event. ```javascript JavaScript theme={null} async function getOrder(orderId) { const res = await fetch(`${SNAPPY}/v3/orders/${orderId}`, { headers }); if (!res.ok) throw new Error(`GET order failed: ${res.status}`); return res.json(); // full order object } ``` ## Reliability tips * **Acknowledge fast, process async.** Return `200` immediately and do the work in a background job. Slow handlers risk timeouts and missed retries from Snappy. * **Handle duplicate deliveries.** Webhooks may be delivered more than once. Make your handlers idempotent — updating a DB record to the same status is a no-op. * **Use `GET /v3/orders/{orderId}` for reconciliation.** On startup or after downtime, poll recent orders to catch any events you missed. ## Related * [Place Orders](/guides/patterns/place-orders) — creating the order that these webhooks track * [Auto-claim Gifts as Fallback](/guides/patterns/auto-claim-gifts-as-fallback) — a pattern that also relies on webhooks for coordination # Build a Rewards Experience Source: https://docs.snappy.com/guides/recipes/marketplace A field guide to building a recognition or loyalty rewards storefront on Snappy - one that people actually love spending their points in. At Snappy, we believe a reward should feel like a moment, not a transaction. We've watched millions of gifts get chosen, and the pattern is clear: the experiences people treasure are the ones that feel personal and tangible - a beautifully shot product they'd never have bought themselves, a "this is so me" recommendation, a weekend away to remember. The best-in-class rewards programs we see share two ingredients: a rich mix of **tangible rewards and experiences** alongside the usual options, and **personalization** that makes each person feel seen. Get those two right and a rewards storefront stops feeling like a points-cashout screen and starts feeling like a treat. This recipe walks you through building exactly that - end to end, with the Snappy API doing the heavy lifting. We'll cover fetching the catalog, personalizing a "For You" page, browsing and searching, product detail pages with variants, checkout with address validation, placing orders, and tracking fulfillment. Along the way we'll share the UX opinions we've formed from all those gift choices. Snappy's catalog spans physical products, experiences, gift cards, and donations - the full range, so there's something for everyone. This guide leans into the tangible and experiential, because that's where the delight compounds. Think of it as where to point the spotlight, with everything else close at hand. This recipe assembles several standalone patterns. For deeper technical detail on any step, see [Real-Time Catalog Access](/guides/patterns/real-time-catalog-access), [Place Orders](/guides/patterns/place-orders), and [Track Order Fulfillment](/guides/patterns/track-order-fulfillment). ## What you're building A storefront with four surfaces, each backed by Snappy endpoints: A personalized landing page of curated picks. The full browsable store - categories, search, filters, sort. Rich pages with galleries and up to three levels of variants. Recipient details and a shipping address, validated as they type. And underneath it all: **one call to place the order**, plus webhooks to track it home. *** ## Before you start The only real setup task on your side is **billing**. Snappy fulfills real products to real doorsteps, so before you can place an order you need a Billing Method configured (see [Billing Methods](/billing-methods-overview)). In your Snappy account, set up a **billing method** under the account you'll be ordering against. This is a one-time setup in the dashboard - no code required. You'll need three identifiers as you build: * **`accountId`** - the account orders are billed to. At checkout you pass it as the `Snappy-Account-Id` header (not in the body). * **`billingMethodId`** - the billing method within that account. Discover available methods with `GET /v3/billing-methods`. * **`collectionId`** - the product collection you're merchandising from. Most partners get a collection scoped to their program; if you're not sure which one is yours, ask your Snappy contact. All requests authenticate with an `X-Api-Key` header. Your key carries scopes - for this guide you'll want `products:read`, `collections:read`, `billingMethods:read`, `orders:create`, `orders:read:masked`, and `orders:read:unmasked`. See [Authentication & Security](/authentication-and-security). Treat your API key like a password. Keep it server-side, never ship it in your frontend bundle or mobile app, and rotate it if it's ever exposed. Every call in this guide should originate from your backend. All requests share the same base URL and auth header: ```javascript JavaScript theme={null} const SNAPPY = "https://api.snappy.com/public-api"; const headers = { "X-Api-Key": process.env.SNAPPY_API_KEY, "Content-Type": "application/json", // Optional but appreciated - tells us how the call was made. "Request-Source": "api_native", }; ``` ```python Python theme={null} import os SNAPPY = "https://api.snappy.com/public-api" headers = { "X-Api-Key": os.environ["SNAPPY_API_KEY"], "Content-Type": "application/json", # Optional but appreciated - tells us how the call was made. "Request-Source": "api_native", } ``` ```bash cURL theme={null} export SNAPPY="https://api.snappy.com/public-api" export SNAPPY_API_KEY="sk_live_..." # Pass on every request: # -H "X-Api-Key: $SNAPPY_API_KEY" # -H "Request-Source: api_native" ``` *** ## Step 1 - Fetch the catalog Your storefront needs products. Snappy gives you a collection of them through the v3 products endpoint: ```text theme={null} GET /v3/collections/{collectionId}/products ``` There are two ways to get products onto your pages, and the right one depends on your platform: * **Fetch live** - call Snappy when you render a page (with a short cache). Simplest to build, and you never have to think about whether prices or availability are current - they always are, straight from the source. * **Bulk import** - pull the whole catalog into your own database and serve from there. A natural fit if you already have catalog infrastructure (your own search index, merchandising tools, an existing product schema). The tradeoff: your copy can drift from ours, so you'll subscribe to **webhooks** to keep availability and pricing in sync (more on that below). **For this guide we'll fetch live**, because it's the simplest path and it sidesteps the whole question of keeping prices and stock up to date. If a bulk import fits your platform better, skip to [Keeping a local catalog in sync](#keeping-a-local-catalog-in-sync) for the webhook side of the story - everything else in this guide applies either way. Here's a paginated fetch of a collection. The endpoint uses **cursor pagination** - follow `links.next` until it's null. ```javascript JavaScript theme={null} async function fetchCollection(collectionId, { location = "US" } = {}) { const products = []; let cursor = null; do { const url = new URL(`${SNAPPY}/v3/collections/${collectionId}/products`); url.searchParams.set("location", location); // ISO 3166-1 alpha-2 url.searchParams.set("include", "brand,tags"); // hydrate brand + tags url.searchParams.set("fields", "priceRange,variantsCount"); url.searchParams.set("page[size]", "100"); // 1 to 300, default 100 if (cursor) url.searchParams.set("page[cursor]", cursor); const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Snappy ${res.status}`); const body = await res.json(); products.push(...body.data); cursor = body.links?.next ? new URL(body.links.next).searchParams.get("page[cursor]") : null; } while (cursor); return products; } ``` ```python Python theme={null} from urllib.parse import urlparse, parse_qs import requests def fetch_collection(collection_id, location="US"): products = [] cursor = None while True: params = { "location": location, # ISO 3166-1 alpha-2 "include": "brand,tags", # hydrate brand + tags "fields": "priceRange,variantsCount", "page[size]": 100, # 1 to 300, default 100 } if cursor: params["page[cursor]"] = cursor res = requests.get( f"{SNAPPY}/v3/collections/{collection_id}/products", headers=headers, params=params, ) res.raise_for_status() body = res.json() products.extend(body["data"]) next_link = (body.get("links") or {}).get("next") if not next_link: break cursor = parse_qs(urlparse(next_link).query)["page[cursor]"][0] return products ``` ```bash cURL theme={null} curl -G "$SNAPPY/v3/collections/$COLLECTION_ID/products" \ -H "X-Api-Key: $SNAPPY_API_KEY" \ --data-urlencode "location=US" \ --data-urlencode "include=brand,tags" \ --data-urlencode "fields=priceRange,variantsCount" \ --data-urlencode "page[size]=100" # Follow the `links.next` URL for the next page. ``` Each product comes back shaped like this: ```json theme={null} { "id": "prod_8fawE03MlR", "title": "Ember Travel Mug²", "createdAt": "2026-04-14T19:12:33Z", "catalog": "marketplace", "type": "physical", "category": { "fullName": "Home & Kitchen / Drinkware" }, "media": [{ "type": "image", "src": "https://cdn.snappy.com/..." }], "brand": { "id": "brnd_01", "name": "Ember" }, "tags": [{ "id": "tag_travel", "name": "Travel" }], "priceRange": { "min": { "amount": 39.95, "currency": "USD" }, "max": { "amount": 39.95, "currency": "USD" } }, "variantsCount": 3 } ``` A few fields earn their keep right away: * **`media`** drives your product imagery - use the first image as the card thumbnail. * **`category.fullName`** is a breadcrumb you can split for category filters. * **`tags`** are your personalization fuel (next step). * **`type`** tells you whether this is `physical`, `digital`, `giftCard`, or `donation` (a single value, not an array) - useful for steering merchandising. * **`priceRange`** is what you'll map into points. ### Keeping a local catalog in sync If you've chosen the bulk-import route - pulling the catalog into your own database with the Export endpoints rather than fetching live - there's one job you take on in return: keeping your copy fresh. A product that's out of stock or repriced on our side should reflect that on yours, ideally within minutes. Snappy handles this with **webhooks**. Subscribe to the stock availability event and update your local records as changes roll in, instead of re-importing the whole catalog on a timer. ```text theme={null} stock-availability-updates ``` See [Webhook Event Types](/webhook-event-types#catalog--stock-events) for the full payload shape and the current handling rules. Fetching live (the path this guide follows) skips all of this - there's nothing to keep in sync because every render reads the source of truth. Reach for bulk import + webhooks when you have real reasons to own a local copy, not by default. *** ## Step 2 - Model your points economy Snappy's catalog is priced in real currency. Your users think in points - recognition points, loyalty tier points, anniversary credits, whatever your program calls them. The bridge between the two is a ratio you decide and own. Pick a ratio that feels generous and legible. A round number is your friend: ```javascript JavaScript theme={null} // You own this number. 100 points = $1 is easy to reason about. const POINTS_PER_DOLLAR = 100; const toPoints = (usd) => Math.round(usd * POINTS_PER_DOLLAR); const toDollars = (points) => points / POINTS_PER_DOLLAR; // Show every product's cost in points: const pointsCost = toPoints(product.priceRange.min.amount); ``` ```python Python theme={null} # You own this number. 100 points = $1 is easy to reason about. POINTS_PER_DOLLAR = 100 def to_points(usd): return round(usd * POINTS_PER_DOLLAR) def to_dollars(pts): return pts / POINTS_PER_DOLLAR # Show every product's cost in points: points_cost = to_points(product["priceRange"]["min"]["amount"]) ``` The real magic is filtering the catalog by **what a person can actually afford**. Translate their points balance into a max budget and pass it straight to the API, so you can recommend products that they can redeem instantly: ```javascript JavaScript theme={null} const balancePoints = user.pointsBalance; // e.g. 12,450 const maxBudgetUsd = toDollars(balancePoints); // 124.50 const url = new URL(`${SNAPPY}/v3/collections/${collectionId}/products`); url.searchParams.set("filter[price][lte]", maxBudgetUsd.toFixed(2)); // Optionally hide the truly tiny stuff so the page feels aspirational: url.searchParams.set("filter[price][gte]", "15"); ``` ```python Python theme={null} balance_points = user["points_balance"] # e.g. 12,450 max_budget_usd = to_dollars(balance_points) # 124.50 params = { "filter[price][lte]": f"{max_budget_usd:.2f}", # Optionally hide the truly tiny stuff so the page feels aspirational: "filter[price][gte]": "15", } ``` Showing an "almost there" rail of items slightly above someone's balance is a lovely nudge - it gives points a sense of momentum. *** ## Step 3 - A personalized "For You" page The "For You" page is where you earn the user's attention. Instead of dropping them into a 2,000-item grid, you greet them with a handful of curated rails that feel hand-picked. The raw material is **tags**. Snappy tags span categories ("Home & Kitchen"), occasions ("Birthday"), and values ("Sustainable", "Women-Owned"). Pull the list: ```text theme={null} GET /v3/product-tags ``` ```javascript JavaScript theme={null} async function fetchTags(search) { const url = new URL(`${SNAPPY}/v3/product-tags`); if (search) url.searchParams.set("title", search); // min 3 chars url.searchParams.set("page[size]", "100"); // 1 to 100, default 100 const res = await fetch(url, { headers }); const { data } = await res.json(); return data; // [{ id, name }, ...] } ``` ```python Python theme={null} def fetch_tags(search=None): params = {"page[size]": 100} # 1 to 100, default 100 if search: params["title"] = search # min 3 chars res = requests.get(f"{SNAPPY}/v3/product-tags", headers=headers, params=params) return res.json()["data"] # [{ id, name }, ...] ``` ```bash cURL theme={null} curl -G "$SNAPPY/v3/product-tags" \ -H "X-Api-Key: $SNAPPY_API_KEY" \ --data-urlencode "page[size]=100" ``` `GET /v3/product-tags` returns a paginated `{ data, links }` envelope (page-number pagination, `page[size]` max 100). Follow `links.next` if you need more than one page. Now group a few meaningful tags into **interests** - human-friendly buckets like "Wellness & Self-Care," "Travel & Adventure," "Food & Dining," "Home & Living." Each interest maps to one or more tags, and each becomes a rail. Build a rail by fetching the collection with `include=tags` and matching on tag names client-side (the standalone `GET /v3/products` endpoint also supports server-side `filter[tagId]` / `filter[brandId]` if you'd rather filter the whole catalog by ID): ```javascript JavaScript theme={null} // Your own mapping from a friendly interest to Snappy tag names. const INTERESTS = { "Wellness & Self-Care": ["Wellness", "Beauty", "Fitness"], "Travel & Adventure": ["Travel", "Outdoors"], "Food & Dining": ["Food", "Drinkware", "Kitchen"], "Home & Living": ["Home & Kitchen", "Decor"], }; async function buildRail(collectionId, interest, maxBudgetUsd) { const url = new URL(`${SNAPPY}/v3/collections/${collectionId}/products`); url.searchParams.set("include", "brand,tags"); url.searchParams.set("filter[price][lte]", maxBudgetUsd.toFixed(2)); url.searchParams.set("page[size]", "12"); const res = await fetch(url, { headers }); const { data } = await res.json(); // Keep products whose tags intersect the interest's tag names. const wanted = new Set(INTERESTS[interest]); return data.filter((p) => (p.tags ?? []).some((t) => wanted.has(t.name))); } ``` ```python Python theme={null} INTERESTS = { "Wellness & Self-Care": {"Wellness", "Beauty", "Fitness"}, "Travel & Adventure": {"Travel", "Outdoors"}, "Food & Dining": {"Food", "Drinkware", "Kitchen"}, "Home & Living": {"Home & Kitchen", "Decor"}, } def build_rail(collection_id, interest, max_budget_usd): params = { "include": "brand,tags", "filter[price][lte]": f"{max_budget_usd:.2f}", "page[size]": 12, } res = requests.get( f"{SNAPPY}/v3/collections/{collection_id}/products", headers=headers, params=params, ) data = res.json()["data"] wanted = INTERESTS[interest] # Keep products whose tags intersect the interest's tag names. return [p for p in data if any(t["name"] in wanted for t in p.get("tags", []))] ``` For the **featured rail** at the very top, this is your moment to set the tone: lead with a beautiful tangible product or a standout experience. You can highlight key products directly by fetching them by ID, or do a broad fetch from Snappy. Our products are sorted by relevance by default, meaning that the most trending products will be fetched first. *** ## Step 4 - The personalization flow A "For You" page is only as good as what it knows. The fastest way to learn is to ask - once, gently, up front. Show a short interest picker the first time someone visits: a grid of friendly, image-led cards ("Wellness," "Travel," "Food & Dining," ...). Ask them to pick **at least two**, then assemble their rails from those choices. Render your interest buckets as tappable cards. Lead with imagery - people pick with their eyes. Require a minimum of two so you have enough signal to personalize, but keep it optional to *finish*: never trap someone behind this screen. Save the chosen interest labels against the user (your database, or a cookie for a logged-out demo). That's all the state you need. On the next render, build one rail per selected interest using `buildRail` from Step 3, plus a featured hero and a couple of evergreen rails ("Popular this month," "Worth saving for"). **Personalization UX rules we live by:** * **Default smartly.** Even before someone picks anything, show a strong generic page - never a blank one. * **Always let them change it.** Put an "Update picks" affordance somewhere visible. Tastes change; so should the page. * **Never gate the catalog.** Personalization is a shortcut, not a tollbooth. The full store is always one tap away. Keep this flow simple and structured - a fixed set of interests, not an open-ended "tell us in your own words" text box. Structured input is faster for users, easier to map to tags, and produces more predictable rails. *** ## Step 5 - The catalog page Some people know exactly what they want. The catalog page is for them: the full store, with the controls to slice it down fast. Four controls cover the vast majority of needs, and the v3 endpoint backs all of them: | Control | How | | :----------- | :--------------------------------------------------------------------------------------------- | | **Category** | Group by `category.fullName` client-side, or narrow with free-text `filter[search]`. | | **Search** | `filter[search]` - free-text across product title, category, and brand. | | **Sort** | `sort=minPrice` (ascending) or `sort=-minPrice` (descending); also `createdAt` / `-createdAt`. | ```javascript JavaScript theme={null} async function searchCatalog(collectionId, { query, sort, cursor } = {}) { const url = new URL(`${SNAPPY}/v3/collections/${collectionId}/products`); url.searchParams.set("include", "brand,tags"); url.searchParams.set("page[size]", "48"); if (query) url.searchParams.set("filter[search]", query); if (sort) url.searchParams.set("sort", sort); // e.g. "minPrice" if (cursor) url.searchParams.set("page[cursor]", cursor); const res = await fetch(url, { headers }); const body = await res.json(); return { items: body.data, nextCursor: body.links?.next ? new URL(body.links.next).searchParams.get("page[cursor]") : null, }; } ``` ```python Python theme={null} from urllib.parse import urlparse, parse_qs def search_catalog(collection_id, query=None, sort=None, cursor=None): params = {"include": "brand,tags", "page[size]": 48} if query: params["filter[search]"] = query if sort: params["sort"] = sort # e.g. "minPrice" if cursor: params["page[cursor]"] = cursor res = requests.get( f"{SNAPPY}/v3/collections/{collection_id}/products", headers=headers, params=params, ) body = res.json() next_link = (body.get("links") or {}).get("next") next_cursor = parse_qs(urlparse(next_link).query)["page[cursor]"][0] if next_link else None return {"items": body["data"], "next_cursor": next_cursor} ``` Your **default sort and category order** are a merchandising choice, not just a technical one. Leading with physical products and experiences sets the tone and surfaces the rewards people remember most. The full range stays one filter away, so anyone with something specific in mind can get there in a tap - you're simply putting the most delightful stuff front and center. *** ## Step 6 - Product detail pages When someone taps a product, give them a page worth the tap: a real gallery, a clear description, and confident variant selection. Pull the full detail for a single product. The list response you've been using is a lightweight summary; for the detail page, request the product directly and ask for its aggregated **options** matrix. ```text theme={null} GET /v3/products/{productId} ``` This endpoint returns **product-level data only** - `media` is always present, and `fields=options` adds the aggregated option matrix. The full list of orderable variants comes from a separate endpoint (below). ```javascript JavaScript theme={null} async function fetchProduct(productId) { const url = new URL(`${SNAPPY}/v3/products/${productId}`); url.searchParams.set("include", "brand,tags"); url.searchParams.set("fields", "options,priceRange,variantsCount"); const res = await fetch(url, { headers }); return res.json(); // the product object is returned directly (no `data` wrapper) } ``` ```python Python theme={null} def fetch_product(product_id): params = {"include": "brand,tags", "fields": "options,priceRange,variantsCount"} res = requests.get(f"{SNAPPY}/v3/products/{product_id}", headers=headers, params=params) return res.json() # the product object is returned directly (no `data` wrapper) ``` ### Showing variants (up to three levels) Many products come in variations - a hoodie in three colors and five sizes, a laptop in two configurations etc. Snappy models this across **up to three option levels** (for example: Color, Size, and a third axis like Sports Team or Material). Rendering it cleanly takes two pieces: 1. The product's aggregated **`options`** (from `GET /v3/products/{productId}` with `fields=options`) - what you render pickers from. Each option value carries a `firstSelectableVariant` you can default to. 2. The product's **variants** (a separate, paginated endpoint) - each concrete, orderable variant with its `selectedOptions`, price, and media. **The order is placed against a variant, not a product.** The aggregated `options` come back on the product like this: ```json theme={null} { "id": "655277e68e0719000d6c3fd5", "title": "Cloud Fleece Hoodie", "type": "physical", "media": [{ "type": "image", "src": "https://media.snappy.com/..." }], "options": [ { "name": "color", "displayName": "Color", "displayType": "swatch", "values": [ { "displayName": "Heather Grey", "value": "heather_grey", "firstSelectableVariant": { "id": "FB6bgFV4lf", "title": "Cloud Fleece Hoodie", "selectedOptions": { "color": "heather_grey", "size": "m" } } } ] }, { "name": "size", "displayName": "Size", "displayType": "button", "values": [ { "displayName": "Medium", "value": "m", "firstSelectableVariant": { "id": "FB6bgFV4lf", "title": "Cloud Fleece Hoodie", "selectedOptions": { "color": "heather_grey", "size": "m" } } } ] } ] } ``` Fetch the orderable variants from their own endpoint (page-number pagination). Pass `fields=price` (or `price,priceBreakdown`) for pricing and `include=brand` for the variant brand: ```text theme={null} GET /v3/products/{productId}/variants ``` Each variant looks like this: ```json theme={null} { "data": [ { "id": "FB6bgFV4lf", "productId": "655277e68e0719000d6c3fd5", "title": "Cloud Fleece Hoodie", "selectedOptions": { "color": "heather_grey", "size": "m" }, "taxable": true, "media": [{ "type": "image", "src": "https://media.snappy.com/..." }], "personalization": { "isPersonalized": false, "personalizationTemplateFields": [] }, "price": { "amount": 58.0, "currency": "USD" } } ], "links": { "first": "/v3/products/655277e68e0719000d6c3fd5/variants?page[size]=100&page[number]=1", "next": null, "prev": null } } ``` ```javascript JavaScript theme={null} // `variants` is the `data` array from GET /v3/products/{productId}/variants. // `selection` maps option name -> chosen value, e.g. { color: "black", size: "l" }. function resolveVariant(variants, selection) { return variants.find((v) => Object.entries(selection).every(([level, value]) => v.selectedOptions[level] === value) ); } const variant = resolveVariant(variants, { color: "black", size: "l" }); // variant.id is what you'll send to checkout. // Default a picker to an option value's `firstSelectableVariant` to avoid dead-end combinations. ``` ```python Python theme={null} def resolve_variant(variants, selection): for v in variants: if all(v["selectedOptions"].get(level) == value for level, value in selection.items()): return v return None variant = resolve_variant(variants, {"color": "black", "size": "l"}) # variant["id"] is what you'll send to checkout. # Default a picker to an option value's `firstSelectableVariant` to avoid dead-end combinations. ``` The variant object carries no inline stock flag. To confirm a specific variant can ship to a country, call `GET /v3/variants/{variantId}/availability`. **Variant UX that respects people:** * Pre-select each option's `firstSelectableVariant` so a single-variant product needs zero taps and pickers never start on a dead-end combination. * Reflect the selected variant's image and price immediately. Surprise at checkout erodes trust. * Confirm shippability for the recipient's country with the availability endpoint, and show the state plainly - a graceful "Not available in your region" beats a failed checkout. *** ## Step 7 - Search and related products Two features make a store feel alive: a search box that finds things, and "you might also like" rows that keep people exploring. Both are first-class in v3. **Product search** - there's no separate search endpoint. Use **`filter[search]`** on the catalog endpoint (Step 5) - free-text ranked across product title, category, and brand: ```text theme={null} GET /v3/collections/{collectionId}/products?filter[search]=cold+brew&location=US ``` **Related products** - for "Complete the set" or "More like this" rows on a product page, use the recommendations endpoint keyed off a product ID. It returns products ordered by relevance: ```text theme={null} GET /v3/products/{productId}/recommendations?location=US&page[limit]=10 ``` ```javascript JavaScript theme={null} async function fetchRecommendations(productId, { location = "US", limit = 10, collectionId } = {}) { const url = new URL(`${SNAPPY}/v3/products/${productId}/recommendations`); url.searchParams.set("location", location); // required url.searchParams.set("page[limit]", String(limit)); // 0 to 20, default 10 url.searchParams.set("include", "brand,tags"); if (collectionId) url.searchParams.set("collectionId", collectionId); // scope to your collection const res = await fetch(url, { headers }); const { data } = await res.json(); return data; // [Product, ...] - empty when none qualify } ``` ```python Python theme={null} def fetch_recommendations(product_id, location="US", limit=10, collection_id=None): params = {"location": location, "page[limit]": limit, "include": "brand,tags"} if collection_id: params["collectionId"] = collection_id res = requests.get( f"{SNAPPY}/v3/products/{product_id}/recommendations", headers=headers, params=params, ) return res.json()["data"] # [Product, ...] - empty when none qualify ``` Pass your `collectionId` to keep recommendations inside your merchandised catalog. If you filter by price, send **both** `filter[price][gte]` and `filter[price][lte]` together - the endpoint requires them as a pair. *** ## Step 8 - Checkout Checkout is where good intentions meet reality: you need a real recipient and a real address. The kindest thing you can do here is make the address easy to get right. ### Validate the address as they type Snappy's autocomplete endpoint turns a half-typed string into clean, structured, shippable addresses - fewer failed deliveries, fewer support tickets. ```text theme={null} GET /v3/orders/addresses/autocomplete ``` ```javascript JavaScript theme={null} // Debounce on the client; call this from your backend. async function autocompleteAddress(partial, country = "US") { const url = new URL(`${SNAPPY}/v3/orders/addresses/autocomplete`); url.searchParams.set("filter[address]", partial); // 4 to 128 chars, required url.searchParams.set("filter[country]", country); // 2 to 3 chars, required const res = await fetch(url, { headers }); const { data } = await res.json(); return data; // [{ addressLine1, addressLine2?, city, state, zipcode }, ...] } ``` ```python Python theme={null} def autocomplete_address(partial, country="US"): params = {"filter[address]": partial, "filter[country]": country} # both required res = requests.get( f"{SNAPPY}/v3/orders/addresses/autocomplete", headers=headers, params=params, ) return res.json()["data"] ``` ```bash cURL theme={null} curl -G "$SNAPPY/v3/orders/addresses/autocomplete" \ -H "X-Api-Key: $SNAPPY_API_KEY" \ --data-urlencode "filter[address]=123 Main" \ --data-urlencode "filter[country]=US" ``` A response looks like: ```json theme={null} { "data": [ { "addressLine1": "123 Main St", "city": "San Francisco", "state": "CA", "zipcode": "94105" } ] } ``` Debounce the input (\~300ms) and only call once the user has typed a few characters. When they pick a suggestion, snap the structured fields into your form - then let them eyeball it. Autocomplete plus a quick human glance is the sweet spot for delivery accuracy. The fields you ultimately need for an order: recipient **first name, last name, email, and phone** (E.164 format - required by v3 fulfillment), and a **shipping address**. Note the autocomplete/validate address shape (`addressLine1`, `addressLine2`, `city`, `state`, `zipcode`) maps onto the place-order shape (`address1`, `address2`, `city`, `provinceCode`, `postalCode`, plus `countryCode`). For verified-deliverable addresses, run a pre-flight `POST /v3/orders/addresses/validate` before placing the order. Rewards catalogs typically emphasize physical products and experiences, but Snappy also includes digital rewards like gift cards. For **digital variants** (`variant.shippingRequired: false`), you only need to collect `countryCode` at checkout - the full street/city/postal fields aren't needed since there's no physical delivery. Check `shippingRequired` on the variant before deciding which form fields to show. *** ## Step 9 - Place the order This is the payoff, and it's refreshingly simple. **One call** creates the order: ```text theme={null} POST /v3/orders ``` If you've integrated with Snappy before, you may remember a two-step dance: create a gift, then claim it. This endpoint collapses that into a single atomic call. You hand over the variant and the recipient; you get back an order. Less code, fewer round-trips, no half-finished states. ```javascript JavaScript theme={null} async function placeOrder({ variantId, recipient, idempotencyKey }) { const res = await fetch(`${SNAPPY}/v3/orders`, { method: "POST", headers: { ...headers, "Snappy-Account-Id": process.env.SNAPPY_ACCOUNT_ID, // account is scoped via header }, body: JSON.stringify({ billingMethodId: process.env.SNAPPY_BILLING_METHOD_ID, variantId, recipient: { firstName: recipient.firstName, lastName: recipient.lastName, email: recipient.email, phone: recipient.phone, // E.164, required }, shippingAddress: { address1: recipient.address1, address2: recipient.address2, // optional city: recipient.city, provinceCode: recipient.provinceCode, // e.g. "CA" postalCode: recipient.postalCode, countryCode: recipient.countryCode, // ISO 3166-1 alpha-2 }, idempotencyKey, // top-level, stable per redemption - see below tags: ["rewards-store"], // optional: for your reporting }), }); if (!res.ok) return handleOrderError(res); // see error handling below const { data } = await res.json(); return data; // { id, status: "active", trackingLink } } ``` ```python Python theme={null} import os, requests def place_order(variant_id, recipient, idempotency_key): payload = { "billingMethodId": os.environ["SNAPPY_BILLING_METHOD_ID"], "variantId": variant_id, "recipient": { "firstName": recipient["first_name"], "lastName": recipient["last_name"], "email": recipient["email"], "phone": recipient["phone"], # E.164, required }, "shippingAddress": { "address1": recipient["address1"], "address2": recipient.get("address2"), # optional "city": recipient["city"], "provinceCode": recipient["province_code"], # e.g. "CA" "postalCode": recipient["postal_code"], "countryCode": recipient["country_code"], # ISO 3166-1 alpha-2 }, "idempotencyKey": idempotency_key, # top-level, stable per redemption - see below "tags": ["rewards-store"], # optional: for your reporting } order_headers = {**headers, "Snappy-Account-Id": os.environ["SNAPPY_ACCOUNT_ID"]} res = requests.post(f"{SNAPPY}/v3/orders", headers=order_headers, json=payload) if not res.ok: return handle_order_error(res) # see error handling below return res.json()["data"] # { id, status, trackingLink } ``` ```bash cURL theme={null} curl -X POST "$SNAPPY/v3/orders" \ -H "X-Api-Key: $SNAPPY_API_KEY" \ -H "Snappy-Account-Id: acct_123" \ -H "Content-Type: application/json" \ -d '{ "billingMethodId": "87654321", "variantId": "FB6bgFV4lf", "recipient": { "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "phone": "+12133734253" }, "shippingAddress": { "address1": "123 Main St", "city": "San Francisco", "provinceCode": "CA", "postalCode": "94105", "countryCode": "US" }, "idempotencyKey": "user-7251-order-2026-06", "tags": ["rewards-store"] }' ``` A successful response: ```json theme={null} { "data": { "id": "G7nR4bD9mK", "status": "active", "trackingLink": "https://gift.snappy.com/choose/G7nR4bD9mK?utm_source=l&utm_medium=i&utm_campaign=l2fX39ZvaM" } } ``` The response is intentionally minimal. To retrieve the full order later - line items, fulfillments, and tracking detail - call `GET /v3/orders/{orderId}`. **Digital variants (e.g. gift cards) need only `countryCode`.** For a variant where `shippingRequired` is `false`, you can send `shippingAddress: { countryCode: "US" }` instead of the full address block shown above. Snappy uses the country only for pricing and localization. For physical variants, the full address remains required. ### The idempotency key matters The top-level **`idempotencyKey`** (1 to 120 characters) makes the call safe to retry. Send the same key twice and you get the *same order* back - no duplicate, no double-charge. This is your safety net against the classic failure modes: a flaky network, a double-clicked "Redeem" button, a retried request. Generate one stable key per redemption and reuse it on retries - something like `user-{userId}-order-{cartId}`. Don't generate a fresh random key on each attempt, or you'll defeat the very protection it provides. ### Handle the errors people will actually hit A few failures are worth handling gracefully, because they map to real human moments: | HTTP | Meaning | What to show the user | | :---- | :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | | `422` | Insufficient funds in the billing method | A calm "Something went wrong on our end" - this is your billing, not their fault. Alert your team. | | `422` | Variant not available in the recipient's country | "Not available in your region - here's something similar." Offer the recommendations rail. | | `404` | Variant, billing method, or account not found | A config/data bug on your side - log it and fail safe; never surface raw errors. | | `400` | Invalid request body (bad phone, malformed address, etc.) | Catch at the form step with inline field validation, before they're emotionally committed. | Every failure uses the standard v3 error envelope - `{ message, errorCode, errors[] }`. Branch on `errorCode`, and surface `message` only when it's safe to show an end user. The best error is the one nobody sees. Validate the address (Step 8) and re-confirm availability and affordability *before* the order call, so checkout itself almost always succeeds. Reserve the error UI for genuine surprises. *** ## Step 10 - Track fulfillment with webhooks The order is placed - now keep the recipient (and your support team) in the loop without polling. Subscribe to Snappy's order webhooks and let updates come to you. **Preview - confirm against the final spec.** The `order-*` webhook event names below are the pattern we expect V3 to ship with; confirm exact event names against the current [Webhook Event Types](/webhook-event-types) reference before you build against them. Orders move through a predictable fulfillment lifecycle, and each transition fires an event. The fulfillment status values on V3 Orders are: | Status | Meaning | | :----------------- | :----------------------------------------- | | `confirmed` | Order received by the vendor. | | `processing` | Being prepared for shipment. | | `in_transit` | Shipped and in transit - tracking is live. | | `out_for_delivery` | Out for delivery today. | | `delivered` | Arrived at the recipient's address. | Every V3 webhook arrives in the same envelope - a `webhookData` block describing the event and an `eventData` block with the payload. A delivery-status webhook looks roughly like: ```json theme={null} { "webhookData": { "id": "wh_9KdP2mLx", "eventType": "order-delivery-status-changed", "target": "https://your-domain.com/webhooks", "triggeredAt": "2026-06-02T15:04:00Z" }, "eventData": { "orderId": "G7nR4bD9mK", "status": "in_transit", "trackingLink": "https://gift.snappy.com/choose/G7nR4bD9mK?utm_source=l&utm_medium=i&utm_campaign=l2fX39ZvaM" } } ``` Handle it on your backend: ```javascript JavaScript theme={null} // Express handler for Snappy order webhooks. app.post("/webhooks/snappy", (req, res) => { const { webhookData, eventData } = req.body; if (webhookData.eventType.startsWith("order-")) { updateOrderStatus(eventData.orderId, eventData.status); // your DB if (eventData.status === "in_transit") { notifyUser(eventData.orderId, eventData.trackingLink); // your email/push } } res.sendStatus(200); // acknowledge fast }); ``` ```python Python theme={null} # Flask handler for Snappy order webhooks. @app.post("/webhooks/snappy") def snappy_webhook(): body = request.get_json() webhook_data, event_data = body["webhookData"], body["eventData"] if webhook_data["eventType"].startswith("order-"): update_order_status(event_data["orderId"], event_data["status"]) # your DB if event_data["status"] == "in_transit": notify_user(event_data["orderId"], event_data["trackingLink"]) # your email/push return "", 200 # acknowledge fast ``` Surface the `trackingLink` to your recipient as soon as the order ships - a "Track your reward" button closes the loop and turns a transaction into an experience. Anticipation is part of the gift. *** ## Putting it together We hope this guide has been a useful introduction to how to build a rewards experience on Snappy. As always, our team is available to help you craft the best experience for your customers. Reach out to your Snappy representative for any questions, needs, or feedback. Full endpoint specs for everything used in this guide. # Triggered Gifting (Recipient Choice) Source: https://docs.snappy.com/guides/recipes/triggered-gifting Send a magic-link gift, let the recipient choose, and Snappy handles fulfillment. Use cases, flow, and gift creation reference. *"Your system triggers the gift, Snappy handles the experience, the recipient chooses what they want."* Use this recipe when you want to notify a recipient and let them select their own gift from a Collection, or when you want to send a specific gift but don't have the recipient's shipping address upfront. Common use cases include: * Employee Recognition & Retention * Sales Lead Nurturing * Client Onboarding & Appreciation In this model, your system is responsible for creating the Gift. Snappy then takes over: notifying the recipient, presenting the gift experience, collecting their address, and fulfilling the order. Your integration only needs to handle two things - creating the gift, and listening for status updates via webhooks. *** ## What you're building Four surfaces make up the end-to-end flow, three of them on Snappy's side: Your system creates a gift via `POST /gifts` with recipient identity and campaign context. Snappy emails the recipient a magic link to a personalized claim page (auto or manual). Recipient chooses a gift, provides shipping details, and Snappy generates the order. Your backend consumes webhook events to follow the gift through claim and delivery. *** ## Before you start Every gift is created within a Campaign - a reusable template that carries the Collection or Product, budget, branding, and gift customization settings. Create one via the Snappy Dashboard or via the API (`POST /campaigns`). Campaigns created via the API are automatically assigned the Account's default Billing Method. The Account you're sending under must have an active Billing Method. Discover the available methods via `GET /v3/billing-methods` (V3 endpoint - the funding source model is the same across V2 and V3). All requests authenticate with an `X-Api-Key` header. For this guide you'll want `campaigns:read`, `gifts:create`, and one of `gifts:read:masked` or `gifts:read:unmasked` depending on whether you display recipient details in your UI. See [Authentication & Security](/authentication-and-security). Store your API key as an environment variable, keep it server-side, and never hardcode it or ship it in your frontend bundle. Rotate the key if it's ever exposed. ### Setting up your API client Before making any API calls, initialize your HTTP client with your API key. This setup is used throughout the steps below. ```javascript JavaScript theme={null} const axios = require('axios'); const snappyClient = axios.create({ baseURL: 'https://api.snappy.com/public-api/v2', headers: { 'X-Api-Key': process.env.SNAPPY_API_KEY, 'Content-Type': 'application/json' } }); ``` ```python Python theme={null} import requests import os session = requests.Session() session.headers.update({ 'X-Api-Key': os.environ.get('SNAPPY_API_KEY'), 'Content-Type': 'application/json' }) BASE_URL = 'https://api.snappy.com/public-api/v2' ``` *** ## Step 1 - Identify your Campaign Every gift is created within a Campaign. Before making any API calls, you need the `id` of the Campaign you want to send under. Use `GET /campaigns` to retrieve your Campaign list and confirm the correct id. The endpoint paginates via `skip` / `limit` and returns each Campaign's id, name, status, and Collection/Product configuration. ```javascript JavaScript theme={null} async function getCampaigns({ limit = 100, skip = 0 } = {}) { const response = await snappyClient.get('/campaigns', { params: { limit, skip }, }); const campaigns = response.data.results; console.log('Available campaigns:', campaigns.map(c => ({ id: c.id, name: c.name }))); return campaigns; } // Find the campaign you want by name or by ID: const campaigns = await getCampaigns(); const campaign = campaigns.find(c => c.name === 'Q4 Employee Recognition'); if (!campaign) throw new Error('Campaign not found'); ``` ```python Python theme={null} def get_campaigns(limit=100, skip=0): response = session.get(f'{BASE_URL}/campaigns', params={'limit': limit, 'skip': skip}) response.raise_for_status() campaigns = response.json()['results'] print('Available campaigns:', [(c['id'], c['name']) for c in campaigns]) return campaigns # Find the campaign you want by name or by ID: campaigns = get_campaigns() campaign = next((c for c in campaigns if c['name'] == 'Q4 Employee Recognition'), None) if not campaign: raise ValueError('Campaign not found') ``` In production, cache the campaign lookup - Campaigns change infrequently, and you don't want a `GET /campaigns` call on every gift send. A short TTL (a few minutes) is plenty. Your Campaign should already have a Collection or Product assigned, along with your preferred Gift Customization settings. For the full list of Campaign configuration options, see the [Campaigns V2 Overview](/campaigns-v2-overview). *** ## Step 2 - Create the Gift Call `POST /gifts` with your Campaign ID and recipient details. This is the core action that initiates the entire gifting flow. ```javascript JavaScript theme={null} async function createGift(campaignId, recipient) { const response = await snappyClient.post('/gifts', { campaignId, recipients: [ { firstname: recipient.firstname, // V2 uses lowercase merged casing lastname: recipient.lastname, email: recipient.email, key: recipient.key, // permanent idempotency key metadata: recipient.metadata, // optional passthrough - round-trips in webhooks }, ], }); const gift = response.data.results[0]; console.log('Gift created:', gift.id); console.log('Claim link:', gift.link); return gift; } // Example usage await createGift('cmp_12345', { firstname: 'Jane', lastname: 'Doe', email: 'jane@example.com', key: 'jane-doe-anniversary-2026', metadata: { internalReferenceId: 'REF-ABC-123', occasion: 'work-anniversary' }, }); ``` ```python Python theme={null} def create_gift(campaign_id, recipient): response = session.post(f'{BASE_URL}/gifts', json={ 'campaignId': campaign_id, 'recipients': [ { 'firstname': recipient['firstname'], # V2 uses lowercase merged casing 'lastname': recipient['lastname'], 'email': recipient['email'], 'key': recipient['key'], # permanent idempotency key 'metadata': recipient.get('metadata'), # optional passthrough } ] }) response.raise_for_status() gift = response.json()['results'][0] print(f"Gift created: {gift['id']}") print(f"Claim link: {gift['link']}") return gift # Example usage create_gift('cmp_12345', { 'firstname': 'Jane', 'lastname': 'Doe', 'email': 'jane@example.com', 'key': 'jane-doe-anniversary-2026', 'metadata': {'internalReferenceId': 'REF-ABC-123', 'occasion': 'work-anniversary'}, }) ``` ```json Request body theme={null} { "campaignId": "cmp_12345", "recipients": [ { "firstname": "Jane", "lastname": "Doe", "email": "jane@example.com", "key": "jane-doe-anniversary-2026", "metadata": { "internalReferenceId": "REF-ABC-123", "occasion": "work-anniversary" } } ] } ``` Snappy creates a Gift object and returns it in the response, including a unique `link` - the recipient's personal claim URL. If your Campaign's Notification Policy is configured to notify automatically, Snappy sends the recipient an email immediately. If not, you can use the link from the response to trigger your own notification. Include a `key` for every recipient to prevent duplicate gifts. Send the same key twice and you get the same gift back, no duplicate. Use a stable identifier tied to the send occasion, like `user-{userId}-anniversary-{year}`. See [Duplicate Gift Detection](/duplicate-gifts-detection) for details. V2 uses `firstname` / `lastname` (lowercase, merged) in this endpoint. V3 uses `firstName` / `lastName` (camelCase). Match the casing per version. *** ## Step 3 - Recipient claims the Gift This step happens entirely within Snappy's recipient experience - no API calls required from your side. The recipient opens their claim link, browses the Collection (or sees their assigned Product), selects a variant, and enters their shipping address. Snappy automatically generates an Order once the selection is complete. From your integration's perspective, the next signal you receive is a webhook (Step 4). *** ## Step 4 - Track status via webhooks Rather than polling the API, listen for webhook events to track the gift as it moves through its lifecycle. Key events to handle: | Event | Meaning | | :------------------------------------------------------- | :-------------------------------------------------------- | | `gift-notification-initial-sent` | The gift notification has been sent to the recipient. | | `gift-status-changed` (status: `opened`) | Recipient viewed the available gift options. | | `gift-status-changed` (status: `claimed`) | Recipient selected their gift and provided their details. | | `gift-delivery-status-changed` (status: `orderReceived`) | Order has been placed with the fulfillment partner. | | `gift-delivery-status-changed` (status: `inTransit`) | Product has shipped. | | `gift-delivery-status-changed` (status: `delivered`) | Product has reached its final destination. | Every webhook arrives in the same envelope - a `webhookData` block describing the event and an `eventData` block with the payload. Any `metadata` you attached at creation round-trips through the `eventData`, so you can always map an event back to your internal IDs. ```javascript JavaScript theme={null} const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhooks/snappy', (req, res) => { const { webhookData, eventData } = req.body; switch (webhookData.eventType) { case 'gift-notification-initial-sent': console.log(`Notification sent for gift: ${eventData.giftId}`); break; case 'gift-status-changed': console.log(`Gift ${eventData.giftId} status: ${eventData.status}`); if (eventData.status === 'claimed') { console.log('Gift claimed - order will be generated shortly'); } if (eventData.status === 'expired') { console.log('Gift expired - consider follow-up action'); } break; case 'gift-delivery-status-changed': console.log(`Delivery update for gift ${eventData.giftId}: ${eventData.deliveryStatus}`); if (eventData.deliveryStatus === 'delivered') { console.log('Gift delivered successfully'); } break; } res.status(200).send('OK'); // acknowledge fast }); app.listen(3000, () => console.log('Webhook listener running on port 3000')); ``` ```python Python theme={null} from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/webhooks/snappy', methods=['POST']) def handle_webhook(): payload = request.json webhook_data = payload['webhookData'] event_data = payload['eventData'] event_type = webhook_data['eventType'] if event_type == 'gift-notification-initial-sent': print(f"Notification sent for gift: {event_data['giftId']}") elif event_type == 'gift-status-changed': status = event_data['status'] print(f"Gift {event_data['giftId']} status: {status}") if status == 'claimed': print('Gift claimed - order will be generated shortly') elif status == 'expired': print('Gift expired - consider follow-up action') elif event_type == 'gift-delivery-status-changed': print(f"Delivery update for gift {event_data['giftId']}: {event_data['deliveryStatus']}") if event_data['deliveryStatus'] == 'delivered': print('Gift delivered successfully') return jsonify({'status': 'ok'}), 200 if __name__ == '__main__': app.run(port=3000) ``` For the full list of webhook events, payload shapes, and setup instructions, see [Webhooks: Setup](/overview-and-setup) and [Webhook Event Types](/webhook-event-types). *** ## Handle the errors people will actually hit Most of your gift sends will succeed silently. The few that fail tend to fail for the same handful of reasons: | HTTP | Meaning | What to do | | :---- | :------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------- | | `400` | Invalid request body - malformed email, missing required field | Catch at your form validation before you call Snappy. Log the raw response and inline the field-level message. | | `401` | Invalid or missing `X-Api-Key` | Check your environment variables. Rotate the key if it was recently compromised. | | `403` | Key missing the `gifts:create` scope | Reissue the key from the Snappy Dashboard with the right scope. | | `404` | Campaign or Account not found | Verify the `campaignId` and that the key is scoped to the right Account. | | `422` | Insufficient budget on the Campaign's Billing Method | Alert your team - the send is blocked at billing, not on the recipient's side. Don't retry until resolved. | Every failure uses the V2 error envelope - `{ status, errorCode, message }` on non-400 responses, or `{ path, errorCode, message }` on validation errors. Branch on `errorCode`, never on `message`. For bulk sends, plan for **partial failure** from the start. When you send to 500 recipients and 3 fail, you want to log the 3 failures with enough context to retry them individually - not throw away the whole batch. The permanent idempotency `key` makes safe retries trivial: re-send with the same key and you get the same gift back, no duplicate. *** ## Putting it together The end-to-end shape: 1. `GET /campaigns` - retrieve your campaign ID. 2. `POST /gifts` - create the gift, get back the claim link. 3. Snappy notifies the recipient, they select their gift, Snappy generates the Order automatically. 4. Webhooks - track gift and order status through the fulfillment lifecycle. That's Triggered Gifting end to end. From here, common next steps are: * Adding a **bulk send** flow (multiple recipients per `POST /gifts` call, with idempotent keys per recipient). * Wiring **HRIS or CRM triggers** so gifts fire automatically on milestones instead of manually. * Layering **status reporting** on top of the webhook stream so your ops team can see program health at a glance. Reach out to your Snappy representative for any questions, needs, or feedback. Full endpoint specs for the V2 Gifts and Campaigns endpoints used in this guide. # Create account Source: https://docs.snappy.com/modules/api/v2/accounts/create-account post /v2/accounts Use this endpoint to create a new Account under your Company. Use this when you need to programmatically set up a new team, department, or budget owner with its own campaigns and billing method. ###### Required fields - `name` - display name of the Account - `billingMethod` - the initial billing method to attach to the Account - `billingMethod.type` - currently must be `INV` (invoice). Other billing types (Prepay, PO, CC) must be set up via the Snappy Dashboard. - `billingMethod.amount` - billing amount - `billingMethod.name` - display name of the billing method **Optional parameters** - `companyId` query parameter - Company ID (when not inferable from the API key context) - `Request-Source` header - source of the request ###### Behavior Notes - Returns `201` with the new Account details (`id`, `name`, `createdAt`, `updatedAt`, `companyId`) on success. - Returns `409` (`409_PBLC_001`) when an Account with the same name already exists in the Company. - Returns `422` (`422_PBLC_001`) for business-rule violations (e.g. invalid billing configuration). - Currently only invoice (`INV`) billing methods can be created via the API. To set up Prepay, PO, or Credit Card billing, create the Account first and then configure billing via the Snappy Dashboard. #### Permissions - Requires: `accounts:create` # Get account by ID Source: https://docs.snappy.com/modules/api/v2/accounts/get-account-by-id get /v2/accounts/{accountId} Use this endpoint to retrieve the details of a specific Account by its identifier. Use this when you have an Account ID and need to confirm its name, Company membership, or timestamps. ###### Required fields - `accountId` - the Account identifier, passed as a path parameter (alphanumeric) ###### Optional parameters - `companyId` query parameter - Company ID (when not inferable from the API key context) - `Request-Source` header - source of the request ###### Please note - Returns `404` (`404_PBLC_001`) if no Account exists for the supplied `accountId`, or if it's not accessible to the calling API key. #### Permissions - Requires: `accounts:read` # Get accounts Source: https://docs.snappy.com/modules/api/v2/accounts/get-accounts get /v2/accounts Use this endpoint to retrieve a list of Accounts available to your API key. Use this when you need to discover which Accounts you have access to, find a specific Account by name, or load Account options into your UI. ###### Filtering options - `companyId` query parameter - filter by Company ID (8+ alphanumeric characters) - `name` query parameter - return Accounts whose name matches - `fields` query parameter - comma-separated list of fields to return. Valid values: `id`, `name`, `createdAt`, `updatedAt`, `companyId`, `full` - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`) ###### Pagination - `skip` query parameter - number of records to skip (default `0`) - `limit` query parameter - max records per page (1-1000, default `100`) ###### Please note - The response wraps Accounts in a `results` array, with `skip` and `limit` echoed back in the envelope. #### Permissions - Requires: `accounts:read` # Accounts API (V2): Organize Gifting by Team, Department, or Budget Source: https://docs.snappy.com/modules/api/v2/accounts/overview Manage Accounts and sub-accounts via the V2 API. Skip/limit pagination and the V2 error envelope. 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. Want to understand how **Accounts** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page. Looking for the V3 endpoints? See [Accounts (V3)](/modules/api/v3/accounts/overview). Both V2 and V3 are supported in parallel. *** ## The Account Object | Field | Type | Description | | :---------- | :---------------- | :-------------------------------------------------- | | `id` | string | Unique identifier for the Account (e.g. `a12bcd34`) | | `name` | string | Display name of the Account | | `companyId` | string | The ID of the Company this Account belongs to | | `createdAt` | string (ISO 8601) | Timestamp when the Account was created | | `updatedAt` | string (ISO 8601) | Timestamp of the most recent update | Billing Methods belong to an Account but are not currently exposed through the V2 API as a separate resource. To list billing methods on an Account programmatically, use the V3 [Billing Methods API](/modules/api/v3/billing-methods/overview). *** ## Key Concepts & Business Rules #### Accounts organize your gifting activity All Campaigns and Gifts are created within an Account. If your organization has multiple teams or departments sending gifts independently, each should operate through its own Account with its own budget and Billing Method. #### One default Billing Method per Account Each Account has one Billing Method set as the default. This default is applied automatically to any Campaign created via the API. Campaigns created through the Dashboard allow explicit Billing Method selection at the time of creation. #### Initial billing setup via Create Account The `POST /v2/accounts` endpoint accepts an initial Billing Method (currently invoice-only - `type: INV`) at Account creation time. To configure other Billing Method types (Prepay, PO, Credit Card), create the Account first and then set up the Billing Methods via the Snappy Dashboard. *** ## How to Work with Accounts (V2) **List Accounts** ```text theme={null} theme={null} GET /v2/accounts ``` Returns a paginated list of Accounts available to your API key. Filter by `companyId` or `name`, control returned fields with `fields`, and paginate with `skip` and `limit`. **Get a single Account** ```text theme={null} theme={null} GET /v2/accounts/{accountId} ``` Returns the details of a specific Account by its ID. **Create an Account** ```text theme={null} theme={null} POST /v2/accounts ``` Creates a new Account under your Company with an initial Billing Method (currently invoice-only via the API). # Create API key Source: https://docs.snappy.com/modules/api/v2/api-keys/create-api-key post /v2/authentication/apiKeys Use this endpoint to programmatically create a new API key for your Company. Use this when rotating keys, provisioning a key for a new integration, or spinning up a scoped key for a specific Account from a backend service or automation pipeline. ###### Required fields - `name` - display name of the API key (must be unique within the Company) ###### Optional fields - `expirationInDays` - number of days until the key expires. Accepted values: `30`, `60`, `90`, `180`, `365`. Default: `90`. - `enforceMtls` - when `true`, requests with this key must use mTLS. Default: `false`. - `permissions` - array of permission scopes the new key should have. See the [permission reference](/pages/authentication-and-security#available-scopes). - `accountIds` - array of Account IDs the key should be scoped to. Omit for all-Accounts access. ###### Behavior Notes - **The API key secret is visible only in this response.** The `apiKey` field in the response body is your one and only chance to capture the secret value - store it securely. After this response, only metadata is retrievable. - **Permission inheritance.** Keys created via the API can only have the same permissions as the calling key, or a more restrictive subset. Attempts to grant permissions the calling key doesn't have are rejected. - **Max 100 active API keys per Company.** Delete an existing key before creating the 101st. #### Permissions Authenticated via `Authorization: Bearer `. Only Company owners and tools admins have access. # Delete API key Source: https://docs.snappy.com/modules/api/v2/api-keys/delete-api-key delete /v2/authentication/apiKeys/{apiKeyId} Use this endpoint to permanently delete an existing API key by its ID. Use this when rotating keys, removing a compromised key, or cleaning up unused keys. ###### Required fields - `apiKeyId` - the API key identifier, passed as a path parameter (24-character hex) ###### Optional parameters - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Behavior Notes - **Deletion is immediate and permanent.** Once deleted, any application using this key will receive `401 Unauthorized` on its next request. Update your applications to use a replacement key *before* deleting the old one. - Returns `204 No Content` on success - no response body. #### Permissions Authenticated via `Authorization: Bearer `. Only Company owners and tools admins have access. # Get API keys Source: https://docs.snappy.com/modules/api/v2/api-keys/get-api-keys get /v2/authentication/apiKeys Use this endpoint to retrieve a list of the active API keys for your Company. Use this when building a key-management view, auditing existing keys, or checking which keys belong to which Accounts. ###### Filtering options - `companyId` query parameter - Company ID (when not inferable from the calling key) - `accountIds` query parameter - array of Account IDs to scope the list to keys with access to those Accounts - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`) ###### Please note - The actual API key secret (`apiKey` field value) is **never returned by this endpoint**. The secret is shown only once - in the response to `POST /v2/authentication/apiKeys` - and is not retrievable afterward. This endpoint returns metadata only. #### Permissions Authenticated via `Authorization: Bearer `. Only Company owners and tools admins have access. # API Keys (V2): Programmatic Key Management Source: https://docs.snappy.com/modules/api/v2/api-keys/overview Programmatically manage the API keys your Company uses to authenticate with the Snappy API - list, create, and revoke keys using an existing key for authentication. The V2 API Keys endpoints let any service with a valid Snappy API key manage other keys programmatically - list active keys, create new ones, and revoke keys you no longer need. Use this when you need automated key rotation in a backend service or CI/CD pipeline. For the full authentication concept guide - including how scopes work, how to use mTLS, and best practices for key rotation - see [Snappy API Authentication: API Keys, Scopes & mTLS](/pages/authentication-and-security). The [V3 API Keys](/modules/api/v3/api-keys/overview) endpoints offer the same list, create, and delete operations using V3 JSON:API conventions. You can also create and manage keys in the Snappy dashboard on the **Sharing & Access** page. *** ## The API Key Object | Field | Type | Description | | :--------------- | :---------------- | :-------------------------------------------------------------------------------- | | `id` | string | Unique identifier of the API key | | `name` | string | Display name (unique within the Company) | | `apiKey` | string | The secret key value (24 hex characters). **Returned only on creation.** | | `companyId` | string | The ID of the Company the key belongs to | | `createdAt` | string (ISO 8601) | When the key was created | | `expirationDate` | string (ISO 8601) | When the key expires. `null` if the key has no expiration. | | `enforceMtls` | boolean | When `true`, requests with this key must use mTLS | | `permissions` | array | Permission scopes granted to this key (e.g. `gifts:create`, `orders:read:masked`) | | `accountsAccess` | object | Account scope: `{ scope: "all-accounts" \| "specific-accounts", ids: [] }` | *** ## Key Concepts #### The secret value is shown only once When you create an API key, the secret `apiKey` value is returned in the response body. **This is the only time the value is visible** - it's hashed and stored, and cannot be retrieved later. If you lose it, delete the key and create a new one. #### Maximum 100 active keys per Company Companies can have up to 100 active API keys at any time. Plan rotations accordingly - typically you'd create the new key first, update your integrations to use it, then delete the old key. #### Permission inheritance on creation Keys created via this endpoint can only have permissions equal to or more restrictive than the calling key. This prevents privilege escalation: a key with read-only access cannot mint a key with write access. #### mTLS for enhanced security For production environments, set `enforceMtls: true` when creating a key. mTLS-enforced keys must connect through the dedicated mTLS endpoint (`https://mtls-api.snappy.com/public-api`) and present a valid client certificate. See the [Authentication & Security guide](/pages/authentication-and-security) for setup details. *** ## How to Work with API Keys (V2) **List API keys** ```text theme={null} theme={null} GET /v2/authentication/apiKeys ``` Returns the active API keys for your Company. Returns metadata only - the secret `apiKey` value is never included. **Create an API key** ```text theme={null} theme={null} POST /v2/authentication/apiKeys ``` Creates a new API key with the specified permissions, Account scope, expiration, and mTLS setting. The secret value is returned in this response only. **Delete an API key** ```text theme={null} theme={null} DELETE /v2/authentication/apiKeys/{apiKeyId} ``` Permanently deletes the specified key. Returns `204 No Content` on success. # Create campaign Source: https://docs.snappy.com/modules/api/v2/campaigns/create-campaign post /v2/campaigns Use this endpoint to create a new Campaign under an Account. When created via the API, the Campaign is automatically assigned the Account's default Billing Method. ###### Required fields: - `name` - display name of the Campaign (must be unique within the Company) - `accountId` - the Account this Campaign belongs to - `customization` - Gift Customization configuration (`giftProperties`, `notificationPolicy`, optionally `recipientExperience`) ###### Optional parameters: - `companyId` query parameter - Company identifier, when not inferable from the API key context - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`) ###### Behavior Notes: - Returns `201` with the new Campaign on success. - Campaigns created via the API are always created with `type: oneOffs`. Other Campaign types (`anniversary`, `birthday`, `schedule`, `marketing`, `newHire`) can only be created through the Snappy Dashboard. - Returns `409` when a Campaign with the same name already exists in the Company. - Returns `422` for business-rule violations (e.g. invalid Collection/Product reference, conflicting customization). #### Permissions - Requires: `campaigns:create` # Get campaign by ID Source: https://docs.snappy.com/modules/api/v2/campaigns/get-campaign-by-id get /v2/campaigns/{campaignId} Use this endpoint to retrieve the full details of a specific Campaign by its identifier, including its Gift Customization settings and assigned Collection or Product. ###### Required fields: - `campaignId` - the Campaign identifier, passed as a path parameter ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Please note: - Returns `404` if no Campaign exists for the supplied `campaignId`, or if it's not accessible to the calling API key. - Returns `422` when the Campaign references a Collection that can no longer be found - the Campaign still exists but its catalog can't be resolved. #### Permissions - Requires: `campaigns:read` # Get campaigns Source: https://docs.snappy.com/modules/api/v2/campaigns/get-campaigns get /v2/campaigns Use this endpoint to retrieve a paginated list of Campaigns with flexible filtering options. Use this when building a Campaign picker in your UI, syncing Campaigns to your system, or auditing existing Campaign configurations. **Filtering options** - `companyId` query parameter - Company ID (when not inferable from the calling key) - `accountId` query parameter - filter to Campaigns belonging to a specific Account - `types` query parameter - comma-separated Campaign types (`anniversary`, `birthday`, `schedule`, `marketing`, `oneOffs`, `newHire`) - `sources` query parameter - comma-separated Campaign sources (`dashboard`, `dashboard_ai`, `api_native`, `api_zapier`, `api_make`, `api_salesforce`, `api_ftp`) - `statuses` query parameter - comma-separated Campaign statuses (e.g. `draft,active,paused`) - `fields` query parameter - comma-separated field projection. Valid values: `id`, `name`, `createdAt`, `updatedAt`, `companyId`, `accountId`, `account`, `properties`, `giftsExpirationInDays`, `giftExpirationDate`, `type`, `source`, `status`, `customization`, `full` - `Request-Source` header - source of the request **Pagination** - `skip` query parameter - number of records to skip (default `0`) - `limit` query parameter - max records per page (1-1000, default `100`) **Please note** - The response wraps Campaigns in a `results` array with `skip` and `limit` echoed back. #### Permissions - Requires: `campaigns:read` # Get estimated cost for campaign Source: https://docs.snappy.com/modules/api/v2/campaigns/get-estimated-cost-for-campaign get /v2/campaigns/{campaignId}/estimatedCost Use this endpoint to retrieve the projected cost of a Campaign based on its configured budget and a target number of gifts. Use this before launching a Campaign to verify available funds and confirm the expected spend. ###### Required fields: - `campaignId` - the Campaign identifier, passed as a path parameter ###### Optional parameters: - `numberOfGifts` query parameter - the number of gifts to estimate for (1-99,999, default `1`) - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Please note: - The response includes the configured `budget`, plus `estimatedFee`, `estimatedTax`, and `estimatedTotalCost` per gift. Multiply by `numberOfGifts` to project the total Campaign spend. - Estimates use the Campaign's default shipping country and current pricing. Actual cost per gift may vary slightly based on the recipient's country and the variant they select. #### Permissions - Requires: `campaigns:read` # Campaigns API (V2): Plan, Schedule, and Report on Gifting Campaigns Source: https://docs.snappy.com/modules/api/v2/campaigns/overview Plan, schedule, and report on gifting campaigns. Estimate cost, track delivery, and trigger from CRM or HR events. A **Campaign** is the primary organizational object for configuring and sending gifts. It acts as a reusable template that defines all the settings for a gifting activity - including the gift type, budget, branding, notification messages, and billing. Every gift in Snappy must be created within a Campaign. Want to understand how **Campaigns** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page. *** ## The Campaign Object | Field | Type | Description | | :-------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the Campaign | | `name` | string | Display name of the Campaign | | `status` | string | Current status of the Campaign. See [Campaign Statuses](#campaign-statuses) below | | `type` | string | Campaign type. Possible values: `anniversary`, `birthday`, `schedule`, `marketing`, `oneOffs`, `newHire`. The API only allows creating `oneOffs` - other types are created via the Snappy Dashboard. | | `source` | string | Indicates how the Campaign was created (e.g. `dashboard`, `dashboard_ai`, `api_native`, `api_zapier`, `api_make`, `api_salesforce`, `api_ftp`) | | `accountId` | string | The ID of the Account this Campaign belongs to | | `companyId` | string | The ID of the Company this Campaign belongs to | | `account` | object | Embedded Account summary containing `id` and `name` | | `customization` | object | Gift Customization configuration including `giftProperties`, `notificationPolicy`, and `recipientExperience`. See [Core Concepts & Data Models → Gift Customization](/pages/snappy-core-concepts-and-data-models) | | `createdAt` | string | ISO 8601 timestamp of when the Campaign was created | | `updatedAt` | string | ISO 8601 timestamp of the last update | *** ## Campaign Statuses | Status | Description | | :---------- | :-------------------------------------------------------------------------- | | `draft` | The Campaign is being configured and is not yet ready to send gifts | | `pending` | The Campaign is awaiting activation (typically a Dashboard approval step) | | `scheduled` | The Campaign is scheduled to activate at a future date | | `active` | The Campaign is live and gifts can be created under it | | `live` | Equivalent to `active` for some Campaign types (Dashboard-driven) | | `paused` | The Campaign has been temporarily paused. No new gifts can be created | | `sent` | All gifts in the Campaign have been sent (terminal state for one-off sends) | | `archived` | The Campaign has been archived. No new gifts can be created | *** ## Key Concepts & Business Rules #### Every gift requires a Campaign A Campaign must exist before any gifts can be created. It provides the context, budget, and configuration for every gift sent within it. #### Collection or Product - not both A Campaign is configured with either a Collection (recipient chooses from a catalog) or a specific Product (a predetermined item). These are mutually exclusive - you cannot assign both to the same Campaign. #### Billing Method Each Campaign is assigned exactly one Billing Method at creation. This is the funding source that will be debited each time a gift is sent under this Campaign. If the Billing Method has insufficient funds, gift creation will fail. When a Campaign is created via the API, it is automatically assigned the Account's default Billing Method. When created via the Dashboard, you can select any active Billing Method on the Account. #### Gift Customization inheritance Campaigns inherit Gift Customization defaults from their parent Account and Company. Any settings defined at the Campaign level override the Account defaults and apply to all gifts created under this Campaign, unless overridden again at the individual Gift level. See [Core Concepts & Data Models → Gift Customization](/pages/snappy-core-concepts-and-data-models) for the full inheritance model. *** ## How to Work with Campaigns **Create a Campaign** Campaigns can be created via the Snappy Dashboard or programmatically via the API. For most use cases we recommend creating Campaigns in the Dashboard, where you can configure Gift Customization settings visually and preview the recipient experience. ```text theme={null} theme={null} POST /v2/campaigns ``` **Retrieve Campaigns** To retrieve a list of all Campaigns available to your API key: ```text theme={null} theme={null} GET /v2/campaigns ``` To retrieve a specific Campaign by ID: ```text theme={null} theme={null} GET /v2/campaigns/{campaignId} ``` **Update a Campaign** Campaign settings can be updated after creation. Changes to Gift Customization settings apply only to gifts created **after** the update - previously created gifts are not affected. ```text theme={null} theme={null} PATCH /v2/campaigns/{campaignId} ``` **Estimate the Campaign Cost** Before launching a Campaign, you can retrieve an estimated cost based on the configured budget and a target number of gifts: ```text theme={null} theme={null} GET /v2/campaigns/{campaignId}/estimatedCost ``` * # Update campaign by ID Source: https://docs.snappy.com/modules/api/v2/campaigns/update-campaign-by-id patch /v2/campaigns/{campaignId} Use this endpoint to update the settings of an existing Campaign, including its Gift Customization configuration. Changes apply only to gifts created **after** the update - previously created gifts are not affected. ###### Required fields: - `campaignId` - the Campaign identifier, passed as a path parameter ###### Optional fields: (in request body) - `name` - new display name of the Campaign (must be unique within the Company) - `status` - Campaign lifecycle status. Allowed values via API: `active`, `paused`, `archived` - `customization` - updated Gift Customization configuration. The entire `customization` object must be provided when updating. ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Behavior Notes: - Only `active`, `paused`, and `archived` status transitions are allowed via the API. Other status changes (`draft` → `pending` → `scheduled` etc.) are managed by Snappy automatically or via the Dashboard. - Returns `404` if the Campaign doesn't exist. - Returns `409` when renaming the Campaign to a name that already exists in the Company. - Returns `422` when the requested update conflicts with business rules (e.g. updating a `sent` or `archived` Campaign). #### Permissions - Requires: `campaigns:update` # Get collection budgets Source: https://docs.snappy.com/modules/api/v2/collections/get-collection-budgets get /v2/collections/budgets Use this endpoint to retrieve the available minimum and maximum budget ranges for Collections. Use this to determine the price points available to recipients before configuring a Campaign. ###### Required fields: - `type` query parameter - Collection type (`swag`, `gifts`, `local experiences`, `custom`) ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `accountId` query parameter - Account ID for Account-scoped Collections - `collectionId` query parameter - scope budget ranges to a specific Collection - `countries` query parameter - comma-separated list of supported countries (default `US`) - `Request-Source` header - source of the request ###### Please note: - The response returns an array of `{ min, max }` ranges. Use these as the price tiers recipients can choose from on the gift page. - Returns `422` when the supplied `collectionId` doesn't exist or isn't accessible. #### Permissions - Requires: `collections:read` # Get collection product Source: https://docs.snappy.com/modules/api/v2/collections/get-collection-product get /v2/collections/{collectionId}/products/{productId} Use this endpoint to retrieve a specific Product from a specific Collection. ###### Required fields: - `collectionId` - the Collection identifier, passed as a path parameter - `productId` - the Product identifier, passed as a path parameter ###### Optional parameters: - `minBudget` / `maxBudget` query parameters - optional inclusive budget range - `companyId` query parameter - Company ID (when not inferable from the calling key) - `accountId` query parameter - Account ID for Account-scoped Collections - `country` query parameter - ISO 3166-1 alpha-2 country code (default `US`) - `fields` query parameter - comma-separated field projection. Valid values: `mediaItems`, `tags`, `brand`, `optionAttributes`, `notices`, `pricing`, `supportedCountries`, `full` - `throwIfNotFound` query parameter - when `true` (default), returns `404` if the product isn't in the Collection. When `false`, returns an empty response instead. - `Request-Source` header - source of the request ###### Please note: - Returns the full Product object including variants when `fields=full` is provided. - Returns `404` when the Product isn't in the Collection and `throwIfNotFound=true`. #### Permissions - Requires: `products:read` # Get collection products Source: https://docs.snappy.com/modules/api/v2/collections/get-collection-products get /v2/collections/{collectionId}/products Use this endpoint to retrieve a list of products available within a specific Collection. Filter by budget range and country. ###### Required fields: - `collectionId` - the Collection identifier, passed as a path parameter - `minBudget` query parameter - inclusive minimum budget (1-20,000) - `maxBudget` query parameter - inclusive maximum budget (1-20,000) ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `accountId` query parameter - Account ID for Account-scoped Collections (Swag) - `country` query parameter - ISO 3166-1 alpha-2 country code (default `US`) - `fields` query parameter - comma-separated field projection. Valid values: `mediaItems`, `tags`, `brand`, `optionAttributes`, `notices`, `pricing`, `supportedCountries`, `full` - `Request-Source` header - source of the request ###### Pagination: - `skip` query parameter - number of records to skip (default `0`) - `limit` query parameter - max records per page (1-100, default `100`) ###### Please note: - This endpoint uses a single `country` (not the plural `countries` used elsewhere). - The Product objects returned include their full variant list when `fields=full` is provided. - Returns `422` when the Collection doesn't exist or isn't accessible to the calling key. #### Permissions - Requires: `products:read` # Get collection products count Source: https://docs.snappy.com/modules/api/v2/collections/get-collection-products-count get /v2/collections/{collectionId}/products/count Use this endpoint to retrieve the number of products available within a specific Collection. Use this for display purposes or pagination planning before fetching the full product list. ###### Required fields: - `collectionId` - the Collection identifier, passed as a path parameter - `minBudget` query parameter - inclusive minimum budget (1-20,000) - `maxBudget` query parameter - inclusive maximum budget (1-20,000) ###### Optional parameters: - `countries` query parameter - comma-separated list of supported countries (default `US`) - `Request-Source` header - source of the request ###### Please note: - Returns a single `count` field with the total number of products matching the supplied budget range and country filter. - Returns `422` when the Collection doesn't exist or isn't accessible. #### Permissions - Requires: `products:read` # Get collections Source: https://docs.snappy.com/modules/api/v2/collections/get-collections get /v2/collections Use this endpoint to search for and retrieve a list of available Collections based on your specified criteria, including budget range, supported countries, and Collection type. ###### Required fields: - `budget` query parameter - budget value (1-20,000) to scope Collections to ###### Filtering options: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `accountId` query parameter - Account ID. **Required to retrieve Swag Collections.** - `countries` query parameter - comma-separated list of supported countries (default `US`) - `types` query parameter - comma-separated Collection types (`swag`, `gifts`, `local experiences`, `custom`) - `fields` query parameter - comma-separated field projection. Valid values: `coverImage`, `thumbnails`, `createdBy`, `updatedAt`, `full` - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`) ###### Pagination: - `skip` query parameter - number of records to skip (default `0`) - `limit` query parameter - max records per page (1-100, default `100`) ###### Please note: - Swag Collections will not appear in the response unless `accountId` is supplied. - Returns `422` (`422_PBLC_001`) when the supplied `companyId` or `accountId` doesn't exist. #### Permissions - Requires: `collections:read` # Collections API (V2): Curated Gift Catalogs Source: https://docs.snappy.com/modules/api/v2/collections/overview Bundle products into curated collections by budget, theme, or audience, and serve them through the Snappy V2 API. A **Collection** is a curated catalog of gift items tailored to a specific theme, budget range, and audience (e.g. "Birthday Gifts Under \$50"). The gift recipient then chooses their preferred item directly from this collection. 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. Looking for the V3 endpoint that returns Products within a Collection? See [Collections (V3)](/modules/api/v3/collections/overview) for `GET /v3/collections/{collectionId}/products` with JSON:API filtering, cursor pagination, and the V3 response envelope. *** ## The Collection Object | Field | Type | Description | | :----------- | :-------------------------- | :-------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the Collection | | `name` | string | Display name of the Collection | | `types` | array | The category of items in this Collection. Possible values: `gifts`, `swag`, `local experiences`, `custom` | | `coverImage` | string (nullable) | URL of the Collection's cover image, used for display in campaign setup and recipient experience | | `thumbnails` | array (nullable) | List of URLs of thumbnail images representing items within the Collection | | `createdBy` | string (nullable) | The team or user who created the Collection | | `createdAt` | string (ISO 8601) | When the Collection was created | | `updatedAt` | string (ISO 8601, nullable) | When the Collection was last updated | *** ## Paginated Response The Collections list endpoint returns a paginated response with the following envelope: | Field | Type | Description | | :-------- | :----- | :------------------------------------------------------------------------ | | `results` | array | The list of Collection objects returned for the current page | | `skip` | number | The number of items skipped from the start of the list | | `limit` | number | The maximum number of items returned per page. Default and maximum is 100 | For details on how to paginate through large result sets, see [Request & Response Standards → Pagination](/pages/request-response-standards). *** ## Key Concepts & Business Rules #### Collections are read-only via the API Collections available to your Account are curated by Snappy or created via the Snappy Dashboard. You cannot create or modify Collections through the API - only retrieve them. #### Collection types The `types` field indicates the category of items within the Collection. This can be useful for filtering Collections when configuring a Campaign for a specific use case - for example, selecting only `swag` Collections for a branded merchandise campaign. #### Swag Collections require `accountId` Swag Collections are scoped to specific Accounts. To retrieve them, you must include the `accountId` parameter on the list endpoint - they will not appear without it. #### Two scopes, depending on what you call The Collections endpoints split across two permission scopes: * **`collections:read`** - list Collections and retrieve budget ranges (`GET /v2/collections`, `GET /v2/collections/budgets`) * **`products:read`** - retrieve products within a Collection or count them (`GET /v2/collections/{id}/products`, `GET /v2/collections/{id}/products/{productId}`, `GET /v2/collections/{id}/products/count`) Make sure your API key carries both scopes if your integration browses Collections and their products. #### Assigning a Collection to a Campaign To use a Collection in a gifting flow, assign its `id` to a Campaign. Recipients will then browse and select from that Collection when they claim their gift. A Campaign is assigned either a Collection or a specific Product - not both. See [Campaigns](/modules/api/v2/campaigns/overview) for details. *** ## How to Work with Collections **List Collections** Search for and retrieve a list of available Collections based on your specified criteria: ```text theme={null} theme={null} GET /v2/collections ``` Filtering options: * Budget value (required) * Supported countries * Collection types (e.g. `gifts`, `swag`, `local experiences`, `custom`) * Account ID (required for Swag Collections) **Retrieve Collection Budget Ranges** Retrieve the available minimum and maximum budget ranges for Collections - useful for determining the price points available to recipients before configuring a Campaign: ```text theme={null} theme={null} GET /v2/collections/budgets ``` Filtering options: * Collection type (required) * Collection ID * Supported countries **List Products within a Collection** Retrieve a list of products available within a specific Collection: ```text theme={null} theme={null} GET /v2/collections/{collectionId}/products ``` Filtering options: * Budget range (min/max - both required) * Country **Get a Specific Product from a Collection** Retrieve a specific product from a specific Collection by ID: ```text theme={null} theme={null} GET /v2/collections/{collectionId}/products/{productId} ``` The Product objects returned in this response follow the standard Product schema. See [Products & Variants](/modules/api/v2/products/overview) for the full object structure. **Count Products in a Collection** Retrieve the number of products available within a specific Collection - useful for display purposes or pagination planning before fetching the full product list: ```text theme={null} theme={null} GET /v2/collections/{collectionId}/products/count ``` # Create demo gift Source: https://docs.snappy.com/modules/api/v2/gifts/create-demo-gift post /v2/gifts/demo Use this endpoint to create non-claimable demo gifst to preview and test the full recipient experience without incurring any costs. ###### Required fields: - `campaignId` - the Campaign ID to send under - `recipients` - array of demo recipients ###### Optional fields: - `customization` - Gift Customization overrides - `metadata` - optional key-value pairs (max 50 pairs) - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Behavior Notes: - Demo gifts are **free** and do not affect your Account's budget or Billing Method. - Demo gifts **cannot be redeemed** for an actual product - they're for preview only. - The recipient experience is fully interactive: unwrapping animation, collection browsing, etc. Just no Order is placed at the end. - Same request body shape as [Create gifts](/modules/api/v2/gifts/create-gifts). #### Permissions - Requires: `gifts:create:demo` # Create gifts Source: https://docs.snappy.com/modules/api/v2/gifts/create-gifts post /v2/gifts Use this endpoint to create one or more gifts within a Campaign and initiate the Triggered Gifting flow. Snappy will notify recipients based on the Campaign's Notification Policy. ###### Required fields: - `campaignId` - the Campaign ID to send under - `recipients` - array of recipients, each requiring at minimum a `firstname` and an identifier to send to (email or phone). Include a unique `key` per recipient for duplicate detection. ###### Optional fields: - `customization` - Gift Customization overrides (`giftProperties`, `notificationPolicy`, `recipientExperience`). Overrides apply only to gifts created in this call. - `metadata` - optional key-value pairs (max 50 pairs, keys up to 40 chars, values up to 500 chars) - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`) ###### Behavior Notes: - Supports **batch creation** - multiple recipients in a single request. Each recipient generates its own Gift with its own `link` and `key`. - A successful request returns the Gift objects with an initial status of `unopened` and a unique claim `link` for each recipient. - Webhook events fired through the lifecycle: `gift-status-changed` (`unopened` → `unwrapped` → `opened` → `claimed` / `expired`), and `gift-notification-initial-sent`. - The top-level `sendingMethod` field is **deprecated** - use `customization.notificationPolicy.sendingChannels` instead. #### Permissions - Requires: `gifts:create` # Expire a gift Source: https://docs.snappy.com/modules/api/v2/gifts/expire-gift post /v2/gifts/{giftId}/expire Use this endpoint to manually expire a gift that has not yet been claimed, preventing the recipient from selecting a product and generating an order. ###### Required fields: - `giftId` - the Gift identifier, passed as a path parameter ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Behavior Notes: - Returns the updated `status` (always `expired`) on success. - Returns `409` (`409_PBLC_001`) if the Gift has already been claimed and cannot be expired. - Fires the `gift-status-changed` webhook event with `status: expired`. #### Permissions - Requires: `gifts:update` # Get gift by ID Source: https://docs.snappy.com/modules/api/v2/gifts/get-gift-by-id get /v2/gifts/{giftId} Use this endpoint to retrieve a specific Gift by its ID. Useful for tracking delivery status, retrieving final gift cost, or reviewing the recipient and campaign context. ###### Required fields: - `giftId` - the Gift identifier, passed as a path parameter ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `expand` - comma-separated objects to inline (`finalCost`, `deliveryDetails`, `estimatedCost`, `recipient`, `orders`, `customization`) - `Request-Source` header - source of the request ###### Please note: - Use `expand=orders` to retrieve the full order history including cancelled orders. - Use `expand=finalCost` to retrieve the actual cost after the gift has been claimed. #### Permissions - Requires: `gifts:read:masked` or `gifts:read:unmasked` # Get gifts Source: https://docs.snappy.com/modules/api/v2/gifts/get-gifts get /v2/gifts Use this endpoint to search for and retrieve a list of gifts based on your specified criteria - campaign, status, sending channels, recipient, and more. ###### Filtering options: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `campaignIds` - comma-separated list of Campaign IDs (min 1 item) - `keys` - comma-separated list of idempotency keys - `statuses` - comma-separated list of gift statuses (`unopened`, `opened`, `unwrapped`, `claimed`, `expired`) - `sendingChannels` - comma-separated list of sending channels (`mail`, `sms`, `link`, `code`, `slack`, `teams`) - `externalRecipientId` - filter by the recipient's external (your-system) ID - `experienceId` - filter by experience ID - `invoiceId` - filter by invoice ID - `createdBefore` / `createdAfter` - YYYY-MM-DD date bounds (exclusive) - `fields` - comma-separated field projection. Valid values: `id`, `status`, `budgetPlan`, `sendingChannels`, `tyn`, `expirationDate`, `createdAt`, `link`, `campaignId`, `metadata`, `full` - `expand` - comma-separated objects to include in the response. Valid values: `finalCost`, `deliveryDetails`, `estimatedCost`, `recipient`, `orders`, `customization` - `Request-Source` header - source of the request ###### Pagination: - `skip` query parameter - number of records to skip (default `0`) - `limit` query parameter - max records per page (1-100, default `100`) ###### Please note: - The default `fields` set returns `id`, `link`, and `status` only. Use `fields=full` for the complete Gift shape, or specify the fields you need. - Use `expand` to inline related objects like `recipient`, `orders`, and `finalCost` - by default these are not included. - The `senderId` and `senderEmail` parameters are **deprecated** - do not use them in new integrations. #### Permissions - Requires: `gifts:read:masked` or `gifts:read:unmasked` # Gifts API (V2): Create, Track, and Expire Gifts Source: https://docs.snappy.com/modules/api/v2/gifts/overview Programmatically create, retrieve, update, and expire gifts. Magic-link claims, demo gifts, and webhook-triggered creation. A **Gift** is the core transactional object in Snappy. It represents the entire gifting experience for a single recipient within a Campaign - from creation through notification, selection, and final delivery. Want to understand how **Gifts** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page. This page focuses on the **Triggered Gifting** model, where the recipient is notified and selects their own gift. In the **Embedded Marketplace** model, a Gift object is still created internally, but the developer primarily interacts with the Order object. See [Orders](/modules/api/v2/orders/overview) for details. *** ## The Gift Lifecycle The Gift object moves through the following stages in the Triggered Gifting model: | Stage | Description | | :------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | | **Creation** | Gift is initiated and linked to a recipient and Campaign | | **Notification** | Recipient is notified via email, SMS, or other channels | | **Selection** | Recipient clicks the claim link, opens the Snappy Recipient Experience, browses the Collection, selects a Variant, 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 | Use Webhooks to track Gift status changes in real time rather than polling. See [Webhook Event Types](/pages/webhook-event-types). *** ## The Gift Object ### Core Fields | Field | Type | Description | | :----------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the Gift | | `campaignId` | string | The ID of the Campaign this Gift was created under | | `companyId` | string | The ID of the Company this Gift belongs to | | `status` | string | Current status of the Gift. See [Gift Statuses](#gift-statuses) below | | `link` | string | The recipient's unique claim URL. Share this link to notify recipients manually, or let Snappy send it automatically based on the Campaign's Notification Policy | | `success` | boolean | Indicates whether the Gift was created successfully. Relevant for both single and batch gift creation | | `createdAt` | string | ISO 8601 timestamp of when the Gift was created | *** ### Recipient The `recipient` object contains the details of the person this Gift was sent to. | Field | Type | Description | | :--------------------- | :----- | :---------------------------------------------------------------------------------------------------------------- | | `recipient.firstname` | string | Recipient's first name | | `recipient.lastname` | string | Recipient's last name | | `recipient.email` | string | Recipient's email address | | `recipient.phone` | string | Recipient's phone number (E.164 format) | | `recipient.externalId` | string | Your internal ID for this recipient, used to map Snappy recipients to records in your own system | | `recipient.key` | string | The unique idempotency key provided at gift creation. See [Duplicate Detection](/pages/duplicate-gifts-detection) | Recipient fields may be masked depending on the permissions of your API key. See [Authentication & Security](/pages/authentication-and-security#data-privacy-pii-masking) for details. *** ### Cost Snappy returns cost information in two fields depending on the Gift's current stage: | Field | Type | Description | | :-------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `estimatedCost` | object | The projected cost at the time of gift creation, before the recipient has selected a product. Contains `budget`, `estimatedFee`, `estimatedTax`, and `estimatedTotalCost` | | `finalCost` | object | The actual cost after the recipient has claimed the gift and an Order has been placed. Contains `cost`, `finalFee`, `finalTax`, and `totalFinalCost`. Populated only after the gift is claimed | `estimatedCost` is available immediately after gift creation. `finalCost` is populated once the recipient claims the gift and an Order is generated. *** ### Delivery Details The top-level `deliveryDetails` object represents the delivery status of the **active order** associated with this Gift. | Field | Type | Description | | :----------------------------------- | :----- | :------------------------------------------------------------------------- | | `deliveryDetails.status` | string | Current delivery status. See [Delivery Statuses](#delivery-statuses) below | | `deliveryDetails.carrier` | string | The shipping carrier handling the delivery | | `deliveryDetails.trackingNumber` | string | The carrier's tracking number | | `deliveryDetails.trackingLink` | string | A direct link to the carrier's tracking page | | `deliveryDetails.outForDeliveryDate` | string | ISO 8601 timestamp of when the item was marked out for delivery | | `deliveryDetails.deliveredAt` | string | ISO 8601 timestamp of when the item was delivered | A Gift can hold multiple Orders - for example if an original Order was cancelled and a replacement was placed. The top-level `deliveryDetails` always reflects the active Order. See the [Orders API](/modules/api/v2/orders/overview) for the full Order detail. *** ### Orders The `orders` array contains all Orders associated with this Gift, including cancelled ones. See [Orders API (V2)](/modules/api/v2/orders/overview) for the full Order schema documentation. | Field | Type | Description | | :------------------------- | :----- | :-------------------------------------------------------------------------------------------------- | | `orders[].id` | string | Unique identifier for the Order | | `orders[].status` | string | Current status of the Order (`active` or `cancelled`) | | `orders[].orderRecipient` | object | The recipient details used for this specific Order. Contains `firstName`, `lastName`, and `country` | | `orders[].orderedProducts` | array | The products included in this Order | *** ### Customization The `customization` object on the Gift represents the effective Gift Customization settings applied to this specific Gift - including any overrides made at the Gift level on top of the Campaign defaults. | Field | Type | Description | | :------------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------- | | `customization.giftProperties` | object | Budget range, expiration, and other core gift settings | | `customization.recipientNotifications` | object | Notification channels (`mail`, `sms`, etc.) and reminder settings | | `customization.recipientExperience` | object | Visual and interactive settings for the recipient claim experience, including reveal animation, greeting, and post-claim redirect | For a full explanation of Gift Customization and its inheritance model, see [Core Concepts & Data Models → Gift Customization](/pages/snappy-core-concepts-and-data-models). *** ### Thank You Note After claiming a gift, recipients have the option to leave a thank you note for the sender. Thank you notes are delivered to the sender by email and are also displayed in the Gratitude Wall in the Snappy Dashboard. | Field | Type | Description | | :---- | :----- | :----------------------------------------------------------------------------------------------------- | | `tyn` | string | The thank you note left by the recipient after claiming the gift. `null` if no note has been submitted | To be notified when a recipient submits a thank you note, listen for the `thank-you-note-created` webhook event. See [Webhook Event Types](/pages/webhook-event-types). *** ## Gift Statuses | Status | Description | | :---------- | :---------------------------------------------------------------------- | | `unopened` | Gift has been sent but the recipient has not clicked the claim link yet | | `unwrapped` | Recipient has clicked the link but has not yet viewed the gift options | | `opened` | Recipient has viewed the available gift options | | `claimed` | Recipient has selected a gift and provided their shipping details | | `expired` | Gift reached its expiration date without being claimed | *** ## Delivery Statuses | Status | Description | | :--------------- | :----------------------------------------- | | `orderReceived` | The fulfillment request has been received | | `processing` | The item is being prepared for shipment | | `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 | *** ## Key Concepts & Business Rules #### Every Gift must belong to a Campaign A Gift cannot be created without a Campaign. The Campaign provides the configuration context - budget, collection or product, branding, and notification settings - that the Gift inherits. #### The Gift is the Triggered Gifting experience In the Triggered Gifting model, the Gift object represents the full recipient journey from notification through selection to delivery. A Gift is essentially an "intent to send" until the recipient actually claims it. No physical item is reserved, and no final billing occurs until the `status` changes to `claimed` and an Order is generated. #### One Gift per recipient per send Each Gift represents the experience for a single recipient. To send to multiple recipients, include multiple entries in the `recipients` array - each will generate its own Gift object with its own unique `link` and `key`. #### The claim link is single-use and recipient-specific The `link` returned on the Gift object is a unique, personalized URL for that recipient. It should not be shared with other recipients or reused across sends. #### Gift Customization overrides Any customization settings provided at the Gift level override the Campaign defaults for that specific Gift only. The Campaign's default settings are not affected. #### Expiration Gifts do not remain open indefinitely. They are governed by the expiration settings defined in their parent Campaign. If the window closes before the recipient makes a selection, the gift status becomes `expired` and the claim link is permanently deactivated. #### Updating Gifts As long as the gift is not claimed (meaning its status is `unopened`, `unwrapped`, or `opened`), you can update its Customization settings or manually expire it. Once a gift has been claimed, its settings are locked. #### Duplicate detection We **strongly recommend** including a unique `key` for every recipient in a gift creation request. While not strictly required, this key is permanent and does not expire even if the gift itself expires. Reusing a key will trigger a duplicate detection error, which protects your account from accidental double-billing. #### Estimated vs. Final Cost Because Snappy covers shipping and taxes, the exact cost of a gift isn't known until the recipient provides their shipping address. Use `estimatedCost` to check your budget exposure, but rely on `finalCost` for your actual accounting once the gift is claimed. *** ## How to Work with Gifts **Create Gifts** Create one or more gifts within a Campaign: ```text theme={null} theme={null} POST /v2/gifts ``` **Retrieve Gifts** ```text theme={null} theme={null} GET /v2/gifts ``` **Retrieve a Gift by ID** ```text theme={null} theme={null} GET /v2/gifts/{giftId} ``` **Update Gift by ID** ```text theme={null} theme={null} PATCH /v2/gifts/{giftId} ``` **Expire a Gift** ```text theme={null} theme={null} POST /v2/gifts/{giftId}/expire ``` **Create a Demo Gift** A non-claimable gift used to preview the recipient experience without incurring any cost: ```text theme={null} theme={null} POST /v2/gifts/demo ``` **Create Gifts by Webhook** A simplified webhook-style endpoint designed for integration with third-party systems (CRMs, marketing automation platforms). Authentication is via API key as a query parameter: ```text theme={null} theme={null} POST /v2/webhooks/send-gifts ``` *** **Claim a Gift** and **Cancel an Order on a Gift** are documented under the [Orders API (V2)](/modules/api/v2/orders/overview) since they sit on the Order side of the lifecycle. Both use `/v2/gifts/{giftId}/claim` and `/v2/gifts/{giftId}/cancel` paths respectively. # Update gift by ID Source: https://docs.snappy.com/modules/api/v2/gifts/update-gift-by-id patch /v2/gifts/{giftId} Use this endpoint to update the Gift Customization settings of a specific gift that has not yet been claimed. Changes apply only to unclaimed gifts. ###### Required fields: - `giftId` - the Gift identifier, passed as a path parameter ###### Optional fields: (in request body, under `customization`) - `giftProperties` - budget, expiration, collection / product, guaranteed gift configuration - `notificationPolicy` - sending channels, reminder settings - `recipientExperience` - type, visual elements, post-claim redirect ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) **Behavior Notes** - Changes apply only to **unclaimed** gifts (status `unopened`, `unwrapped`, or `opened`). Updating a claimed gift returns `409` (`409_PBLC_001`). - Returns `404` if the Gift doesn't exist or isn't accessible to the calling key. #### Permissions - Requires: `gifts:update` # Autocomplete order address Source: https://docs.snappy.com/modules/api/v2/orders/autocomplete-order-address get /v2/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** - `address` query parameter - partial address text from the user. 4-128 characters. - `country` query parameter - country code to scope the suggestions to. 2-3 characters. **Optional parameters** - `companyId` query parameter - Company identifier, when not inferable from the API key context (8+ alphanumeric characters) ###### Please note - Returns an array of suggestions in the `results` field. Each entry follows the standard address shape (`addressLine1`, `addressLine2`, `city`, `state`, `zipcode`). - 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. - This endpoint is a UI helper - it suggests addresses but does **not** validate deliverability. Pair it with `POST /v2/orders/addresses/validate` before placing an order if you need verified-deliverable addresses. #### Permissions - Requires: `orders:read:masked` or `orders:read:unmasked` # Cancel an order Source: https://docs.snappy.com/modules/api/v2/orders/cancel-order post /v2/gifts/{giftId}/cancel Use this endpoint to cancel an order that has not yet been processed or shipped. Use this when the recipient or sender requests a cancellation before the shipment is in transit. ###### Required fields - `giftId` - the Gift identifier whose Order should be cancelled, passed as a path parameter ###### Optional parameters - `companyId` query parameter - Company identifier, when not inferable from the API key context - `Request-Source` header - source of the request ###### Behavior Notes - Cancels the active Order on the Gift and returns the cancelled Order ID with `status: cancelled`. - Orders can only be cancelled before they are processed or shipped. Once a shipment is in transit, cancellation returns an error and the request is rejected. - A new Order can be placed against the same Gift after cancellation by calling `POST /v2/gifts/{giftId}/claim` again with a different variant or address. - The Billing Method is credited back when the Order is successfully cancelled. #### Permissions - Requires: `orders:cancel` # Claim a gift Source: https://docs.snappy.com/modules/api/v2/orders/claim-gift post /v2/gifts/{giftId}/claim Use this endpoint to programmatically claim an existing Gift on behalf of a recipient by providing the selected variant and shipping address. Use this when your system needs to place an order for a Gift without recipient interaction - for example, when you already know the recipient's address and the variant you want to ship. ###### Required fields - `giftId` - the Gift identifier, passed as a path parameter - `variantId` - the specific product Variant to order - `recipient` - the order recipient's contact details and shipping address ###### Optional parameters - `companyId` query parameter - Company identifier, when not inferable from the API key context - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`) ###### Behavior Notes - Returns the Gift with the attached Order on success. The Order's delivery details are surfaced at the top level of the Gift response for convenience. - The Billing Method is debited at this step. - If the Billing Method has insufficient funds, the claim fails and no Order is created. - The Gift transitions to a claimed state and the Order begins fulfillment processing. #### Permissions - Requires: `orders:create` # Orders API (V2): Address Validation, Cancellation & Claims Source: https://docs.snappy.com/modules/api/v2/orders/overview Validate addresses, claim gifts to place orders, cancel orders, and autocomplete shipping data with the V2 Snappy Orders API. An **Order** represents the physical fulfillment event. In V2, Orders are not first-class API resources - they're created as a side effect of the Gifts flow. An Order can be created either **directly by your system** in the **Direct Fulfillment** model (via the two-step gift-claim flow), or automatically by Snappy when a recipient claims their gift in the **Triggered Gifting** model. 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. Looking for the V3 endpoints? See [Orders (V3)](/modules/api/v3/orders/overview). V3 introduces Orders as first-class resources with a dedicated `POST /v3/orders` endpoint that replaces the V2 two-step flow - and adds direct order retrieval, listing, and cancellation by `orderId`. Both V2 and V3 are supported in parallel. This page focuses on the **Direct Fulfillment** model, where your system places orders directly. If you're working with the **Triggered Gifting** model, Orders are created automatically when recipients claim their gift - see [Gifts](/modules/api/v2/gifts/overview) for details. *** ## The Order Object In V2, the Order object is returned as part of the Gift object. A Gift can hold multiple Orders - for example, if an original Order was cancelled and a new one was placed. The active Order's delivery details are surfaced at the top level of the Gift object for convenience. ### Core Fields | Field | Type | Description | | :---------------- | :----- | :-------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the Order | | `status` | string | Current status of the Order. See [Order Statuses](#order-statuses) below | | `orderRecipient` | object | The recipient details used for this specific Order. Contains `firstName`, `lastName`, and `country` | | `orderedProducts` | array | The products included in this Order. See [Ordered Products](#ordered-products) below | *** ### Ordered Products | Field | Type | Description | | :---------------------------------------------- | :----- | :--------------------------------------------------------------------------------- | | `orderedProducts[].selectedProduct.variantId` | string | The Variant ID of the product ordered | | `orderedProducts[].selectedProduct.title` | string | The title of the ordered product | | `orderedProducts[].selectedProduct.type` | string | The product type (e.g. `physicalGift`, `digital`) | | `orderedProducts[].selectedProduct.orderStatus` | string | The fulfillment status of this specific product within the Order | | `orderedProducts[].deliveryDetails` | object | Delivery details for this product. See [Delivery Details](#delivery-details) below | *** ### Delivery Details | Field | Type | Description | | :--------------------------------------------- | :----- | :------------------------------------------------------------------------- | | `deliveryDetails.status` | string | Current delivery status. See [Delivery Statuses](#delivery-statuses) below | | `deliveryDetails.carrier` | string | The shipping carrier handling the delivery | | `deliveryDetails.trackingNumber` | string | The carrier's tracking number | | `deliveryDetails.trackingLink` | string | A direct link to the carrier's tracking page | | `deliveryDetails.deliveryDates.outForDelivery` | string | ISO 8601 timestamp of when the item was marked out for delivery | | `deliveryDetails.deliveryDates.estimated` | string | ISO 8601 timestamp of the estimated delivery date | | `deliveryDetails.deliveryDates.deliveredAt` | string | ISO 8601 timestamp of when the item was delivered | *** ## Order Statuses | Status | Description | | :---------- | :-------------------------------------- | | `active` | The Order is active and being processed | | `cancelled` | The Order has been cancelled | *** ## Delivery Statuses | Status | Description | | :--------------- | :----------------------------------------- | | `orderReceived` | The fulfillment request has been received | | `processing` | The item is being prepared for shipment | | `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 | Use Webhooks to track delivery status changes in real time rather than polling. See [Webhook Event Types](/pages/webhook-event-types). *** ## Key Concepts & Business Rules #### An Order is a sub-entity of a Gift In V2, the Order lives within a Gift - even when your system places the order directly via the Direct Fulfillment flow, a Gift object is created internally and the Order is attached to it. V3 changes this: Orders are first-class resources you can retrieve, list, and cancel directly by `orderId`. #### 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](/modules/api/v2/products/overview). #### Orders can only be cancelled before processing Once an Order has been picked up by the fulfillment partner and is in transit, it can no longer be cancelled. Use the `order-out-of-stock` and `order-canceled` webhook events to handle edge cases proactively. #### A Gift can have multiple Orders but only one is active A Gift maintains a history of all Orders placed against it. If an Order is cancelled, a new one can be placed against the same Gift - but only one Order is active at any time. The Gift's top-level `deliveryDetails` always reflects the active Order. The `orders` array on the Gift object contains the full Order history, including cancelled ones.. #### Address validation reduces fulfillment failures Invalid or incomplete shipping addresses are a common cause of fulfillment failures. We recommend calling `POST /v2/orders/addresses/validate` before placing any order, especially when addresses are entered by end users in your platform UI. #### Billing is triggered at Order creation The Billing Method 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. #### Order Status vs. Delivery Status The top-level Order `status` indicates the lifecycle of the transaction (`active` vs. `cancelled`). The actual shipping progress is tracked inside the `deliveryDetails.status` of each ordered product. *** ## How to Work with Orders (V2) **Claim a Gift** Claiming a Gift programmatically places an Order on behalf of the recipient. Provide the selected variant and shipping address - Snappy creates the Order, debits the Billing Method, and begins fulfillment. ```text theme={null} theme={null} POST /v2/gifts/{giftId}/claim ``` Required fields: * `variantId` - the specific product Variant to order * `recipient` - the order recipient's contact details and shipping address Common use cases: * **Auto-claim fallback** - a Gift sent through Triggered Gifting hasn't been claimed by the recipient within a defined window. Your system auto-claims with a pre-selected variant and a known shipping address, ensuring the gift still goes out. * **Embedded Marketplace** - your platform creates a Gift via `POST /v2/gifts` and immediately claims it on the recipient's behalf, placing the Order in a single flow inside your UI. Use the `giftId` to track order delivery status via `GET /v2/gifts/{giftId}` or Webhooks. V3 introduces `POST /v3/orders` as a single-call alternative for placing orders without the Gift-and-claim pattern. If you're starting a new integration, consider [V3 Orders](/modules/api/v3/orders/overview). **Cancel an Order** Cancel an order that has not yet been processed or shipped. A successful request returns the cancelled order ID and its updated status: ```text theme={null} theme={null} POST /v2/gifts/{giftId}/cancel ``` Orders can only be cancelled before they have been processed or shipped. Once a shipment is in transit, cancellation is no longer possible. **Validate Order Address** Validate a shipping address before placing an order. Use this endpoint to catch address errors early and reduce the likelihood of fulfillment failures: ```text theme={null} theme={null} POST /v2/orders/addresses/validate ``` We recommend calling this endpoint as part of your order placement flow, especially when addresses are entered by end users in your platform UI. **Autocomplete Order Address** Retrieve address suggestions based on a partial input string. Useful for building address input fields in your platform UI that help users enter accurate shipping addresses: ```text theme={null} theme={null} GET /v2/orders/addresses/autocomplete ``` # Validate order address Source: https://docs.snappy.com/modules/api/v2/orders/validate-order-address post /v2/orders/addresses/validate Use this endpoint to validate a 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. ###### Required fields - `country` - country code, 2-3 uppercase letters (ISO 3166-1 alpha-2 or alpha-3) - `address` - the address object to validate. Contains: - `addressLine1` - street address (validated as required) - `addressLine2` - apartment, suite, floor (optional) - `city` - city name (validated as required) - `state` - state or province code (2-3 uppercase letters) - `zipcode` - postal/ZIP code (alphanumeric, 3-10 characters) ###### Optional parameters - `companyId` query parameter - Company identifier, when not inferable from the API key context - `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` (`400_PBLC_003`) with a per-field `errors` array when address validation fails. Each entry includes the offending `path`, a human-readable `message`, and the `errorCode`. - Returns `422` (`422_PBLC_001`) 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 the order placement flow once validation succeeds. #### Permissions - Requires: `orders:read:masked` or `orders:read:unmasked` # V2 API Overview Source: https://docs.snappy.com/modules/api/v2/overview The Gifting API - send gifts to recipients and let them choose their item. Fully supported alongside V3. V2 is the Gifting API. Create a campaign, send a gift, and Snappy handles the recipient experience - they choose their item, enter their address, and an order is generated automatically. ## What's in V2 Create, track, and expire gifts. Configure gifting occasions. Browse the gift catalog. ## When to use V2 Use V2 when you want Snappy to host the recipient experience - the unwrapping, item selection, and address entry. Your integration creates the gift; Snappy handles the rest. Building a new integration? Check out [V3](/modules/api/v3/overview) for direct ordering without a gift flow. # Get product tags Source: https://docs.snappy.com/modules/api/v2/products/get-product-tags get /v2/products/tags Use this endpoint to retrieve a list of all available product tags. Use tags to categorize and filter products when building your catalog UI. ###### Filtering options: - `title` query parameter - search string to filter tags by name. Minimum 3 characters when provided. Omit to return all tags. - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Pagination: - `skip` query parameter - number of records to skip (default `0`) - `limit` query parameter - max records per page (1-100, default `100`) #### Permissions - Requires: `products:read` # Get products Source: https://docs.snappy.com/modules/api/v2/products/get-products get /v2/products Use this endpoint to search for and retrieve a list of available products based on your specified criteria - including budget range, Collection, brand, tags, or free-text search on title and description. ###### Required fields: - `minBudget` query parameter - inclusive minimum budget (1-20,000) - `maxBudget` query parameter - inclusive maximum budget (1-20,000) ###### Filtering options: - `collectionId` - scope search to a specific Collection. When supplied, `brandName`, `brands`, `tags`, `title`, and `description` are ignored. - `brandName` - brand name match (used when `collectionId` is not provided) - `brands` - comma-separated list of brand IDs (used when `collectionId` is not provided) - `tags` - comma-separated list of tag IDs (used when `collectionId` is not provided) - `title` - product title match (used when `collectionId` is not provided) - `description` - product description match (used when `collectionId` is not provided) - `country` query parameter - ISO 3166-1 alpha-2 country code (default `US`) - `companyId` / `accountId` - scoping parameters - `fields` query parameter - comma-separated field projection. Valid values: `mediaItems`, `tags`, `brand`, `optionAttributes`, `notices`, `pricing`, `supportedCountries`, `full` - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`) ###### Pagination: - `skip` query parameter - number of records to skip (default `0`) - `limit` query parameter - max records per page (1-100, default `100`) ###### Please note: - `collectionId` is mutually exclusive with the other text/brand/tag filters. Supplying both will use `collectionId` and ignore the others. - Returns `422` when the supplied `collectionId` doesn't exist or isn't accessible. #### Permissions - Requires: `products:read` # Get variants by product ID Source: https://docs.snappy.com/modules/api/v2/products/get-variants-by-product-id get /v2/products/{productId} Use this endpoint to retrieve a list of all available Variants for a specific Product. Since the Variant is the orderable unit, every Product returned by this endpoint contains at least one Variant - even Products without variations will have a single Variant representing the item. ###### Required fields: - `productId` - the Product identifier, passed as a path parameter ###### Optional parameters: - `minBudget` / `maxBudget` query parameters - inclusive budget range to filter the returned Variants - `companyId` / `accountId` - scoping parameters - `country` query parameter - ISO 3166-1 alpha-2 country code (default `US`) - `fields` query parameter - comma-separated field projection. Valid values: `mediaItems`, `tags`, `brand`, `optionAttributes`, `notices`, `pricing`, `supportedCountries`, `full` - `Request-Source` header - source of the request ###### Please note: - This endpoint returns the full Product object with its `variants` array - not just the variants in isolation. - Returns `422` when the Product doesn't exist or isn't accessible to the calling key. #### Permissions - Requires: `products:read` # Products & Variants API (V2): Catalog Access for Gifting Source: https://docs.snappy.com/modules/api/v2/products/overview Access Snappy's global catalog, product variants, and tags to render a native gifting marketplace in your own UI. A **Product** is a single specific item available in the Snappy catalog - a physical gift, branded swag, a digital item, or a local experience. A **Variant** represents the orderable version of a Product, holding the specific details required to place an order such as size, color, pricing, and supported countries. Because the Variant is always the orderable unit, **every Product contains at least one Variant** - even Products with no variations will have a single Variant object representing the item. When placing an order, always use the `variantId` from the Product's `variants` array. Want to understand how **Products** and **Variants** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page. V3 has shipped a refreshed Catalog API split into two sections: [Products (V3)](/modules/api/v3/products/overview) and [Variants (V3)](/modules/api/v3/variants/overview). V3 introduces JSON:API conventions, per-country availability, and a cleaner separation between the two entities. V2 and V3 are supported in parallel. *** ## The Product Object | Field | Type | Description | | :----------- | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the Product | | `title` | string | Display name of the Product | | `category` | string | Product category path (e.g. `Electronics / Audio / Headphones`) | | `types` | array | The type(s) of the product. Possible values: `digital`, `physical`, `base-product-swag`, `on-demand-swag`, `on-demand-plus-swag`, `premium-swag`, `experience`, `personalized` | | `brand` | object | The brand associated with this Product. Contains `id`, `name`, and `description`. May be `null` when no brand applies. | | `coverImage` | object | The primary display image for the Product. Contains `src`, `type`, and `position` | | `mediaItems` | array | Additional images for the Product. Each item contains `src`, `type`, `position`, and `publicCloudinaryId` | | `options` | object | The available option dimensions for this Product (e.g. `color`, `size`, `scent`, `flavor`). Used to understand what variations exist before retrieving Variants | | `tags` | array | Tags associated with this Product for categorization and filtering. Each tag contains `id`, `name`, `color`, and `position` | | `variants` | array | The orderable variations of this Product. Always contains at least one entry. See [The Variant Object](#the-variant-object) below | | `notices` | object | Any additional notices or disclaimers associated with this Product | *** ## The Variant Object A Variant represents a specific, orderable version of a Product. When placing an order you must always specify the `variantId` - the Product ID alone is not sufficient. | Field | Type | Description | | :------------------- | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the Variant. This is the ID required when placing an order | | `title` | string | Display name of the Variant | | `optionAttributes` | object | The specific option values for this Variant (e.g. `color: Black`, `size: S`) | | `category` | string | Full category path for this Variant | | `types` | array | The type(s) of the variant. Same enum as Product `types` | | `brand` | object | The brand associated with this Variant. Contains `id`, `name`, and `description` | | `description` | string | Short description of the Variant | | `features` | string | Key features of the Variant | | `components` | string | Components included with the Variant | | `mediaItems` | array | Images specific to this Variant. Each item contains `src`, `type`, `position`, and `publicCloudinaryId` | | `information` | object | Detailed product information including `description`, `features`, `specifications` (dimensions, materials, weight, color family), and `including` (what's in the box) | | `pricing` | object | Pricing details by country code. Each entry contains `cost`, `ddp`, `shipping`, `totalFee`, `totalTax`, and `totalFinalCost`. Returned only when `fields=pricing` or `fields=full` is requested. | | `supportedCountries` | array | List of countries this Variant can be shipped to, identified by `countryCode`. Returned only when `fields=supportedCountries` or `fields=full` is requested. | | `tags` | array | Tags associated with this Variant | | `notices` | object | Any additional notices or disclaimers specific to this Variant | | `position` | number | Display order of this Variant relative to others in the same Product | *** ## Key Concepts & Business Rules #### Always order by Variant ID Products are the display-level object - they represent what a recipient sees when browsing. Variants are the orderable units. When placing an order, always use the `variantId`, not the `productId`. Every Product contains a `variants` array with at least one entry, even if the product has no variations. For products without variations, the array will contain a single Variant representing the product itself. Always retrieve the `variantId` from this array before placing an order. #### Options vs. Variant Attributes The `options` field on the Product object shows the available option dimensions (e.g. `color`, `size`). The `optionAttributes` field on each Variant shows the specific combination of those options that Variant represents. Use `options` to understand what choices exist, and `optionAttributes` to identify the exact Variant. #### Pricing is per country Variant `pricing` is keyed by country code and only returned when requested via the `fields` parameter (`fields=pricing` or `fields=full`). Always check that a Variant's `supportedCountries` includes the recipient's country before placing an order. #### Product types Products and Variants carry a `types` array. The enum includes: * **`physical`** - physical items shipped to the recipient's address * **`digital`** - digital items delivered electronically * **`experience`** - local experiences and bookings * **`personalized`** - items with recipient-specific personalization * **`base-product-swag`** / **`on-demand-swag`** / **`on-demand-plus-swag`** / **`premium-swag`** - swag fulfillment categories #### Tags Tags exist at both the Product and Variant level and can be used to filter and categorize items when building a catalog UI. Retrieve all available tags using `GET /v2/products/tags`. #### Field projection with `fields` By default, response objects return a slim set of fields. Use the `fields` query parameter to include additional data: * `mediaItems`, `tags`, `brand`, `optionAttributes`, `notices`, `pricing`, `supportedCountries` * `full` - return everything *** ## How to Work with Products & Variants **List Products** Search for and retrieve a list of available products based on your specified criteria: ```text theme={null} theme={null} GET /v2/products ``` Filtering options: * **Budget range (required)** - `minBudget` and `maxBudget` * Specific Collection (`collectionId`) * Brand (`brandName` or `brands`) * Tags (`tags`) * Title or description text match **List Product Tags** Retrieve a list of all available product tags. Tags can be used to categorize and filter products when building your catalog UI: ```text theme={null} theme={null} GET /v2/products/tags ``` Supports searching by tag name using the `title` parameter (minimum 3 characters). Results are paginated. **Get a Product (with Variants) by ID** Retrieve a Product by its ID, including the full list of Variants: ```text theme={null} theme={null} GET /v2/products/{productId} ``` Filtering options: * Budget range * Country Despite the endpoint summary "Get variants by product ID," this endpoint returns the full Product object with its `variants` array - not just the variants in isolation. **Get a Variant by ID** Retrieve the full details of a specific Variant by its ID: ```text theme={null} theme={null} GET /v2/variants/{variantId} ``` This endpoint returns the parent Product object with the requested Variant in its `variants` array, not the Variant alone. Use the variant ID to locate the specific variant within the returned `variants` list. # Create recipient Source: https://docs.snappy.com/modules/api/v2/recipients/create-recipient post /v2/recipients Use this endpoint to add a new Recipient to your Company's contact list. Once created, the Recipient can be associated with multiple Accounts and referenced by ID across multiple gift sends. ###### Required fields: - `firstName` - the Recipient's first name - `country` - ISO 3166-1 alpha-2 country code - `accounts` - array of at least one Account ID to associate this Recipient with ###### Optional fields: - `lastName` - the Recipient's last name - `email` - the Recipient's primary email (used as the primary identifier) - `emailOverride` - alternative email for notifications (e.g. personal email when `email` is work email) - `mobilePhone` - mobile phone number - `birthday` - ISO 8601 date for birthday-trigger flows - `externalId` - your internal ID for this Recipient (HRIS, CRM) - `type` - Recipient type. Currently only `employee` is supported. - `employee` - work-related details: `workingSince` (start date), `department` - `owner` - the person responsible for this Recipient: `firstName`, `lastName`, `email` - `recipientCustomFields` - array of custom field objects (`fieldName`, `displayName`, `fieldValue`) ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Behavior Notes: - Returns `201` with the created Recipient on success. - Returns `409` if a Recipient with the same identifying data already exists in the Company. - Returns `422` for business-rule violations (e.g. invalid Account IDs). - The `source.type` on the response will be `apiIntegration` for recipients created via this endpoint. #### Permissions - Requires: `recipients:create` # Delete recipient by ID Source: https://docs.snappy.com/modules/api/v2/recipients/delete-recipient-by-id delete /v2/recipients/{recipientId} Use this endpoint to permanently delete a specific Recipient from your Company's contact list. ###### Required fields: - `recipientId` - the Recipient identifier, passed as a path parameter ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Behavior Notes: - **Deletion is immediate and permanent.** The Recipient record cannot be recovered after deletion. - Existing Gifts that were sent to this Recipient are not affected - they remain valid and retrievable. - Returns `204 No Content` on success. - Returns `404` if the Recipient doesn't exist or isn't accessible. #### Permissions - Requires: `recipients:delete` # Get recipient by ID Source: https://docs.snappy.com/modules/api/v2/recipients/get-recipient-by-id get /v2/recipients/{recipientId} Use this endpoint to retrieve the full details of a specific Recipient. ###### Required fields: - `recipientId` - the Recipient identifier, passed as a path parameter ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Please note: - PII fields are masked unless your API key has the `recipients:read:unmasked` scope. - Returns `404` if no Recipient exists for the supplied `recipientId`, or if it's not accessible to the calling API key. - Returns `422` for business-rule violations. #### Permissions - Requires: `recipients:read:masked` or `recipients:read:unmasked` # Get recipients Source: https://docs.snappy.com/modules/api/v2/recipients/get-recipients get /v2/recipients Use this endpoint to retrieve a list of Recipients based on your specified criteria. Use this when building a recipient picker in your UI, syncing recipients to your system, or finding recipients by name, email, or external ID. ###### Filtering options: - `firstNames` query parameter - comma-separated list of first names - `lastNames` query parameter - comma-separated list of last names - `emails` query parameter - comma-separated list of email addresses (each must be a valid email format) - `emailOverrides` query parameter - comma-separated list of email override addresses - `externalIds` query parameter - comma-separated list of your-system IDs - `accountIds` query parameter - comma-separated list of Account IDs - `sources` query parameter - comma-separated list of recipient sources (`apiIntegration`, `hrisIntegration`, `manual`, `ftpIntegration`, `fileSync`) - `fields` query parameter - comma-separated field projection. Valid values: `id`, `createdAt`, `updatedAt`, `firstName`, `lastName`, `email`, `emailOverride`, `mobilePhone`, `country`, `externalId`, `accounts`, `updatedBy`, `source`, `owner`, `type`, `recipientCustomFields`, `birthday`, `full` - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`) ###### Pagination: - `skip` query parameter - number of records to skip (default `0`) - `limit` query parameter - max records per page (1-200, default `200`) ###### Please note: - All array filters use OR semantics - passing multiple `firstNames` returns recipients matching any of the supplied names. - PII fields are masked unless your API key has the `recipients:read:unmasked` scope. #### Permissions - Requires: `recipients:read:masked` or `recipients:read:unmasked` # Recipients API (V2): Manage Gift Recipients at Scale Source: https://docs.snappy.com/modules/api/v2/recipients/overview Create, retrieve, update, and delete recipients programmatically. Build recipient lists for campaigns and automated triggers. A **Recipient** is the end-user who receives a Gift. Recipients are saved at the Company level, meaning the same Recipient can be added to multiple Accounts and receive gifts from all of them. Want to understand how **Recipients** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page. *** ## The Recipient Object ### Core Fields | Field | Type | Description | | :-------------- | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the Recipient | | `firstName` | string | Recipient's first name | | `lastName` | string | Recipient's last name | | `email` | string | Recipient's primary email address. Used as the primary identifier | | `emailOverride` | string | If set, gift notifications will be sent to this address instead of `email`. The original `email` is retained as the primary identifier | | `mobilePhone` | string | Recipient's mobile phone number | | `country` | string | Recipient's country, identified by ISO 3166-1 alpha-2 country code (e.g. `US`) | | `externalId` | string | Your internal ID for this Recipient. Use this to map Snappy Recipients to records in your own system (e.g. HRIS, CRM) | | `type` | string | The Recipient type (currently `employee` only) | | `accounts` | array | List of Account IDs this Recipient belongs to. A Recipient can belong to multiple Accounts and receive gifts from all of them | | `birthday` | string | ISO 8601 date of the Recipient's birthday. Can be used to trigger automated birthday gifting flows | | `source` | object | Object indicating how the Recipient was created. Contains `type` - one of: `apiIntegration`, `hrisIntegration`, `manual`, `ftpIntegration`, `fileSync` | | `createdAt` | string | ISO 8601 timestamp of when the Recipient was created | | `updatedAt` | string | ISO 8601 timestamp of the last update | | `updatedBy` | string | The ID of the user who last updated this Recipient | *** ### Employee Details The `employee` object contains work-related information about the Recipient. It's populated when the Recipient is synced from an HRIS integration, or manually provided when creating or updating a Recipient via the API. | Field | Type | Description | | :---------------------- | :----- | :------------------------------------------------------------------------------------------------------------ | | `employee.workingSince` | string | ISO 8601 date of the Recipient's employment start date. Can be used to trigger work anniversary gifting flows | | `employee.department` | string | The department the Recipient belongs to | *** ### Owner The `owner` object represents the person responsible for this Recipient - typically their manager or HR contact. | Field | Type | Description | | :---------------- | :----- | :-------------------- | | `owner.firstName` | string | Owner's first name | | `owner.lastName` | string | Owner's last name | | `owner.email` | string | Owner's email address | *** ### Custom Fields The `recipientCustomFields` array contains additional custom attributes defined during your HRIS integration setup. These fields let you store supplemental data about a Recipient beyond the standard fields. | Field | Type | Description | | :------------------------------------ | :----- | :----------------------------------------------- | | `recipientCustomFields[].fieldName` | string | The internal name of the custom field | | `recipientCustomFields[].displayName` | string | The display name of the custom field | | `recipientCustomFields[].fieldValue` | string | The value of the custom field for this Recipient | *** ## Key Concepts & Business Rules #### Recipients are scoped to a Company Recipients are stored at the Company level, not the Account level. A single Recipient record can be associated with multiple Accounts and receive gifts from any of them without needing to be created multiple times. #### Managed list vs. inline Snappy supports two ways to provide recipient details when sending a gift via `POST /v2/gifts`. The choice is made per-request - there's no global setting. **Managed Recipients** Create and maintain a persistent contact list via the API. Once created, a Recipient can be referenced by their `recipientId` across multiple gift sends without re-submitting their details. Recommended for recurring use cases such as employee anniversaries or loyalty rewards. ```json theme={null} theme={null} { "campaignId": "cmp_12345", "recipients": [ { "recipientId": "rec_12345", "key": "jane-doe-anniversary-2026" } ] } ``` **Inline Recipients** Pass recipient details directly in the request body without creating a persistent Recipient record. Recommended for one-off sends where you don't need to maintain the contact in Snappy. Omitting `recipientId` is what tells the API to treat the entry as inline: ```json theme={null} theme={null} { "campaignId": "cmp_12345", "recipients": [ { "firstname": "Jane", "lastname": "Doe", "email": "jane.doe@example.com", "phone": "+15551234567", "externalId": "emp_98765", "key": "jane-doe-anniversary-2026" } ] } ``` Field-name reminder: inline recipients (used inside `POST /v2/gifts`) use `firstname` / `lastname` / `phone` (lowercase). The standalone Recipient resource managed by this API uses `firstName` / `lastName` / `mobilePhone` (camelCase). Same data, different field names depending on which surface you're using. Inline recipients apply to gift creation only. `POST /v2/gifts/{giftId}/claim` does not create or accept inline recipients - it uses an `orderRecipient` object that captures the order's shipping details for that specific claim. #### Email override If a Recipient has an `emailOverride` set, all gift notifications will be sent to that address. The original `email` field is retained as the primary identifier and is unaffected. This is useful when a Recipient's work email should be used for identification but notifications should go to a personal address. #### PII Masking The Recipient object contains significant personal data. If your API key does not have the `recipients:read:unmasked` scope, sensitive fields will be partially redacted in all responses: * **Name:** `J*** D***` * **Email:** `j*******@e*****.com` * **Phone:** `(***) ***-1234` * **IDs:** `3****` See [Authentication & Security](/pages/authentication-and-security#data-privacy-pii-masking) for details on configuring data access permissions. *** ## How to Work with Recipients **List Recipients** Search for and retrieve a list of Recipients based on your specified criteria: ```text theme={null} theme={null} GET /v2/recipients ``` Filtering options: * Account * Name * Email * External ID **Get a Recipient by ID** ```text theme={null} theme={null} GET /v2/recipients/{recipientId} ``` **Create Recipient** Add a new Recipient to your Company's contact list: ```text theme={null} theme={null} POST /v2/recipients ``` Once created, a Recipient can be referenced by ID across multiple gift sends without re-submitting their details. **Update Recipient** ```text theme={null} theme={null} PATCH /v2/recipients/{recipientId} ``` **Delete Recipient by ID** ```text theme={null} theme={null} DELETE /v2/recipients/{recipientId} ``` Deleting a Recipient is permanent and cannot be undone. Existing gifts sent to this Recipient will not be affected, but the Recipient record will no longer be retrievable. # Update recipient Source: https://docs.snappy.com/modules/api/v2/recipients/update-recipient patch /v2/recipients/{recipientId} Use this endpoint to update the details of a specific Recipient, including their contact information, employee details, and custom fields. ###### Required fields: - `recipientId` - the Recipient identifier, passed as a path parameter ###### Optional fields: (in request body - all updatable) - `firstName`, `lastName` - names - `email`, `emailOverride` - email and notification override - `mobilePhone` - phone - `country` - ISO 3166-1 alpha-2 country code - `birthday` - ISO 8601 date - `accounts` - array of Account IDs (replaces existing list) - `type` - Recipient type - `employee` - `workingSince`, `department` - `recipientCustomFields` - array of custom field objects **Optional parameters** - `companyId` query parameter - Company ID (when not inferable from the calling key) - `Request-Source` header - source of the request ###### Behavior Notes: - Returns the full updated Recipient on success. - Returns `404` if the Recipient doesn't exist or isn't accessible. - Returns `422` for business-rule violations (e.g. invalid Account IDs). - The `accounts` field is a **replace, not merge** - supplying it overwrites the existing list. To add or remove Accounts, fetch the current list first and submit the modified array. #### Permissions - Requires: `recipients:update` # Get variant by ID Source: https://docs.snappy.com/modules/api/v2/variants/get-variant-by-id get /v2/variants/{variantId} Use this endpoint to retrieve a specific Variant by its ID. Returns the parent Product object with the requested Variant in its `variants` array. ###### Required fields: - `variantId` - the Variant identifier, passed as a path parameter ###### Optional parameters: - `companyId` query parameter - Company ID (when not inferable from the calling key) - `country` query parameter - ISO 3166-1 alpha-2 country code - `fields` query parameter - comma-separated field projection. Valid values: `mediaItems`, `tags`, `brand`, `optionAttributes`, `notices`, `pricing`, `supportedCountries`, `full` - `Request-Source` header - source of the request ###### Please note: - This endpoint returns the **parent Product** with the requested Variant inside its `variants` array - not the Variant in isolation. Locate the specific variant inside the returned `variants` list using the supplied `variantId`. - Returns `422` when the Variant doesn't exist or isn't accessible to the calling key. #### Permissions - Requires: `products:read` # Create account Source: https://docs.snappy.com/modules/api/v3/accounts/create-account post /v3/accounts Use this endpoint to create a new Account under your Company. Use this when you need to programmatically set up a new team, department, or budget owner with its own campaigns and billing method. ###### Required fields: - `name` - display name of the Account ###### Optional fields: - `billingMethod` - the initial billing method to attach to the Account - `billingMethod.type` - currently must be `INV` (invoice). Other billing types (Prepay, PO, CC) must be set up via the Snappy Dashboard. - `billingMethod.spendingLimit.amount` - billing amount - `billingMethod.name` - display name of the billing method ###### Optional parameters: - `Snappy-Account-Id` header - optional account scoping - `Snappy-Company-Id` header - optional company scoping ###### Behavior Notes: - Returns `201` with the new Account details wrapped in `{ data: { id, name } }` on success. - Returns `409` when an Account with the same name already exists in the Company. - Returns `422` for business-rule violations (e.g. invalid billing configuration). - Currently only invoice (`INV`) billing methods can be created via the API. To set up Prepay, PO, or Credit Card billing, create the Account first and then configure billing via the Snappy Dashboard. #### Permissions - Requires: `accounts:create` # Get account by ID Source: https://docs.snappy.com/modules/api/v3/accounts/get-account-by-id get /v3/accounts/{accountId} Use this endpoint to retrieve the details of a specific Account by its identifier. Use this when you have an Account ID and need to confirm its name, Company membership, or timestamps. ###### Required fields: - `accountId` - the Account identifier, passed as a path parameter (alphanumeric) ###### Optional parameters: - `Snappy-Company-Id` header - optional company scoping ###### Please note: - Returns `404` if no Account exists for the supplied `accountId`, or if it's not accessible to the calling API key. #### Permissions - Requires: `accounts:read` # Get accounts Source: https://docs.snappy.com/modules/api/v3/accounts/get-accounts get /v3/accounts Use this endpoint to retrieve a list of Accounts available to your API key. Use this when you need to discover which Accounts you have access to, find a specific Account by name, or load Account options into your UI. ###### Filtering options: - `filter[name]` - case-insensitive substring match on Account name - `fields` - comma-separated list of fields to return. Valid values: `id`, `name` - `Snappy-Account-Id` header - optional account scoping - `Snappy-Company-Id` header - optional company scoping - `Request-Source` header - source of the request (`api_native`, `api_zapier`, `api_salesforce`, `api_ftp`, `api_make`). ###### Pagination: - `page[number]` - 1-indexed page number (default `1`). - `page[size]` - Accounts per page (max `1000`, default `100`). #### Permissions - Requires: `accounts:read` # Accounts API (V3): Organize Gifting by Team, Department, or Budget Source: https://docs.snappy.com/modules/api/v3/accounts/overview Manage Accounts and sub-accounts via the V3 API. JSON:API filtering, page-number pagination, and the standardized V3 error envelope. 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. Want to understand how **Accounts** fit into the bigger picture? Check out the [Core Concepts & Data Models](/pages/snappy-core-concepts-and-data-models) page. Looking for the V2 endpoints? See [Accounts (V2)](/modules/api/v2/accounts/overview). Both V2 and V3 are supported in parallel. *** ## The Account Object | Field | Type | Description | | :---------- | :---------------- | :-------------------------------------------------- | | `id` | string | Unique identifier for the Account (e.g. `a12bcd34`) | | `name` | string | Display name of the Account | | `companyId` | string | The ID of the Company this Account belongs to | | `createdAt` | string (ISO 8601) | Timestamp when the Account was created | | `updatedAt` | string (ISO 8601) | Timestamp of the most recent update | Billing Methods belong to an Account but are managed via the dedicated [Billing Methods API](/modules/api/v3/billing-methods/overview). Use `GET /v3/billing-methods` with the `Snappy-Account-Id` header to retrieve the billing methods available on a given Account. *** ## Key Concepts & Business Rules #### Accounts organize your gifting activity All Campaigns and Gifts are created within an Account. If your organization has multiple teams or departments sending gifts independently, each should operate through its own Account with its own budget and Billing Method. #### One default Billing Method per Account Each Account has one Billing Method set as the default. This default is applied automatically to any Campaign created via the API. Campaigns created through the Dashboard allow explicit Billing Method selection at the time of creation. Embedded Marketplace (`POST /v3/orders`) references the Billing Method explicitly via the `billingMethodId` field. #### Initial billing setup via Create Account The `POST /v3/accounts` endpoint accepts an initial Billing Method (currently invoice-only - `type: INV`) at Account creation time. To configure other Billing Method types (Prepay, PO, Credit Card), create the Account first and then set up the Billing Methods via the Snappy Dashboard. #### Account scoping in other APIs Most V3 APIs (Billing Methods, Orders, product retrieval) accept a `Snappy-Account-Id` header to scope queries to a specific Account when your API key has access to multiple Accounts. *** ## How to Work with Accounts (V3) **List Accounts** ```text theme={null} theme={null} GET /v3/accounts ``` Returns a paginated list of Accounts available to your API key. Filter by `filter[name]`, paginate with `page[number]` / `page[size]`, and control returned fields with the `fields` parameter. **Get a single Account** ```text theme={null} theme={null} GET /v3/accounts/{accountId} ``` Returns the details of a specific Account by its ID. **Create an Account** ```text theme={null} theme={null} POST /v3/accounts ``` Creates a new Account under your Company with an initial Billing Method (currently invoice-only via the API). # Create API key Source: https://docs.snappy.com/modules/api/v3/api-keys/create-api-key post /v3/authentication/api-keys Use this endpoint to create a new API key from an authenticated dashboard session. Use this when an owner or tools admin needs to mint a key - for a new integration, for key rotation, or to scope access to specific Accounts. ###### Required fields - `name` - display name of the API key (must be unique within the Company) ###### Optional fields - `expirationInDays` - number of days until the key expires. Accepted values: `30`, `60`, `90`, `180`, `365`. Default: `90`. - `enforceMtls` - when `true`, requests with this key must use mTLS. Default: `false`. - `permissions` - array of permission scopes the new key should have. See the [permission reference](/pages/authentication-and-security#available-scopes). - `accountsAccess` - `{ scope: "all-accounts" | "specific-accounts", ids: [] }` to scope the key to specific Accounts. ###### Optional headers - `Snappy-Account-Id` - optional account scoping - `Snappy-Company-Id` - required for multi-company users to select the target Company ###### Behavior Notes - **The API key secret is visible only in this response.** Store it securely - it cannot be retrieved later. - **Max 100 active API keys per Company.** Delete an existing key before creating the 101st. #### Permissions Authenticated via `Authorization: Bearer `. Only Company owners and tools admins have access. # Delete API key Source: https://docs.snappy.com/modules/api/v3/api-keys/delete-api-key delete /v3/authentication/api-keys/{apiKeyId} Use this endpoint to permanently delete an existing API key by its ID. Use this when an owner or tools admin needs to revoke a key - for rotation, compromise response, or cleanup. ###### Required fields - `apiKeyId` - the API key identifier, passed as a path parameter (24-character hex) ###### Optional headers - `Snappy-Account-Id` - optional account scoping - `Snappy-Company-Id` - required for multi-company users to select the target Company ###### Behavior Notes - **Deletion is immediate and permanent.** Once deleted, any application using this key will receive `401 Unauthorized` on its next request. Update your applications to use a replacement key *before* deleting the old one. - Returns `204 No Content` on success - no response body. #### Permissions Authenticated via `Authorization: Bearer `. Only Company owners and tools admins have access. # Get API keys Source: https://docs.snappy.com/modules/api/v3/api-keys/get-api-keys get /v3/authentication/api-keys Use this endpoint to retrieve a paginated list of API keys for the calling Company. Use this when building a key-management view inside a dashboard or admin tool. ###### Pagination - `page[number]` - 1-indexed page number (default `1`) - `page[size]` - keys per page (max `100`, default `100`) ###### Optional headers - `Snappy-Account-Id` - optional account scoping - `Snappy-Company-Id` - required for multi-company users to select the target Company ###### Please note - The actual API key secret is **never returned by this endpoint**. The secret is shown only once - in the response to `POST /v3/authentication/api-keys` - and is not retrievable afterward. #### Permissions Authenticated via `Authorization: Bearer `. Only Company owners and tools admins have access. # API Keys (V3): Programmatic Key Management Source: https://docs.snappy.com/modules/api/v3/api-keys/overview Manage Snappy API keys programmatically in V3 - list, create, and revoke keys by authenticating with an existing API key. The V3 API Keys endpoints let you manage keys programmatically - listing, creating, and revoking keys - by authenticating with an existing API key (`X-Api-Key`). You can also create and manage keys in the Snappy dashboard on the **Sharing & Access** page. For the full authentication concept guide - including how scopes work, how to use mTLS, and best practices for key rotation - see [Snappy API Authentication: API Keys, Scopes & mTLS](/pages/authentication-and-security). The [V2 API Keys](/modules/api/v2/api-keys/overview) endpoints offer the same list, create, and delete operations using V2 conventions. *** ## The API Key Object The V3 response shape **never includes the secret value**, even on creation responses. Treat the secret as opaque once issued. | Field | Type | Description | | :--------------- | :---------------- | :-------------------------------------------------------------------------------- | | `id` | string | Unique identifier of the API key | | `name` | string | Display name (unique within the Company) | | `companyId` | string | The ID of the Company the key belongs to | | `createdAt` | string (ISO 8601) | When the key was created | | `expirationDate` | string (ISO 8601) | When the key expires. `null` if the key has no expiration. | | `enforceMtls` | boolean | When `true`, requests with this key must use mTLS | | `permissions` | array | Permission scopes granted to this key (e.g. `gifts:create`, `orders:read:masked`) | | `accountsAccess` | object | Account scope: `{ scope: "all-accounts" \| "specific-accounts", ids: [] }` | *** ## Key Concepts #### Maximum 100 active keys per Company Companies can have up to 100 active API keys at any time. #### Page-number pagination The List endpoint uses **page-number pagination** (`page[number]`, `page[size]`), with the standard V3 `links` envelope (`first`, `next`, `prev`). #### Standard V3 error envelope Errors follow the standard V3 shape: `{ message, errorCode, errors[] }` with structured error codes (e.g. `403_PBLC_001`) and dot-separated paths to field-level errors. *** ## How to Work with API Keys (V3) **List API keys** ```text theme={null} theme={null} GET /v3/authentication/api-keys ``` Returns a paginated list of the active API keys for your Company. Filter by Account access scope, paginate with `page[number]` / `page[size]`. **Create an API key** ```text theme={null} theme={null} POST /v3/authentication/api-keys ``` Creates a new API key. The secret value is returned in this response only. **Delete an API key** ```text theme={null} theme={null} DELETE /v3/authentication/api-keys/{apiKeyId} ``` Permanently deletes the specified key. Returns `204 No Content` on success. # Get base product by ID Source: https://docs.snappy.com/modules/api/v3/base-products/get-base-product-by-id get /v3/base-products/{baseProductId} Returns a single base product by id. `fields` accepts an optional free-form list of base-product field expansions. # Get base products Source: https://docs.snappy.com/modules/api/v3/base-products/get-base-products get /v3/base-products Returns a paginated list of base products (product templates) used for swag experiences. Page-number pagination (`page[number]`, `page[size]`). The brand filter for base products is named `brandIds` (the JSON:API `filter[brandId]` family is not yet applied to base products). # Get base variants by base product ID Source: https://docs.snappy.com/modules/api/v3/base-products/get-base-variants-by-base-product-id get /v3/base-products/{baseProductId}/variants Returns a paginated list of base variants for a base product. Page-number pagination (`page[number]`, `page[size]`). # Swag Products: Branded Merchandise Templates Source: https://docs.snappy.com/modules/api/v3/base-products/overview Browse Snappy's swag catalog - base products and base variants for branded merchandise. Schema, relationships, and V3 endpoints for building a swag store. **Swag** in Snappy refers to branded merchandise - items like t-shirts, mugs, hats, and notebooks that customers apply their own logo or design to. Swag is the second of Snappy's two product catalogs (the first being [Products & Variants](/modules/api/v2/products/overview), the curated marketplace catalog). A **Base Product** is a swag template - the unbranded item from which customized swag is derived (e.g. "standard cotton t-shirt"). Base Products are the swag-catalog counterpart to standard Products. 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 counterpart to standard Variants - and as with the marketplace catalog, the variant is the orderable unit. Looking for the end-to-end integration walkthrough for building a swag store? See the [Swag Store API](/pages/swag-store-api) guide. The Base Product and Base Variant schemas are intentionally minimal in V3 and will be extended as the swag track matures - customization layers, design assets, and approval flows are planned. Subscribe to the [Changelog](/pages/changelog) for updates. *** ## The Base Product Object | Field | Type | Description | | :--------- | :----- | :-------------------------------------------------------------------------------------------------------------- | | `id` | string | Base product identifier (e.g. `bp_a1b2c3`) | | `title` | string | Display name of the Base Product | | `media` | array | Media items (images, video). Each item contains `type` and `src`. | | `category` | object | Category taxonomy. Contains `fullName` - the full path separated by `/` (e.g. `apparel / tops`) | | `type` | enum | Product type. One of: `physical`, `digital`, `giftCard`, `donation` | | `brand` | object | The brand associated with this Base Product (`id`, `name`, `description`). May be `null` when no brand applies. | *** ## The Base Variant Object A Base Variant represents a specific, orderable version of a Base Product. When placing a swag order you must always specify the variant ID - the Base Product id alone is not sufficient. | Field | Type | Description | | :---------------- | :----- | :------------------------------------------------------------------------------------ | | `id` | string | Unique identifier for the Base Variant (e.g. `bv_x9y8z7`) | | `baseProductId` | string | The parent Base Product identifier | | `title` | string | Display name of the Base Variant | | `selectedOptions` | object | The specific option values for this variant (e.g. `{ "size": "M", "color": "Navy" }`) | | `media` | array | Images and video specific to this variant | *** ## Key Concepts & Business Rules #### Swag vs Marketplace Snappy maintains two separate product catalogs: | Catalog | What it contains | Use case | | :------------ | :-------------------------------------------------------- | :---------------------------------------------- | | `marketplace` | Curated catalog of finished gifts from third-party brands | Send a recipient a specific branded gift | | `swag` | Templates for branded merchandise | Build a swag store with your company's branding | The two catalogs use distinct endpoints (`/v3/products/*` vs `/v3/base-products/*`) and distinct schemas. #### Base Product → Base Variant → Order The flow mirrors the marketplace flow: browse Base Products, drill into the Base Variants for the one you want, and place an order specifying the variant. Customization (logo application, design preview, approval) layers in between variant selection and order placement and will be exposed in future API releases. #### Pagination Base Product and Base Variant list endpoints use **page-number pagination** (`page[number]`, `page[size]`): | Endpoint | Max page size | Default page size | | :----------------------------------- | :------------ | :---------------- | | List base products | 100 | 40 | | Get base variants by base product ID | 500 | 100 | See [Request & Response Standards](/pages/request-response-standards) for the full V3 pagination contract. #### Brand filtering uses `brandIds`, not `filter[brandId]` Base Product endpoints use a flat `brandIds` query parameter rather than the JSON:API `filter[brandId]` family used elsewhere in V3. This is a known divergence - for all other V3 endpoints, use the `filter[]` 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.