---
title: "Gifts"
description: "Buy a credit pack for someone else, by email. The purchaser pays on their own billing account and gets the receipt; the recipient signs in and claims the credits at no cost."
canonical_url: "https://nuxt-backend.dev/docs/billing/credits/gifts"
---
# Gifts

> Buy a credit pack for someone else, by email. The purchaser pays on their own billing account and gets the receipt; the recipient signs in and claims the credits at no cost.

A signed-in user can buy a credit pack **for someone else**, identified only by email. The purchaser pays on their own billing account and gets the provider's receipt and invoice. The recipient signs in and claims the pack into their own account at no cost, and the provider grants them the credits. Gift records live in the `backend` component's `billingGifts` table, so there is nothing to add to your schema.

Gifts are credit packs only, in service units, never money: no plan, no balance and no gift card.

## How a gift works

1. **Purchase.** The purchaser buys the pack's **gift product** on their own billing account: their personal workspace (or the active workspace when they own it), or their account with `billTo: 'user'`. The gift product is a private twin of the pack that `billing sync` creates beside it, with the same prices and none of its benefits, so the purchase grants the purchaser nothing. Discount codes are off. The checkout carries nothing about the recipient: no customer, name, address or email of theirs.
2. **Fulfilment.** When the provider says the order is paid, the package checks it is the gift's own: the gift product, on the purchaser's account, charged above zero with no discount. It then mints the gift's claim discount (code-free, 100%, once, a single redemption, on the pack only, named `gift-<id>`) and emails the recipient.
3. **Claim.** The recipient signs in with the address, which proves it by code, and claims the gift: a claim, and the list of received gifts, need the session's email to be verified. The server opens a checkout of the pack on the recipient's own account with the discount locked on, and confirms it itself. The $0 order makes the provider grant the pack's credits to the recipient, with the pack's rollover. A pack that asks for checkout fields opens the provider's $0 checkout to finish instead. The gift counts as received once an order that redeemed its discount is paid, on whichever of the recipient's accounts it landed: only the server puts that discount on a checkout.
4. **Refund.** Any refund of the purchase before the claim, a partial one too, ends the discount and deletes it: the gift is `refunded` and can't be claimed. A gift is one pack, so a refunded share would otherwise still deliver all of it. A refund the provider reports before the order's `order.paid` voids the pending gift, so it is never delivered. A gift whose discount was already redeemed is claimed, or about to be: the provider can't take back what a $0 order granted, so the gift is left to its claim and the deployment logs `billing.gift_refunded_after_claim`, as `refundOrder` does before it refunds a claimed one. A claim paid after the gift was voided logs `billing.gift_claimed_after_refund`.

## Sending a gift

```vue
<script setup lang="ts">
const { gift } = useCredits()

async function sendGift() {
  // The pack's key in billing.catalog.ts
  await gift('credits500', {
    recipientEmail: 'friend@example.com',
    message: 'Happy birthday!',
  })
}
</script>
```

`useCredits().gift(pack, { recipientEmail, message?, metadata?, successUrl?, redirect? })` and `useBilling().gift(...)` both call the `giftCheckout` action and open the purchaser's checkout.

The server checks, in this order:

