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

# Merchant-Locked Virtual Cards

> Issue a virtual card that binds to a single merchant on first use, by selecting a brand-locked card program.

A **merchant-locked card** — called a *brand-locked* card in Fluz product surfaces — is a virtual card that ties itself to the first merchant it transacts with. Every later authorization at any other merchant is declined.

There is no `merchantId` parameter on `createVirtualCard`. Merchant locking is a property of the **card program**, not of the individual card. You get a merchant-locked card by issuing against a brand-locked `offerId`; everything else about the request is identical to a standard card.

<Warning>
  **The lock binds on first use, not at issuance.**

  You cannot choose *which* merchant the card locks to. You issue an unbound card from a brand-locked program, and the first successful authorization determines the merchant permanently. There is no way to pre-assign, change, or clear the binding after it is set.

  If your product needs a card that only works at a merchant *you* nominate up front, this is not that feature — see [Choosing the right control](#choosing-the-right-control).
</Warning>

***

## Step 1 — Find a brand-locked offer

Filter [`getVirtualCardOffers`](/features/get-card-offers) with `cardBrandLocked: true`. Only programs enabled for your account are returned, so always discover offers at runtime rather than hardcoding an ID.

```graphql theme={null}
query GetVirtualCardOffers($input: GetVirtualCardOffersInput!) {
  getVirtualCardOffers(input: $input) {
    offerId
    bin
    bankName
    programName
    rewardValue
    programLimits {
      dailyLimit
      weeklyLimit
      monthlyLimit
    }
  }
}
```

```json theme={null}
{
  "input": {
    "cardBrandLocked": true,
    "cardType": "PREPAID",
    "cardNetwork": "MASTERCARD"
  }
}
```

### Filter fields

| Field             | Type                   | Description                                                                 |
| :---------------- | :--------------------- | :-------------------------------------------------------------------------- |
| `cardBrandLocked` | `Boolean`              | `true` returns only merchant-locking programs. Omit to return all programs. |
| `cardType`        | `VirtualCardOfferType` | `DEBIT` or `PREPAID`.                                                       |
| `cardNetwork`     | `VirtualCardNetwork`   | `MASTERCARD` or `VISA`.                                                     |

### Sample response

```json theme={null}
{
  "data": {
    "getVirtualCardOffers": [
      {
        "offerId": "e0fb2c5f-6a75-498f-95f6-37359a64cb0f",
        "bin": "123456",
        "bankName": "Example Bank - Mastercard",
        "programName": "Brand Locked Virtual Card - Mastercard Debit",
        "rewardValue": "1.5%",
        "programLimits": {
          "dailyLimit": "100000",
          "weeklyLimit": "700000",
          "monthlyLimit": "3100000"
        }
      }
    ]
  }
}
```

<Note>
  **Read `rewardValue` per offer — do not assume the standard rate.** Cashback on brand-locked programs is set independently of the standard virtual card programs and is frequently different, in both directions: some brand-locked BINs earn a materially lower flat rate, while specific merchants carry boosted rates that only apply on brand-locked cards. `programLimits` also varies by program and caps what `spendLimit` you can request.
</Note>

### Sandbox

Staging exposes one brand-locked program for testing:

| Offer ID                               | Program Name                                   | Reward Value |
| :------------------------------------- | :--------------------------------------------- | :----------- |
| `592c394e-26cc-44ac-a145-a5f81301fe77` | Brand Locked Virtual Card - Mastercard Prepaid | 1.5%         |

See [Test Virtual Card Offers](/test-virtual-card-offers) for the full sandbox offer list.

***

## Step 2 — Issue the card

<Card title="Restricted Access" icon="lock">
  This mutation requires a Bearer token with the `CREATE_VIRTUALCARD` scope.
</Card>

Pass the brand-locked `offerId` to [`createVirtualCard`](/features/create-card). No other field changes behavior with respect to the merchant lock.

```graphql theme={null}
mutation CreateVirtualCard($input: CreateVirtualCardInput!) {
  createVirtualCard(input: $input) {
    virtualCardId
    cardholderName
    virtualCardLast4
    expiryMonth
    expiryYear
    status
    cardType
    initialAmount
    usedAmount
    createdAt
  }
}
```

```json theme={null}
{
  "input": {
    "idempotencyKey": "63b2c9e0-62d1-42ab-b1c2-1a7ee2f8c0a9",
    "offerId": "592c394e-26cc-44ac-a145-a5f81301fe77",
    "spendLimit": 250.00,
    "spendLimitDuration": "MONTHLY",
    "cardNickname": "Ad spend — locks on first use",
    "primaryFundingSource": "FLUZ_BALANCE",
    "userCashBalanceId": "b1155504-ad30-4b2f-873d-b8795277b128"
  }
}
```

### cURL

```bash theme={null}
curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \
  -d '{
  "query": "mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId cardholderName virtualCardLast4 expiryMonth expiryYear status cardType initialAmount createdAt } }",
  "variables": {
    "input": {
      "idempotencyKey": "63b2c9e0-62d1-42ab-b1c2-1a7ee2f8c0a9",
      "offerId": "592c394e-26cc-44ac-a145-a5f81301fe77",
      "spendLimit": 250.00,
      "spendLimitDuration": "MONTHLY",
      "cardNickname": "Ad spend — locks on first use",
      "primaryFundingSource": "FLUZ_BALANCE"
    }
  }
}'
```

<Note>
  **The billing address still has to be verifiable.** Whether passed inline as `billingAddress` or referenced by `userAddressId`, it must be a real, deliverable US address — no PO boxes. It is validated against USPS data via Smarty during cardholder setup, and an unverifiable address fails the whole request with `VC-0025`. See [Address Formatting Requirements](/concepts/address-formatting-requirements).
</Note>

### TypeScript

```typescript theme={null}
const graphql = async (query: string, variables: Record<string, unknown>) => {
  const response = await fetch(
    'https://transactional-graph.staging.fluzapp.com/api/v1/graphql',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${accessToken}`,
      },
      body: JSON.stringify({ query, variables }),
    },
  );
  return response.json();
};

