> ## Documentation Index
> Fetch the complete documentation index at: https://direct-api.reap.global/llms.txt
> Use this file to discover all available pages before exploring further.

# Issue cards for advertising campaigns at scale

> Create a dedicated virtual card for every ad account or campaign, give each one a fixed budget, and manage them all from your own system.

## Why one card per campaign?

Advertising teams often run dozens or hundreds of ad accounts and campaigns across ad platforms at once. Paying for all of them with one shared card creates problems:

* **You can't tell which campaign spent what** without manual reconciliation.
* **One overspending campaign eats everyone's budget.**
* **If the card is compromised or blocked by a platform,** every campaign stops.

Giving each ad account or campaign **its own virtual card** solves all three:

| Benefit | How the Business Account API delivers it |
| - | - |
| **Hard budget per campaign** | `topUp` cards with a fixed `spendCap`. The card stops when the budget is used |
| **Instant attribution** | Each card's `title` names the campaign; each card reports its own `spentAmount` |
| **Contained risk** | Freeze or close one card without touching any other campaign |
| **No manual work** | Create, top up and close cards from your campaign tool, not the dashboard |

This is the "when a media buyer launches 50 new campaigns in our internal tool, each one automatically gets its own card with its budget loaded. When a campaign needs more budget, we raise its cap. When it ends, the card is closed" use case. Finance sees spend per campaign with no reconciliation.

## How it works

```mermaid theme={"system"}
sequenceDiagram
    participant You as Your campaign tool (backend)
    participant API as Business Account API
    participant Ads as Ad platform
    You->>API: GET /cards/balances (enough credit for the batch?)
    You->>API: GET /team-members (who holds the cards?)
    You->>API: GET /cards/categories (advertising category)
    loop For each campaign
        You->>API: POST /cards + Idempotency-Key
        API-->>You: card id
    end
    You->>API: POST /cards/{cardId}/reveal
    API-->>You: accessUrl (valid 1 min)
    You->>Ads: Media buyer adds card to the ad account
    You->>API: GET /cards?title=... (track spend per campaign)
    You->>API: PUT /cards/{cardId}/spend-control (increase budget)
    You->>API: DELETE /cards/{cardId} (campaign ended)
```

| Step | What you do | Endpoint | Scope needed |
| - | - | - | - |
| 1 | Plan the batch and check credit | `GET /cards/balances` | `card_account_balances:read` |
| 2 | Pick the cardholder | `GET /team-members` | `team_members:read` |
| 3 | Pick the category | `GET /cards/categories` | `cards:read` |
| 4 | Create one card per campaign | `POST /cards` | `cards:write` |
| 5 | Add each card to its ad account | `POST /cards/{cardId}/reveal` | `cards:read_vault` |
| 6 | Track spend per campaign | `GET /cards`, `GET /cards/{cardId}` | `cards:read` |
| 7 | Top up running campaigns | `PUT /cards/{cardId}/spend-control` | `spend_controls:write` |
| 8 | Close cards when campaigns end | `DELETE /cards/{cardId}`, `PUT /cards/{cardId}/freeze` | `cards:write` |

## Before you begin