- **The recipient**: a valid address, never the purchaser's own (`forbidden`, reason `gift_refused`).
- **The message**: plain text of at most 500 characters with no links (`invalid_input`, field `message`). It is shown only inside the app, never in the email.
- **The return URLs and the metadata**: [the same rules as a checkout](https://nuxt-backend.dev/docs/billing/subscriptions/checkout#what-the-server-decides).
- **The pack**: a catalog pack by its key, never a plan's key, a product id or a key outside the packs (`invalid_input`, field `pack`). Its gift product must exist: an app that has not run `billing sync` since gifts changed is `not_ready` (reason `no_catalog`).
- **The purchaser's limit**: at most 10 gifts in 30 days by default (`rate_limited`, limit `gifts`), set with `setupBilling({ giftLimit: { max, days } })`, where a field you leave out keeps its default. Checkouts the purchaser still has open count too. The limit holds again when the order is paid: a gift paid past it, from several checkouts opened at once, is refunded in full instead of delivered (`billing.gift_over_limit`). With a `rateLimiter` configured, each checkout also counts against the `billingSync` limit, keyed by the purchaser.
- **canGift**, when you pass one (below).
- **An earlier paid order**: the purchaser must have paid for something before, on any workspace they own (`forbidden`, reason `gift_refused`). A stolen card that could buy gifts for strangers at once would be a way to cash it out.

## Who can receive a gift

Every address but the purchaser's own is accepted by default. To limit recipients further, to your own domain only or to sandbox addresses on a public demo, pass `canGift` to `setupBilling`. `giftCheckout` asks it before it opens the checkout:

```ts [backend/billing.ts]
export const billing = setupBilling(components, {
  catalog,
  email,
  canGift: (ctx, { recipientEmail, purchaserUserId, purchaserEntityId }) =>
    recipientEmail.endsWith('@example.com') || { allowed: false, message: 'Gifts go to teammates only.' },
})
```

`recipientEmail` arrives lower-cased, `purchaserEntityId` is the billing entity that pays, and `ctx` is the action's, so the check can read your own tables. A refusal is `forbidden` with reason `gift_refused`, carrying the message (default: "This address can't receive gifts."), so it reaches the purchaser even where production hides other errors. `canGift` adds to the package's own rules; it never lifts them.

## Receiving gifts

```vue
<script setup lang="ts">
const gifts = useGifts()
</script>

<template>
  <GiftClaimBanner />  <!-- or your own UI over gifts.unclaimed / gifts.claim() -->

  <div v-for="g in gifts.unclaimed.value" :key="g.id">
    A gift from {{ g.purchaserName ?? 'someone' }}: {{ g.message }}
    <button :disabled="gifts.isClaiming.value" @click="gifts.claim(g.id)">Receive</button>
  </div>
</template>
```

Nothing is claimed until the recipient asks.

<field-group>
<field name="received" type="ComputedRef<ReceivedGift[] | undefined>">

The paid and claimed gifts addressed to the signed-in user's verified email: `id`, `purchaserName` (at most 40 characters, no links), `pack`, `productIds`, `message`, `status` (`'paid'` → `'claimed'`), `createdAt`, `paidAt`, `claimedAt`. Never the purchaser's email or a billing id; a gift still at checkout or refunded is not listed.

</field>

<field name="unclaimed" type="ComputedRef<ReceivedGift[]>">

The `paid` ones, claimable now.

</field>

<field name="isLoading / isClaiming" type="ComputedRef<boolean>">

`isLoading` is `true` until the gift list has loaded (always `false` when signed out); `isClaiming` while a claim is in flight.

</field>

<field name="claim(giftId, { successUrl?, redirect? })" type="Promise<boolean>">

Claim one gift into the signed-in user's own billing account; the session's email must be verified (`forbidden`, reason `no_email`, otherwise). Resolves `true` when the server confirmed the claim: the credits arrive reactively a moment later, once the provider says the $0 order is paid. Resolves `false` after opening the provider's $0 checkout, for a pack that asks for checkout fields. The server refuses a gift addressed to another email or one that does not exist alike (`forbidden`, reason `not_recipient`), a refunded one (`gift_refunded`), and one whose purchase is not paid yet (`not_ready`, reason `not_paid`).

</field>
</field-group>

In workspace mode a gift is claimed into the recipient's personal workspace.

A gift paid before 0.2 was billed to a customer keyed by the recipient's email. Its claim makes that customer the recipient's own when their personal workspace has none yet, and the credits are already there; otherwise the gift gets its claim discount then and is claimed like any other.

## The notification email

It goes out through `email`, the `setupEmail` instance `setupBilling` takes, once per gift, after the purchase is paid. It names the sender (at most 40 characters, with links and domains taken out) and the pack, and links to `SITE_URL` with no token: the recipient signs in to claim. The purchaser's email and message stay out of it.

The package sends first and stamps the gift after, with the key `nuxt-backend:gift:<id>`:

- A send that throws, from a template override, test mode refusing the address or a transient failure, leaves no stamp, so the provider's redelivery of `order.paid` sends it again.
- A run that sent but failed before stamping is absorbed by the key while the first email's record exists.
- Once stamped, a replay emails nobody, however late it comes.

The default is a minimal, dependency-free template. Restyle it as the `gift` [template](https://nuxt-backend.dev/docs/email/templates), with the rest of the package's email:

```ts [backend/email.ts]
export const email = setupEmail(components, {
  templates: {
    gift: ({ recipientEmail, purchaserName, item, claimUrl }) => ({
      to: recipientEmail,
      subject: `${purchaserName ?? 'Someone'} sent you a gift 🎁`,
      html: `…${escapeHtml(item)}… <a href="${claimUrl}">Receive it</a>`,
    }),
  },
})
```

Keep it transactional: no promotion in a gift notice.

<note>

The gift product of each pack is created by `npx nuxt-backend billing sync`, private and out of the price list and the checkout allow-list. `billing.generated.ts` lists it under `gifts`, by pack key.

</note>

<page-keys>

- Try it live in the playground: [/playground/platform/credits](https://nuxt-backend.dev/playground/platform/credits)
- API reference: [/docs/reference/api/use-gifts](https://nuxt-backend.dev/docs/reference/api/use-gifts)
- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/billing/gifts.ts](https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/billing/gifts.ts)

</page-keys>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
