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

# Triggered Gifting (Recipient Choice)

> 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:

<CardGroup cols={2}>
  <Card title="Trigger" icon="gift">
    Your system creates a gift via `POST /gifts` with recipient identity and campaign context.
  </Card>

  <Card title="Notify" icon="bell">
    Snappy emails the recipient a magic link to a personalized claim page (auto or manual).
  </Card>

  <Card title="Claim" icon="hand-pointer">
    Recipient chooses a gift, provides shipping details, and Snappy generates the order.
  </Card>

  <Card title="Track" icon="truck">
    Your backend consumes webhook events to follow the gift through claim and delivery.
  </Card>
</CardGroup>

***

## Before you start

<Steps>
  <Step title="Have a Campaign ready">
    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.
  </Step>

  <Step title="Confirm your Account and 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).
  </Step>

  <Step title="Get your API key with the right scopes">
    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).
  </Step>
</Steps>

<Warning>
  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.
</Warning>

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

<CodeGroup>
  ```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'
  ```
</CodeGroup>

***

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

<CodeGroup>
  ```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')
  ```
</CodeGroup>

<Tip>
  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.
</Tip>

<Note>
  Your Campaign should already have a Collection or Product assigned, along with your preferred Gift Customization settings.
</Note>

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.

<CodeGroup>
  ```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"
          }
        }
      ]
    }
  ```
</CodeGroup>

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.

<Tip>
  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.
</Tip>

<Note>
  V2 uses `firstname` / `lastname` (lowercase, merged) in this endpoint. V3 uses `firstName` / `lastName` (camelCase). Match the casing per version.
</Note>

***

## Step 3 - Recipient claims the Gift

<Note>
  This step happens entirely within Snappy's recipient experience - no API calls required from your side.
</Note>

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.

<CodeGroup>
  ```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)
  ```
</CodeGroup>

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

<Tip>
  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.
</Tip>

***

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

<Card title="API Reference" icon="book" href="/gifts-v2-overview">
  Full endpoint specs for the V2 Gifts and Campaigns endpoints used in this guide.
</Card>