* **Business Account API access is enabled** and you have an API key with the scopes above. See [Authentication](/api-reference/authentication.md).
* **Your business has deposited funds** and has enough credit to cover all campaign budgets combined. See [How to add funds to your Reap Account](https://support.reap.global/hc/en-us/articles/16170706183567-How-to-add-funds-to-your-Reap-Account) in the Help Center.
* **One approved team member will hold the cards**, typically the head of performance marketing or a finance admin.
* **You know whether your business uses Approvals.** Approval is a dashboard feature: if your business uses it, cards created through the API are subject to it and each card in the batch creates its own approval request in the Business Account dashboard. If your business doesn't use it, it never applies. See [How do I set up approval workflows for my team?](https://support.reap.global/hc/en-us/articles/13939633575567-How-do-I-set-up-approval-workflows-for-my-team) in the Help Center.
* **This guide builds on [Issue cards to your team](/guides/issue-cards-to-your-team.md).** Read it first if you're new to the API.

```bash theme={"system"}
export REAP_API="https://api.global.direct.reap.global/2026-07-13"
export REAP_KEY="your-api-key"   # load from a secrets manager in production
```

**1. Plan the batch and check credit**

Start from a list of campaigns in your own system, each with a stable reference and a budget:

```json theme={"system"}
[
  { "ref": "CMP-0001", "name": "Spring Sale – Search – US", "budget": "1500.00" },
  { "ref": "CMP-0002", "name": "Spring Sale - Social - HK", "budget": "800.00" },
  { "ref": "CMP-0003", "name": "App Install – Video – SG",  "budget": "2500.00" }
]
```

Add up the budgets and check your account can cover them:

```bash theme={"system"}
curl "$REAP_API/cards/balances" -H "x-api-key: $REAP_KEY"
```

Compare your total with **`unallocatedCredit`**: credit not yet set aside for other cards, and the best guide to how much new campaign budget you can hand out. Balances are calculated at request time, so check just before each batch.

**2. Pick the cardholder**

All campaign cards are held by one approved team member. Find their `userId`:

```bash theme={"system"}
curl "$REAP_API/team-members?limit=100" -H "x-api-key: $REAP_KEY"
```

Pick the member with `inviteStatus: "approved"` whose `email` matches your cardholder, and store the `userId` in your configuration.

**3. Pick the category**

```bash theme={"system"}
curl "$REAP_API/cards/categories" -H "x-api-key: $REAP_KEY"
```

Choose the category that fits advertising spend and keep its `id`. The examples below use `marketing`; replace it with the `id` your account returns.

**4. Create one card per campaign**

### Name every card after its campaign

The card `title` is how you, finance and the dashboard will recognise each card. Use a consistent format that starts with your own reference:

```text theme={"system"}
CMP-0001 - Spring Sale - Search - US
```

Starting with the reference lets you find the card later with `GET /cards?title=CMP-0001`.

### Create each card

```bash theme={"system"}
curl -X POST "$REAP_API/cards" \
  -H "x-api-key: $REAP_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ads-2026-10-CMP-0001" \
  -d '{
    "type": "virtual",
    "title": "CMP-0001 - Spring Sale - Search - US",
    "cardholderId": "user_01JZ2Q1J4Y8P7M6N5K3H2G1F0E",
    "category": "marketing",
    "planType": "topUp",
    "spendCap": { "amount": "1500.00", "currency": "USD" },
    "expiresAt": "2027-10-31"
  }'
```

| Field | Why it matters for advertising |
| - | - |
| `planType: "topUp"` | Gives the card its own budget, separate from your other cards |
| `spendCap` | The campaign budget. The card declines once it's used up |
| `title` | Campaign reference and name, for attribution and lookup |
| `expiresAt` | Optional. Set it past the campaign's end date so the card doesn't expire mid-flight |
| `spendControl.daily` | Optional. Adds a daily pacing limit on top of the total budget |

### Handle each outcome

Same as in [Issue cards to your team](/guides/issue-cards-to-your-team.md):

* **`201`**: card ready. Store the card `id` against the campaign.
* **`201` with `status: "pending"`**: created, still being set up. Store the `id` and poll the `Location` header until `active`.
* **`202` with `status: "pendingApproval"`**: waiting for an approver. Store the `approvalRequestId`; once approved, find the card with `GET /cards?title=CMP-0001`.

### Rules for running a batch reliably

1. **Build each `Idempotency-Key` from your campaign reference**, for example `ads-2026-10-CMP-0001`. If the batch stops halfway and you re-run it, cards already created come back as-is instead of being duplicated.
2. **Save each result immediately** before moving to the next campaign.
3. **Send requests one after another**, not in parallel. On `429`, back off and retry with the same key.
4. **Stop the batch on errors that affect everything.** `CARD_CREATION_REQUIRES_DEPOSIT`, `401` and `403` will fail every request, so fix the cause first.

| Response | What to do |
| - | - |
| `409 IDEMPOTENCY_KEY_IN_FLIGHT`, `429`, `503 CARD_CREATION_UNAVAILABLE` | Wait, then retry with the **same key** |
| `503 IDEMPOTENCY_UNAVAILABLE` | Check `GET /cards?title=CMP-0001` before retrying |
| `422 CARDHOLDER_NOT_FOUND`, `422 CURRENCY_MISMATCH`, `400` | Fix the data and retry that campaign; for `CARDHOLDER_NOT_FOUND` check `inviteStatus` (see [Managing Your Team](https://support.reap.global/hc/en-us/articles/16170798649743-Managing-Your-Team) in the Help Center) |
| `422 IDEMPOTENCY_KEY_REUSED` | Two campaigns share a key. Make keys unique |
| `422 CARD_CREATION_REQUIRES_DEPOSIT`, `401`, `403` | Stop the batch and fix the account or key |

**5. Add each card to its ad account**

To set up billing on an ad platform, the media buyer needs the card number, expiry and CVV. Show them in the secure, Reap-hosted window right inside your campaign tool:

```bash theme={"system"}
curl -X POST "$REAP_API/cards/card_01JZ2Q4T4J7QF2G6Z3X9A1B5CD/reveal" \
  -H "x-api-key: $REAP_KEY"
```

```json theme={"system"}
{ "accessUrl": "https://secure.card.reap.global/api/card/show-card-info?auth=temporary-token" }
```

```html theme={"system"}
<iframe src="ACCESS_URL_FROM_YOUR_BACKEND" title="Card details"
        style="width: 400px; height: 260px; border: 0;"></iframe>
```

A typical flow: a **Show card** button next to each campaign in your tool. The media buyer clicks it, copies the card number into the ad platform's billing settings, and moves on to the next campaign.

> [!WARNING]
>
> * **The link expires after 1 minute.** Request it only when the button is clicked. Never store or log it.
> * **Call reveal from your backend only.** Your API key must never reach the browser.
> * **Restrict the button to media buyers who should see card numbers.** The `cards:read_vault` scope covers every card in the business.

**6. Track spend per campaign**

Each card reports its own spend, so campaign attribution needs no reconciliation.

**One campaign:**

```bash theme={"system"}
curl "$REAP_API/cards/card_01JZ2Q4T4J7QF2G6Z3X9A1B5CD" -H "x-api-key: $REAP_KEY"
```

Compare `spentAmount` with `spendCap` to see budget used, for example USD 1,120 of 1,500 (75%).

**All campaigns:**

```bash theme={"system"}
curl "$REAP_API/cards?title=CMP-&limit=100&page=1" -H "x-api-key: $REAP_KEY"
```

Page through the results and join them to your campaigns using the reference at the start of each title. Typical uses:

* A **budget pacing dashboard** showing spend vs. budget per campaign.
* An **alert at 80% of cap** so the team can top up before ads stop.
* A **daily sync** of spend into your BI or finance system.

For account-level credit, use `GET /cards/balances`. For activity across all cards, use `GET /cards/activities`.

**7. Top up running campaigns**

When a campaign performs well and needs more budget, raise its cap:

```bash theme={"system"}
curl -X PUT "$REAP_API/cards/card_01JZ2Q4T4J7QF2G6Z3X9A1B5CD/spend-control" \
  -H "x-api-key: $REAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "spendCap": { "amount": "2500.00", "currency": "USD" } }'
```

> [!WARNING]
>
> **`spendCap` is the new total, not an increment.** To add USD 1,000 to a USD 1,500 campaign, send `2500.00`. The new cap can't be lower than what's already been spent (`SPEND_CAP_TOO_LOW`), and its currency must match the card.

**8. Close cards when campaigns end**

| Situation | Action | Endpoint |
| - | - | - |
| Campaign paused, may restart | **Freeze** the card | `PUT /cards/{cardId}/freeze` (reverse with `/unfreeze`) |
| Campaign finished | **Terminate** the card | `DELETE /cards/{cardId}` |
| Suspicious charge on one card | **Freeze** immediately, then investigate | `PUT /cards/{cardId}/freeze` |

Closing cards at the end of each campaign keeps your card list clean and stops leftover budget from being spent later, for example by a platform that keeps billing after a campaign ends.

## Complete example: Node.js batch script

Reads `campaigns.json`, creates one top-up card per campaign, and writes results to `results.json` as it goes. Requires Node 18+.

```js theme={"system"}
import fs from "node:fs/promises";

const API = "https://api.global.direct.reap.global/2026-07-13";
const KEY = process.env.REAP_KEY;
const BATCH = "ads-2026-10";
const HOLDER_EMAIL = "performance@yourco.com";
const CATEGORY = "marketing";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function call(method, path, { body, key } = {}) {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: {
      "x-api-key": KEY,
      "Content-Type": "application/json",
      ...(key && { "Idempotency-Key": key }),
    },
    body: body && JSON.stringify(body),
  });
  return { status: res.status, data: await res.json().catch(() => ({})) };
}

const RETRY = new Set([409, 429, 503]);
const STOP = new Set(["CARD_CREATION_REQUIRES_DEPOSIT"]);

async function createCampaignCard(c, holderId) {
  const key = `${BATCH}-${c.ref}`;
  const body = {
    type: "virtual",
    title: `${c.ref} - ${c.name}`,
    cardholderId: holderId,
    category: CATEGORY,
    planType: "topUp",
    spendCap: { amount: c.budget, currency: "USD" },
  };

  for (let attempt = 0; attempt < 5; attempt++) {
    const r = await call("POST", "/cards", { body, key });
    if (r.status === 201) return { ref: c.ref, cardId: r.data.id, status: r.data.status };
    if (r.status === 202) return { ref: c.ref, approvalRequestId: r.data.approvalRequestId, status: "pendingApproval" };
    if ([401, 403].includes(r.status) || STOP.has(r.data.code)) throw new Error(`Stop batch: ${r.data.code || r.status}`);
    if (!RETRY.has(r.status) || r.data.code === "IDEMPOTENCY_UNAVAILABLE") {
      return { ref: c.ref, status: "failed", code: r.data.code, requestId: r.data.requestId };
    }
    await sleep(1000 * 2 ** attempt); // back off, same key
  }
  return { ref: c.ref, status: "failed", code: "RETRIES_EXHAUSTED" };
}

async function main() {
  const campaigns = JSON.parse(await fs.readFile("campaigns.json", "utf8"));

  // 1. Check credit
  const total = campaigns.reduce((s, c) => s + Number(c.budget), 0);
  const bal = await call("GET", "/cards/balances");
  if (Number(bal.data.unallocatedCredit.amount) < total) throw new Error("Not enough unallocated credit");

  // 2. Resolve cardholder
  const team = await call("GET", "/team-members?limit=100");
  const holder = team.data.items.find((m) => m.inviteStatus === "approved" && m.email === HOLDER_EMAIL);

  // 3. Create cards one by one, saving as we go
  const results = [];
  for (const c of campaigns) {
    results.push(await createCampaignCard(c, holder.userId));
    await fs.writeFile("results.json", JSON.stringify(results, null, 2));
    await sleep(200);
  }
  console.table(results);
}

main();
```

Re-running is safe: campaigns that already have a card return the same card, thanks to the idempotency key.

## Go-live checklist

* One approved team member holds the campaign cards
* Card titles start with your campaign reference
* `Idempotency-Key` built from the campaign reference
* Total budget checked against `unallocatedCredit` before each batch
* `201` and `202 pendingApproval` all handled and saved
* Retries limited to `409`, `429` and `503`, with the same key
* Reveal called from your backend, on demand, for authorised media buyers only
* Alert when a card nears its cap
* Cards terminated when campaigns end


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