// 1. Discover a brand-locked program available to this account.
const offers = await graphql(
  `query GetVirtualCardOffers($input: GetVirtualCardOffersInput!) {
     getVirtualCardOffers(input: $input) {
       offerId
       programName
       rewardValue
       programLimits { dailyLimit weeklyLimit monthlyLimit }
     }
   }`,
  { input: { cardBrandLocked: true, cardNetwork: 'MASTERCARD' } },
);

const brandLocked = offers.data.getVirtualCardOffers[0];

if (!brandLocked) {
  throw new Error('No brand-locked program enabled for this account.');
}

// 2. Issue against it. The card is unbound until its first authorization.
const card = await graphql(
  `mutation CreateVirtualCard($input: CreateVirtualCardInput!) {
     createVirtualCard(input: $input) {
       virtualCardId
       virtualCardLast4
       status
       initialAmount
     }
   }`,
  {
    input: {
      idempotencyKey: crypto.randomUUID(),
      offerId: brandLocked.offerId,
      spendLimit: 250.0,
      spendLimitDuration: 'MONTHLY',
      cardNickname: 'Ad spend — locks on first use',
      primaryFundingSource: 'FLUZ_BALANCE',
    },
  },
);
```

***

## Choosing the right control

"Lock this card to a merchant" means three different things in practice. Only one of them is the brand lock described on this page.

| You want                                                     | Use                                                                     | Where           |
| :----------------------------------------------------------- | :---------------------------------------------------------------------- | :-------------- |
| A card that self-binds to whichever merchant it first hits   | Brand-locked `offerId` — this page                                      | API             |
| A card restricted to a whole merchant *category* (e.g. fuel) | MCC category lock                                                       | Web portal only |
| A card that can only ever be used once                       | `lockCardNextUse: true` on [`createVirtualCard`](/features/create-card) | API             |

<Info>
  **Brand lock and category lock are not the same control.** A category lock restricts the card to every merchant matching a chosen MCC — a fuel-locked card works at any qualifying gas station. A brand lock restricts the card to exactly one merchant, but you do not get to choose which one in advance. They solve different problems and are configured in different places.
</Info>

***

## What cardholders experience

* The card behaves like any other virtual card until its first successful authorization.
* After that authorization, the merchant is fixed. Attempts elsewhere are declined at authorization — there is no partial approval and no prompt.
* The binding survives locking and unlocking the card, and is not affected by editing the nickname, spend limit, or lock date.
* Wallet provisioning is unaffected, but note that Apple Pay and Google Pay transactions typically earn no cashback regardless of program.

Declines from a merchant mismatch appear in [`getDeclinedTransactions`](/features/get-decline-transactions) alongside all other declines. See [Decline Codes](/features/decline-codes) for interpreting the reason returned.

***

## Common mistakes

<AccordionGroup>
  <Accordion title="Passing a merchant identifier to createVirtualCard">
    There is no such field. `CreateVirtualCardInput` accepts no merchant, brand, or MCC parameter — extra keys are rejected as invalid input with `ARG-0001`. Merchant locking comes entirely from the program you select with `offerId`.
  </Accordion>

  <Accordion title="Hardcoding a brand-locked offerId">
    Offer availability is per-account and programs are added and retired over time. Always call `getVirtualCardOffers` with `cardBrandLocked: true` and handle the empty-result case, rather than persisting an ID from a previous integration or from the sandbox table above.
  </Accordion>

  <Accordion title="Assuming the standard cashback rate applies">
    Brand-locked programs carry their own reward rates. Read `rewardValue` off the offer you actually issue against, and do not display a rate to your users that you inherited from a standard program.
  </Accordion>

  <Accordion title="Testing the lock with a single transaction">
    One successful authorization only proves the card works — it does not prove the lock engaged. To verify end to end, transact once at merchant A, then attempt merchant B and confirm the decline.
  </Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="Issue Cards" icon="credit-card" href="/features/create-card">
    The full `createVirtualCard` reference — funding, limits, and lifecycle controls.
  </Card>

  <Card title="Get Virtual Card Offers" icon="layers" href="/features/get-card-offers">
    Enumerate every program available to your account.
  </Card>

  <Card title="Decline Codes" icon="circle-xmark" href="/features/decline-codes">
    Interpreting declines, including merchant mismatches.
  </Card>

  <Card title="Test Virtual Card Offers" icon="flask" href="/test-virtual-card-offers">
    Sandbox offer IDs for every program, including brand-locked.
  </Card>
</CardGroup>
