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

# Issuing on a tier change

> Issue tiered loyalty credentials from your backend when users cross spend or activity thresholds — portable Gold and Platinum tiers across partner platforms.

This recipe walks through issuing a portable loyalty credential that users carry across your partner ecosystem. When a user crosses a tier threshold (e.g. Gold, Platinum), your backend issues a new credential automatically.

<Info>
  This recipe creates a tier credential, not spendable loyalty points. For funded campaign rewards, see [Issuing loyalty points](/products/loyalty/issuing-points).
</Info>

## What you'll build

1. A credential schema that encodes loyalty tier, lifetime points, and membership date.
2. A backend function that issues (or upgrades) the credential when a user crosses a tier boundary.
3. Issuer-signed, holder-encrypted credentials stored in DStorage.

## Prerequisites

* A published issuance program with a loyalty schema
* [Partner JWT](/get-started/authentication/sdk-auth) signing configured
* Issuer signing keys available to your backend

## Step 1: Design the credential schema

Create a schema in the Developer Dashboard (Issuer > Schemas) with fields like:

| Field | Type | Description |
| - | - | - |
| `tier` | string | Loyalty tier name (e.g. `"Gold"`, `"Platinum"`) |
| `lifetimePoints` | integer | Total points accumulated |
| `memberSince` | string | ISO date when membership started |
| `upgradedAt` | string | ISO date of the most recent tier upgrade |

See [Schema Creation](/products/identity/schema-creation) for full setup instructions.

## Step 2: Issue on tier upgrade

When your loyalty engine determines a user has crossed a tier boundary, resolve the recipient, then build, sign, encrypt, and store a fresh tier credential. Dedupe by recipient email + program ID and revoke the previous tier credential before reissuing.

<Warning>
  The recipient's email (passed to `initialize-user`) is the routing key that determines which AIR Account the credential lands in. Resolve it from the triggering event, and never reuse a partner, service, admin, or static email across recipients — every credential issued against that email lands in the same account.
</Warning>

```js theme={null}
const jwt = require("jsonwebtoken");
const fs = require("fs");
// issuer-controlled helpers backed by your signing keys
const { buildVc, signVc, encryptToHolder } = require("./lib/credential");

const privateKey = fs.readFileSync("path/to/private.key");
const BASE_URL = process.env.API_BASE_URL || "https://api.sandbox.mocachain.org/v1";

function getPartnerJwt() {
  const now = Math.floor(Date.now() / 1000);
  return jwt.sign(
    // No email claim — the recipient is passed to initialize-user, not the JWT
    { partnerId: process.env.PARTNER_ID, scope: "issue", iat: now, exp: now + 300 },
    privateKey,
    { algorithm: "RS256", header: { kid: process.env.KEY_ID, typ: "JWT" } }
  );
}

async function issueLoyaltyCredential(recipientEmail, tierData) {
  const token = getPartnerJwt();

  // 1. Resolve or create the recipient's AIR Account
  const initRes = await fetch(`${BASE_URL}/auth/initialize-user`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-partner-auth": token },
    body: JSON.stringify({ email: recipientEmail }),
  });
  if (!initRes.ok) throw new Error(`initialize-user failed: ${initRes.status}`);
  const { did, publicKey } = await initRes.json();

  // 2. Build, sign (SD-JWT VC), and encrypt the credential to the holder
  const schemaId = process.env.LOYALTY_CREDENTIAL_ID;
  const vc = buildVc({
    holderDid: did,
    schemaId,
    credentialSubject: {
      tier: tierData.tier,
      lifetimePoints: tierData.lifetimePoints,
      memberSince: tierData.memberSince,
      upgradedAt: new Date().toISOString(),
    },
  });
  const encrypted = encryptToHolder(signVc(vc), publicKey);

  // 3. Store the encrypted envelope in DStorage
  const storeRes = await fetch(`${BASE_URL}/dstorage/vcs`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-partner-auth": token },
    body: JSON.stringify({
      holderDid: did,
      schemaId,
      expiresAt: vc.expirationDate,
      data: encrypted.encryptedData,
      iv: encrypted.iv,
      authTag: encrypted.authTag,
      encryptedKey: encrypted.dataEncPublicKey,
      externalId: vc.id,
    }),
  });
  if (!storeRes.ok) throw new Error(`dstorage/vcs failed: ${storeRes.status}`);
  return storeRes.json(); // { storagePath }
}
```

## Step 3: Trigger on purchase events

Wire the issuance into your purchase or activity pipeline:

```js theme={null}
async function onPurchaseComplete(userEmail, purchaseAmount) {
  const user = await getUserProfile(userEmail);
  const newPoints = user.lifetimePoints + calculatePoints(purchaseAmount);
  const newTier = resolveTier(newPoints);

  if (newTier !== user.currentTier) {
    const result = await issueLoyaltyCredential(userEmail, {
      tier: newTier,
      lifetimePoints: newPoints,
      memberSince: user.memberSince,
    });
    console.log(`Upgraded ${userEmail} to ${newTier}, storagePath: ${result.storagePath}`);
  }

  await updateUserProfile(userEmail, { lifetimePoints: newPoints, currentTier: newTier });
}
```

## Step 4: Let verifiers read the credential

Any partner in the ecosystem can verify the user's tier using [Credential Verification](/products/identity/verify). The user presents the credential (via AIR Kit SDK), and the verifier confirms tier status without accessing underlying purchase data.

## Duplicate handling

Dedupe by recipient email + program ID before issuing so an event replay can't fan out duplicate credentials. When a user upgrades tiers, revoke the previous tier credential before storing the new one. For one-time membership issuance, skip reissuance if a credential already exists for the user + schema.

## Next steps

* [Try it live](https://demo.air3.com/try-it-live/member-discount): present airline membership at a partner café

* [Loyalty for developers](/products/loyalty/developers) for a broader integration guide

* [Issuance API Reference](/api-reference/issuance-api) for endpoint details

* [Credential Verification](/products/identity/verify) to set up cross-partner verification


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