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

# Sell gift subscriptions from your own storefront

> Sell a gift subscription from your own product page, landing page or headless storefront: set up a gift plan, send the recipient's details as line item properties and pass the checkout rule.

A gift subscription is a prepaid, one-time plan that one customer buys for someone else. The buyer stays the Shopify customer, pays for every delivery up front and enters the recipient's shipping address at checkout, because that's where every delivery goes. After checkout, Appstle emails the recipient an invitation to a gift page, where they can follow their deliveries and thank the buyer. Appstle's own widget collects the recipient's details on the product page. This guide shows how to do the same from your own page: a landing page on a Shopify theme, or a headless storefront.

> [!NOTE]
>
> The cart calls and the Liquid in this guide are Shopify's, so they need no Appstle API key. The Appstle calls (finding or creating the gift plan, and checking the subscription after checkout) use the **Admin API** from your server, with an API key in the `X-API-Key` header (see [Authentication](https://developers.appstle.com/subscription/authentication.md)). Creating a plan needs a **Read & Write** key. Never put an API key in browser code.

## What you'll build

* A gift plan attached to a product.
* A cart add that carries the recipient's details on the gift line.
* Checks in your own page that match the checkout rule, and an optional send date for the invitation.

## Before you start

* **A gift plan on the product.** In the Appstle Subscriptions app, open **Subscription Plans**, create a plan, choose **Prepaid, one time** and turn on **Sell as a gift**. You can also [create it from your server](#create-the-gift-plan-from-your-server). A gift plan is always prepaid, one time, with one billing cycle.
* **The Subscription Validations checkout rule, active.** In Shopify admin, go to **Settings** → **Checkout** → **Checkout rules**, choose **Add rule** → **Subscription Validations**, make sure it's **Active** and save. Until you do, checkout lets a gift cart through with no recipient. The order is placed and tagged `Appstle gift`, but no invitation goes out until the merchant adds a recipient with **Edit recipient** in the subscription's gift details.
* **A test store.** Run the whole flow with a test payment before you publish the page.

## How a gift is stored

A gift is one ordinary line: the product with the gift plan's selling plan. The recipient's details travel on that line as line item properties. Shopify's cart API calls them `properties`, and its Storefront GraphQL API calls them `attributes`. Shopify treats properties whose names start with an underscore as private: checkout doesn't show them, and most themes hide them in the cart. After checkout, they stay on the subscription line.

| Property | Required | Rule |
| - | - | - |
| `_appstle-gift-recipient-name` | Yes | Trimmed, at most 100 characters |
| `_appstle-gift-recipient-email` | Yes | A valid email address, trimmed, at most 254 characters, different from the buyer's checkout email |
| `_appstle-gift-sender` | No | At most 100 characters |
| `_appstle-gift-message` | No | Plain text, at most 1,000 characters |
| `_appstle-gift-send-date` | No | One date, `YYYY-MM-DD`, in the shop's time zone. See [Schedule the invitation](#schedule-the-invitation). |

A gift checkout holds one gift line, quantity **1**, one recipient and nothing else. Mixed carts, several gifts and several recipients aren't supported.

## Step 1: Find the gift plan's selling plan ID

**Sell as a gift** is a setting on one plan, and a plan group can hold gift and regular options side by side. Don't pick the plan by its group's name or its position. Find its ID once, and keep it in your page's configuration, such as a theme or section setting.

From your server, list the plan groups with [Get all subscription groups](https://developers.appstle.com/subscription-admin-api/subscription-plans/get-all-subscription-groups.md). In each group's `subscriptionPlans`, gift plans have `gift` set to `true`, and `id` is the selling plan ID:

```bash theme={null}
curl 'https://subscription-admin.appstle.com/api/external/v2/subscription-groups' \
  -H 'X-API-Key: YOUR_API_KEY'
```

The response lists every group. This one holds a regular plan and a gift plan, with only the fields that matter here:

```json theme={null}
[
  {
    "id": 8031234567,
    "subscriptionPlans": [
      {
        "id": "gid://shopify/SellingPlan/690123456700",
        "frequencyName": "Monthly coffee",
        "planType": "PAY_AS_YOU_GO",
        "maxCycles": null,
        "gift": null,
        "giftSendMaxDays": null
      },
      {
        "id": "gid://shopify/SellingPlan/690123456789",
        "frequencyName": "3-month gift",
        "planType": "PREPAID",
        "maxCycles": 1,
        "gift": true,
        "giftSendMaxDays": 30
      }
    ]
  }
]
```

Use the number at the end of `id` as `selling_plan`. For `gid://shopify/SellingPlan/690123456789`, that's `690123456789`.

On the page, check that the product still carries the gift plan before you show the gift form:

```javascript theme={null}
const GIFT_SELLING_PLAN_ID = 690123456789; // from your page's configuration
const product = await (await fetch('/products/coffee-gift-set.js')).json();
const giftPlanAvailable = product.selling_plan_groups.some((group) =>
  group.selling_plans.some((plan) => plan.id === GIFT_SELLING_PLAN_ID),
);
```

In Liquid, the same plans are in `product.selling_plan_groups`.

## Step 2: Add the gift to the cart

### On a Shopify theme

Add the variant with the gift plan and the recipient's details as `properties`. This is the same route as [Adding a subscription to cart](https://developers.appstle.com/subscription/build-your-own-frontend.md#adding-a-subscription-to-cart), with the extra properties.

```javascript theme={null}
const response = await fetch('/cart/add.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [
      {
        id: 44123456789012,
        quantity: 1,
        selling_plan: 690123456789,
        properties: {
          '_appstle-gift-recipient-name': 'Taylor Reed',
          '_appstle-gift-recipient-email': 'taylor.reed@example.com',
          '_appstle-gift-sender': 'Jordan',
          '_appstle-gift-message': 'Happy birthday!',
        },
      },
    ],
  }),
});
```

Shopify answers `200` with an `items` array, and the gift line's `properties` holds the four values. It answers `422` with `Cannot apply selling plan to variant` when the gift plan isn't attached to the variant.

> [!WARNING]
>
> The checkout rule refuses a gift cart that holds anything else. Read `/cart.js` before you add the gift. If the cart already has items, ask the shopper before you empty it with `POST /cart/clear.js`. If the gift is already in the cart, don't add it again: a second add leaves two gifts in the cart, which the rule also refuses.

### Using a form

A product page can post the same values with a form to `/cart/add`:

```html theme={null}
<form method="post" action="/cart/add">
  <input type="hidden" name="id" value="44123456789012">
  <input type="hidden" name="selling_plan" value="690123456789">
  <input type="hidden" name="quantity" value="1">

  <label>Recipient's name
    <input type="text" name="properties[_appstle-gift-recipient-name]" required maxlength="100">
  </label>
  <label>Recipient's email
    <input type="email" name="properties[_appstle-gift-recipient-email]" required maxlength="254">
  </label>
  <label>Your name
    <input type="text" name="properties[_appstle-gift-sender]" maxlength="100">
  </label>
  <label>Message
    <textarea name="properties[_appstle-gift-message]" maxlength="1000"></textarea>
  </label>
  <label>Send date (optional)
    <input type="date" name="properties[_appstle-gift-send-date]" min="2026-11-02" max="2026-12-02">
  </label>

  <button type="submit">Add gift to cart</button>
</form>
```

Set the date field's `min` and `max` from `giftSendDateRange` (see [Schedule the invitation](#schedule-the-invitation)). The `required` and `maxlength` attributes match the table above. A browser's email check is looser than the rule's, and only checkout knows the buyer's email, so the rule can still refuse an address the form accepted. Leave the date field empty to send the invitation without a date: Appstle treats an empty `_appstle-gift-send-date` as no date.

### On a headless storefront

Send the same keys as line `attributes` (`{ key, value }` pairs) on the gift line of Shopify's [cart creation mutation](https://shopify.dev/docs/api/storefront/latest/mutations/cartCreate), together with the gift plan's `sellingPlanId` and a `quantity` of `1`. Then redirect the shopper to the returned `checkoutUrl`.

These are **line** attributes, not cart attributes or the order note. Keep them with the selling plan whenever you replace a variant or update the line. Handle `userErrors` before you redirect.

## Step 3: Pass the checkout rule

The rule runs when the buyer presses **Pay now**. Earlier checkout steps let the shopper repair the cart, so a half-finished gift cart isn't blocked while they're still filling it in. At that point the rule returns every message that applies. Each message is the default text, and a store can replace it through its widget labels.

| What fails | Message |
| - | - |
| The cart has another line, or the gift's quantity isn't 1 | `Gift subscriptions check out on their own, with quantity 1.` |
| The buyer's email is missing or invalid | `Enter your own email address.` |
| The recipient's name is missing or too long, the recipient's email is invalid, or the sender or message is too long | `Gift details are missing or invalid. Go back to the product page to check them.` |
| The recipient's email is the same as the buyer's, ignoring case | `Use your own email here; the recipient’s email must be different.` |

The same rule also applies the store's other subscription limits to the gift line. If the store limits subscriptions per customer, a buyer who already has a subscription can see `Buying multiple subscriptions is not allowed.`, and a quantity above 1 can also show `Subscription products cannot have a quantity greater than 1.`

The rule doesn't check the send date. Check it in your page (see the next section), and check the other fields too, so the shopper finds out before checkout.

Don't enable accelerated checkout buttons on a gift page unless you've confirmed they keep the selling plan and every property. Treat names, emails and messages as customer information: keep them out of analytics events and logs.

## Schedule the invitation

Without `_appstle-gift-send-date`, or with an empty one, Appstle sends the invitation about five minutes after the subscription is created. To schedule it, add one `_appstle-gift-send-date` property in the shop's time zone, formatted exactly as `YYYY-MM-DD`. Appstle compares it with the purchase date in that time zone:

* **Today or an earlier date:** the invitation goes out about five minutes after the subscription is created, as if there were no date.
* **A later date, within the plan's maximum:** the invitation goes out shortly after 09:00 in the shop's time zone on that date. The maximum is the plan's **Maximum days ahead to schedule a gift invitation** (`giftSendMaxDays`): a whole number from 1 to 365, and 90 when it's empty.
* **A badly formatted date, a date past the maximum, or more than one send-date property:** the invitation waits, and the merchant can send it with **Resend invitation** in the subscription's gift details.

The date schedules the invitation email only. It doesn't hold shipments: deliveries continue on the plan's own schedule. The date can't be changed after purchase.

### Read the time zone and the maximum

Appstle publishes both values in the shop metafield `appstle_subscription.setting`.

* **On a Shopify theme**, read it in Liquid as `shop.metafields.appstle_subscription.setting.value`. No token is needed.
* **On a headless storefront**, read it with Shopify's Storefront GraphQL API and your own Storefront access token, as `shop { metafield(namespace: "appstle_subscription", key: "setting") { value } }`. Shopify serves metafields only to requests with a token.

The metafield's JSON holds two keys you need. On a headless storefront it arrives as a JSON string; on a theme, Liquid has already parsed it:

* `shopIanaTimeZone`: the shop's IANA time zone, such as `America/New_York`. It can arrive escaped one extra time (`America\/New_York`), and the function below decodes it either way. If it's empty, don't offer a send date: Appstle can't evaluate one, and the invitation would wait.
* `sellingPlansJson`: a JSON string that lists the shop's plans. Find your gift plan by its `id` and read its `giftSendMaxDays`. When the field is missing, the maximum is 90.

On a theme, this snippet prints only those two keys into the page as one JSON string. Put it in a section or a snippet that renders on the gift page:

```liquid theme={null}
{%- assign setting = shop.metafields.appstle_subscription.setting.value -%}
<script>
  window.giftSettingValue = JSON.stringify({
    shopIanaTimeZone: {{ setting.shopIanaTimeZone | json }},
    sellingPlansJson: {{ setting.sellingPlansJson | json }}
  });
</script>
```

The value covers every plan in the shop and can be large, so print it only on the gift page, and on a headless storefront cache it instead of fetching it on each page view. Pass the string (`window.giftSettingValue` on a theme, the metafield's `value` on a headless storefront) and your gift plan's ID to this function. It accepts the plan's numeric ID or its `gid://shopify/SellingPlan/...` form, and returns the time zone and the earliest and latest dates to offer, or `null` when scheduling isn't available:

```javascript theme={null}
function giftSendDateRange(settingValue, giftSellingPlanId) {
  const setting = JSON.parse(settingValue);
  const timeZone = JSON.parse(`"${setting.shopIanaTimeZone || ''}"`);
  if (!timeZone) {
    return null;
  }
  const plans = JSON.parse(setting.sellingPlansJson || '[]');
  const numericId = (id) => String(id).split('/').pop();
  const plan = plans.find((entry) => numericId(entry.id) === numericId(giftSellingPlanId));
  const maxDays = Number.isInteger(plan?.giftSendMaxDays) ? plan.giftSendMaxDays : 90;
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(new Date());
  const part = (type) => parts.find((item) => item.type === type).value;
  const earliest = `${part('year')}-${part('month')}-${part('day')}`;
  const latest = new Date(`${earliest}T00:00:00Z`);
  latest.setUTCDate(latest.getUTCDate() + maxDays);
  return { timeZone, earliest, latest: latest.toISOString().slice(0, 10) };
}
```

On a headless storefront, a `null` metafield means the store's **Subscription App Setting** metafield definition doesn't give Shopify's Storefront GraphQL API access. The merchant can turn that access on in Shopify admin: go to **Settings** → **Metafields and metaobjects** → **Shop**, open **Subscription App Setting** and turn on **Storefront API access** under **Options**. Until then, leave the send date out so invitations go out without a schedule, or keep the time zone and the maximum in your storefront's own configuration, in step with the merchant's settings.

## Step 4: Check the gift after checkout

Appstle tags the gift's order `Appstle gift`, and the `_appstle-gift-*` properties stay on the subscription's line. To check them from your server, read the subscription with [Retrieve subscription contracts](https://developers.appstle.com/subscription-admin-api/subscription-contracts/retrieve-subscription-contracts.md) and its `subscriptionContractId` filter:

```bash theme={null}
curl 'https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details?subscriptionContractId=181234567890' \
  -H 'X-API-Key: YOUR_API_KEY'
```

The response is a list with one entry. The example leaves out most fields, including `contractDetailsJSON`, which the next paragraph covers:

```json theme={null}
[
  {
    "subscriptionContractId": 181234567890,
    "status": "paused",
    "orderName": "#1001",
    "maxCycles": 1,
    "billingPolicyInterval": "month",
    "billingPolicyIntervalCount": 3
  }
]
```

`contractDetailsJSON` is a JSON string. Parsed, its single entry is the gift line, with the `_appstle-gift-*` properties under `customAttributes`:

```json theme={null}
{
  "productId": "gid://shopify/Product/8123456789012",
  "title": "Decaf Coffee",
  "quantity": 1,
  "variantId": "gid://shopify/ProductVariant/44123456789012",
  "sellingPlanId": "gid://shopify/SellingPlan/690123456789",
  "sellingPlanName": "3-month gift",
  "customAttributes": [
    { "key": "_appstle-gift-recipient-name", "value": "Taylor Reed" },
    { "key": "_appstle-gift-recipient-email", "value": "taylor.reed@example.com" },
    { "key": "_appstle-gift-sender", "value": "Jordan" },
    { "key": "_appstle-gift-message", "value": "Happy birthday!" }
  ]
}
```

On your server, tell a gift apart by the order's `Appstle gift` tag, which Appstle adds only once it has set up the gift, or by a `sellingPlanId` that is your gift plan's ID. The gift keys alone don't make a line a gift: Appstle ignores them on a plan that isn't a gift plan.

A gift has one billing cycle. If the store has **Auto-pause subscriptions after maximum orders reached** turned on, Appstle pauses the subscription right after it's created, and the API lists it as `paused`. Otherwise it stays `active` with nothing scheduled. Either is expected: the buyer has paid for the whole gift.

## Create the gift plan from your server

You can create a gift plan with the Admin API instead of the app. Call [Create a new subscription group](https://developers.appstle.com/subscription-admin-api/subscription-plans/create-a-new-subscription-group-selling-plan-group.md) with `gift` set to `true` on a prepaid plan that has `maxCycles` of `1`. This example sells three months of coffee for one payment:

```bash theme={null}
curl -X POST 'https://subscription-admin.appstle.com/api/external/v2/subscription-groups' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "groupName": "Gift plans",
    "productIds": "[{\"id\":8123456789012}]",
    "subscriptionPlans": [
      {
        "frequencyName": "3-month gift",
        "frequencyDescription": "Three months of coffee, paid upfront",
        "frequencyCount": 1,
        "frequencyInterval": "MONTH",
        "billingFrequencyCount": 3,
        "billingFrequencyInterval": "MONTH",
        "planType": "PREPAID",
        "maxCycles": 1,
        "gift": true,
        "giftSendMaxDays": 30,
        "discountEnabled": false
      }
    ]
  }'
```

The response is `201` with the group and its new `id`. Each plan's `id` is still `null` in that response. Read the selling plan ID from [Get subscription group by ID](https://developers.appstle.com/subscription-admin-api/subscription-plans/get-subscription-group-by-id.md) (`subscriptionPlans[].id`), or from the product's `selling_plan_groups`.

The call returns `400` for the first rule a gift plan breaks, checked in this order:

| Rule | `errorKey` | Message |
| - | - | - |
| A new gift option has the same name as another option in the group, ignoring case | `giftFrequencyNameNotUnique` | `UserGeneratedError:Each new gift option needs its own name.` |
| `appstleCycles` isn't empty (the plan has rewards) | `giftRewardsNotSupported` | `UserGeneratedError:Remove rewards before selling this plan as a gift.` |
| `minQuantity` is above 1 | `invalidGiftMinimumQuantity` | `UserGeneratedError:Gifts must be purchased one at a time. Leave minimum quantity empty or set it to 1.` |
| `giftSendMaxDays` is below 1 or above 365 | `invalidGiftSendMaxDays` | `UserGeneratedError:Gift invitations can be scheduled between 1 and 365 days ahead.` |
| `planType` isn't `PREPAID`, or `maxCycles` isn't `1` | `invalidGiftPlan` | `UserGeneratedError:Gift plans must use Prepaid One-Time with one billing cycle.` |

The body is `application/problem+json`. This is the response to a pay-as-you-go plan sent with `gift` set to `true`:

```json theme={null}
{
  "entityName": "subscriptionGroup",
  "errorKey": "invalidGiftPlan",
  "type": "https://appstle.com/problem/problem-with-message",
  "title": "UserGeneratedError:Gift plans must use Prepaid One-Time with one billing cycle.",
  "status": 400,
  "message": "UserGeneratedError:Gift plans must use Prepaid One-Time with one billing cycle.",
  "params": "subscriptionGroup"
}
```

Match on `errorKey`, not on the message. An update keeps each saved plan's gift setting as it was saved, whatever the request says, so create a new plan to change it.

## Verify the complete flow

1. On a test store, complete a checkout with a test payment, using a buyer email that isn't the recipient's.
2. Check the order and the subscription as in [Step 4](#step-4-check-the-gift-after-checkout).
3. Confirm the buyer gets the ordinary purchase confirmation, and the recipient gets the invitation about five minutes later. The invitation link works for two hours, and the recipient can ask for a fresh one by email.
4. Test the rule's failure cases: a missing recipient, an email that matches the buyer's, a quantity above 1 and an ordinary item in the same cart. Checkout should refuse each one.
5. For a scheduled invitation, check that it doesn't arrive before 09:00 shop time on the chosen date.

The merchant manages the gift, and resends the invitation, from the subscription's gift details in the Appstle Subscriptions app.

## Next steps

> [card] [Build your own frontend](https://developers.appstle.com/subscription/build-your-own-frontend.md)
>
> Selling plans, the cart and the customer portal from your own UI.

> [card] [Fixed-price Build-a-Box](https://developers.appstle.com/subscription/fixed-price-box-custom-ui.md)
>
> Another cart-properties flow, with a box the customer fills.

> [card] [Webhooks](https://developers.appstle.com/subscription/webhooks.md)
>
> React to new subscriptions on your server.

> [card] [Admin API integration guide](https://developers.appstle.com/subscription/integration-guide.md)
>
> Server-side calls for subscriptions and contracts.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.