# MCP Tools Reference

Phase 1 ships **47 tools** across 11 domains. All tools share a single naming convention `giftronaut_<verb>_<resource>` and a uniform error envelope:

```json
{ "isError": true, "code": "<ERROR_CODE>", "message": "<human-readable>", "status": 4xx|5xx }
```

Every tool is annotated with the standard MCP hint quartet — `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` — plus a human-friendly `title`. Required scope is enforced server-side (a missing scope returns `403 insufficient_scope`).

## Quick Index

| Domain | Tools | Notes |
|--------|-------|-------|
| [Orders](#orders-9-tools) | 9 | Includes 3 destructive `create_*_order` tools, a confirmation-guarded `cancel_order`, an email preview, and a confirmation-guarded reward-link reveal |
| [Balance](#balance-4-tools) | 4 | 2 read-only + ACH top-up request/cancel (production-only, $100 minimum) |
| [Catalog](#catalog-4-tools) | 4 | Read-only |
| [Recipients](#recipients-6-tools) | 6 | Includes confirmation-guarded `delete_recipient` and exact-email lookup |
| [Recipient Groups](#recipient-groups-7-tools) | 7 | Full CRUD + add/remove member; `delete_*` and `remove_*` are confirmation-guarded |
| [Campaigns](#campaigns-6-tools) | 6 | Saved choice-card send templates; `execute_campaign_order` is value-moving |
| [Templates](#templates-5-tools) | 5 | Email visual style (logo + color palette + sender + body) |
| [Draft / Execute](#draft--execute-2-tools) | 2 | Optional 2-step pattern for value-moving orders |
| [CSV](#csv-2-tools) | 2 | Bulk preview + batch create from recipient CSV |
| [Credentials](#credentials-1-tool) | 1 | Sandbox credential bootstrap (REST-only use — MCP is production-only) |
| [Order Limits](#order-limits-1-tool) | 1 | Request a higher daily/monthly order limit (queued for review) |

## Orders (9 tools)

### `giftronaut_create_branded_card_order`

Create a branded gift card order with one or more recipients. Total cost in USD is computed via exchange rate and deducted from your account balance.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `false` |
| `destructiveHint` | `true` |
| `idempotentHint` | `true` |
| `openWorldHint` | `true` |
| Required scope | `orders.write` |

**Parameters:**

| Name | Type | Required | Description                                                              |
|------|------|----------|--------------------------------------------------------------------------|
| `idempotencyKey` | string (≤64) | yes | Reuse to safely retry. 24-hour cache window.                             |
| `productId` | string | yes | Branded card `GOODS_SEQ`.                                                |
| `sendTimingType` | string | yes | `IMMEDIATE` or `SCHEDULED`.                                              |
| `scheduledAt` | string (ISO local) | conditional | Required when `sendTimingType=SCHEDULED`.                                |
| `timezone` | string (IANA) | conditional | Required for `SCHEDULED`.                                                |
| `senderName` | string | yes | Display name shown to recipients.                                        |
| `emailSubject` | string | yes | Email subject line.                                                      |
| `emailMessage` | string | no | Email body (HTML allowed).                                               |
| `templateId` | string | no | Pre-saved email template.                                                |
| `refundOption` | boolean | no | `false` (default) = 180-day window; `true` = 30-day with 50% refund.     |
| `recipients` | array | yes | Each entry: `{email, firstName, lastName, company, department, amount}`. |

**Confirmation:** none. SpendCap is enforced server-side; consider wrapping with `giftronaut_draft_order` for human-in-the-loop review.

### `giftronaut_create_choice_card_order`

Create a Choice Card order. Recipients pick their gift from a curated catalog. All amounts are USD (1:1).

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `true`, scope: `orders.write` |

Parameters mirror `create_branded_card_order` except `productId` references `CHOICE_CARD_DESIGN_SEQ`.

### `giftronaut_create_reward_link_order`

Generate single-use claim links you can distribute manually (no recipient emails). Each link is redeemable **exactly once** and separately **expires** after its window (per the refund option) — single-use and expiry are independent controls. The returned `rewardLinks[]` are **masked** (see [`giftronaut_reveal_reward_links`](#giftronaut_reveal_reward_links)).

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `true`, scope: `orders.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `idempotencyKey` | string | yes | |
| `cardType` | string | yes | `BRANDED_CARD` or `CHOICE_CARD`. |
| `productId` | string | yes | |
| `quantity` | integer (1–500) | yes | Number of links. |
| `amountPerLink` | decimal | yes | Amount per link in product's local currency (≥ 0.01). |
| `refundOption` | boolean | no | |

### `giftronaut_cancel_order`

Cancel a scheduled order at least 10 minutes before send. The held balance is refunded to your account immediately (`refundedImmediately: true` in the response); `cardExpiryWindow` echoes the gift-card expiry policy and is unrelated to refund timing.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `false`, scope: `orders.cancel` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `orderId` | string | yes | |
| `confirm` | string | yes | **Must equal `"YES"`** (case-sensitive). Anything else returns `CONFIRMATION_REQUIRED`. |

### `giftronaut_resend_order`

Resend the gift email to one or more recipients. Allowed within 30 days, not for `CANCELED`/`PENDING` orders.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `true`, scope: `orders.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `idempotencyKey` | string | yes | |
| `orderId` | string | yes | |
| `recipients` | array | yes | Each entry: `{email, updatedEmail?}`. `updatedEmail` replaces the original address before resending. |

### `giftronaut_list_orders`

List orders with filters and pagination.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, `idempotentHint`: `true`, scope: `orders.read` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | no | 1-based, default 1. |
| `pageSize` | integer | no | 1–100, default 20. |
| `orderType` | string | no | `ONE_TIME` \| `SMART` \| `LINK` \| `CAMPAIGN` \| `REWARD`. |
| `cardType` | string | no | `BRANDED_CARD` \| `CHOICE_CARD`. |
| `status` | string | no | `PENDING` \| `IN_PROGRESS` \| `COMPLETE` \| `CANCELED`. |
| `startDate` / `endDate` | string (`YYYY-MM-DD`) | no | |
| `searchKeyword` | string | no | Substring match across orderId / sender name only. Does **not** match recipient email — use `recipientEmail` for that. |
| `recipientEmail` | string | no | Filter orders sent to this recipient email. **Exact, case-sensitive match** — emails are deterministically encrypted at rest, so the value must match exactly as entered at order time (e.g. `John@Corp.com` will not be found by `john@corp.com`) and partial/substring search is not possible. |
| `clientOrderId` | string | no | Filter by your idempotency key. |

### `giftronaut_get_order`

Fetch a single order with all recipients and delivery statuses. Cross-org access returns `RESOURCE_NOT_FOUND`.

Each recipient's `rewardLink` is returned **masked** — scheme + host only (e.g. `https://redeem.giftronaut.com/…`) — because a full reward URL is a live payment instrument: anyone who holds it can claim the funds. To obtain the full URLs, call [`giftronaut_reveal_reward_links`](#giftronaut_reveal_reward_links) with `confirm="YES"`.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, scope: `orders.read` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `orderId` | string | yes | |

### `giftronaut_reveal_reward_links`

Return the **unmasked** reward-link redeem URLs for an order. This is the only MCP surface that exposes full reward URLs — every other order tool masks them because each URL is a **live payment instrument** (anyone holding the complete URL can claim the funds). Requires explicit human confirmation.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, `destructiveHint`: `false`, `idempotentHint`: `true`, scope: `orders.read` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `orderId` | string | yes | Order whose full reward URLs to reveal. |
| `confirm` | string | yes | **Must equal `"YES"`** (case-sensitive). Anything else returns `CONFIRMATION_REQUIRED` and no URLs are exposed. |

Returns `RewardLinkRevealResponse` with `orderId`, `count`, and `rewardLinks[]` (each: `email` — `null` for anonymous LINK orders — and the full `rewardLink`). Treat the returned URLs like passwords: do not log or re-post them.

### `giftronaut_send_test_email`

Send a **preview** of the choice-card gift email to an address you control — rendered with the exact same layout as the real delivery email, including a working redeem button that opens a live preview of the redeem page (issued under Giftronaut's internal demo account). The caller's balance is never charged and nothing appears in their order history. Personalization variables (`{{First Name}}` etc.) render as placeholder text. Daily limit applies (default 10/day). Choice cards only.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `false`, scope: `orders.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `email` | string | yes | Where to send the preview. |
| `productId` | string | yes | Choice card design ID. |
| `amount` | decimal | yes | USD amount shown in the email. |
| `senderName` | string | yes | |
| `emailSubject` | string | yes | |
| `emailMessage` | string | no | |
| `templateId` | string | no | Email layout (colors/logo); defaults to the Giftronaut palette. |

## Balance (4 tools)

### `giftronaut_get_balance`

Returns `availableBalance` and `pendingBalance` (USD). `pendingBalance` matches the portal dashboard: your account's ACH/confirmed-card funding requests still awaiting approval.

When the available balance drops below the platform-configured threshold (`giftronautapi.balance.low-balance-threshold-usd`, default $50), the success envelope's `hints` field surfaces a soft top-up suggestion the LLM can use to prompt the user — see [Low-balance hint](#low-balance-hint) below.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, scope: `balance.read` |

No parameters.

### `giftronaut_get_ledger`

Paginated ledger of balance events (`CHARGE` / `USE` / `REFUND` / `REWARD` / `TRANSFER`).

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | no | 1-based, default 1. |
| `pageSize` | integer | no | 1–100, default 20. |
| `startDate` / `endDate` | string | no | Inclusive `YYYY-MM-DD`. |

### `giftronaut_request_ach_top_up`

Submit a pending ACH top-up request. **Production-only** — sandbox credentials are rejected with `SANDBOX_NOT_SUPPORTED` (403). Funds are NOT immediately available: approval typically takes 1-3 business days, with `BALANCE_ACH_APPROVED` / `BALANCE_ACH_REJECTED` webhook events firing on resolution.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `false`, `destructiveHint`: `false`, `idempotentHint`: `true`, `openWorldHint`: `true`, scope: `balance.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `idempotencyKey` | string (≤64) | yes | Reuse to safely retry. 24h cache window. |
| `amount` | decimal | yes | USD amount. Minimum **$100.00** (server-enforced). |
| `companyName` | string | yes | Legal company name. |
| `ein` | string | yes | Employer Identification Number (9 digits, hyphen optional). |
| `phoneNumber` | string | yes | Business phone number. |
| `billingAddress` | string | yes | Street address (line 1). |
| `city` | string | yes | Billing city. |
| `state` | string | yes | Billing state (e.g. `CA`, `NY`). |
| `country` | string | yes | ISO 3166 alpha-2 country code (e.g. `US`). |
| `zipCode` | string | yes | ZIP / postal code. |
| `companyWebsite` | string | no | Optional. |

**Response shape:**

```json
{
  "data": {
    "fundingNo": "20260521123456",
    "amountUsd": "500.00",
    "currency": "USD",
    "status": "PENDING",
    "message": "Your ACH top-up request has been submitted and is pending review.",
    "estimatedProcessingTime": "1-3 business days",
    "createdAt": "2026-05-21T07:42:11Z"
  },
  "meta": { "env": "prod", "idempotencyKey": "..." }
}
```

**Card top-ups** are NOT available via MCP — they require interactive 3DS / fraud-check at the portal. Direct the user to <https://portal.giftronaut.com/marketing/sign-in>, then **Account Balance → ADD FUNDS + → Credit Card**.

### `giftronaut_cancel_ach_top_up`

Withdraw (void) a **still-pending** ACH top-up request — mirrors the portal's "Void Funding Request" action. Only requests awaiting review can be canceled; completed, rejected, or already-canceled requests return `FUNDING_NOT_CANCELABLE` (409). After cancel the request disappears from `pendingBalance` and the reserved `BALANCE_ACH_APPROVED` / `BALANCE_ACH_REJECTED` webhook events will not fire. **Production-only.**

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `false`, scope: `balance.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `fundingNo` | string | yes | Funding request number (from `giftronaut_request_ach_top_up` or the portal balance history). |
| `confirm` | string | yes | **Must equal `"YES"`** (case-sensitive). Anything else returns `CONFIRMATION_REQUIRED`. |

## Catalog (4 tools)

All Catalog tools require scope `catalog.read` and are `readOnlyHint=true`.

### `giftronaut_search_branded_cards`

List branded card products with optional filters.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` / `pageSize` | integer | no | 1-based page (default 1), pageSize 1..100 (default 20). |
| `search` | string | no | Substring match across card name / brand name. |
| `category` | string | no | Category code filter. |
| `country` | string | no | ISO 3166-1 alpha-2 (e.g. `US`, `JP`). Filters on the card's actual country column; multi-country (e.g. `US,CA`) cards are included when they contain the requested code. |

### `giftronaut_get_branded_card`

Fetch a single branded card with denominations and supported currencies.

| Name | Type | Required |
|------|------|----------|
| `productId` | string | yes |

### `giftronaut_search_choice_cards`

List Choice Card designs available to your organization.

### `giftronaut_get_choice_card`

Fetch a Choice Card design with min/max balance config.

| Name | Type | Required |
|------|------|----------|
| `productId` | string | yes |

## Recipients (6 tools)

### `giftronaut_list_recipients`

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, scope: `recipients.read` |

Standard `page`, `pageSize`, optional `searchKeyword` (substring match across firstName / lastName / company / department — **emails are AES-encrypted in storage so substring email search is not supported via this parameter**; use `find_recipient_by_email` for exact-email lookup).

### `giftronaut_get_recipient`

Cross-org access returns `RESOURCE_NOT_FOUND`.

| Name | Type | Required |
|------|------|----------|
| `recipientId` | string | yes |

### `giftronaut_find_recipient_by_email`

Exact-email lookup against the AES-encrypted column. Use **before** `create_*_order` to confirm a recipient already exists in the recipient list, or to fetch the recipient by email without listing.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, `idempotentHint`: `true`, scope: `recipients.read` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `email` | string | yes | Plaintext email; AES-encrypted internally for exact-match lookup. |

Returns the recipient record on hit, or an `isError: { code: "RESOURCE_NOT_FOUND", resourceType: "Recipient" }` envelope on miss.

### `giftronaut_create_recipient`

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `true`, scope: `recipients.write` |

| Name | Type | Required |
|------|------|----------|
| `idempotencyKey` | string | yes |
| `email` | string | yes |
| `firstName` / `lastName` | string | no |
| `company` / `department` | string | no |

### `giftronaut_update_recipient`

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `true`, scope: `recipients.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `recipientId` | string | yes | |
| `email` / `firstName` / `lastName` / `company` / `department` | string | no | Provide only fields you want to change. |

### `giftronaut_delete_recipient`

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `false`, scope: `recipients.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `recipientId` | string | yes | |
| `confirm` | string | conditional | Must equal `"YES"` exactly. Waived when `dryRun=true`. |
| `dryRun` | boolean | no | If `true`, returns a deletion preview (`DeleteRecipientDryRunResponse`) without invoking the service. Pending-order count is included so the agent can warn before re-running with `dryRun=false`. |

A recipient with one or more pending scheduled orders cannot be deleted and returns `RECIPIENT_HAS_PENDING_ORDER`.

## Recipient Groups (7 tools)

### `giftronaut_list_recipient_groups`

`readOnly`, scope `recipients.read`.

### `giftronaut_get_recipient_group_with_members`

Returns the group plus all members in a single call (REST equivalent is 2 calls).

| Name | Type | Required |
|------|------|----------|
| `groupId` | string | yes |

### `giftronaut_create_recipient_group`

Create an empty named bucket. Group name must be unique within your organization (`DUPLICATE_GROUP_NAME` on conflict). Returns the new `groupId` (format `GR{yyyyMMdd}{6digits}`).

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `true`, scope: `recipients.write` |

| Name | Type | Required |
|------|------|----------|
| `idempotencyKey` | string | yes |
| `groupName` | string (≤200) | yes |

### `giftronaut_update_recipient_group`

Rename an existing group. Only `groupName` is mutable — members are preserved. Same uniqueness rule as create: duplicates within the org return `DUPLICATE_GROUP_NAME` / 409. Does NOT accept an idempotency key (PUT semantics — same payload is naturally re-runnable).

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `false`, scope: `recipients.write` |

| Name | Type | Required |
|------|------|----------|
| `groupId` | string | yes |
| `groupName` | string (≤200) | yes |

### `giftronaut_delete_recipient_group`

Drops the group and all its member-association rows. The underlying recipient records are **preserved** in the recipient list (use `giftronaut_delete_recipient` to remove a recipient itself).

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `false`, scope: `recipients.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `groupId` | string | yes | |
| `confirm` | string | yes | Must equal `"YES"` exactly. |

### `giftronaut_add_recipient_to_group`

| Annotation | Value |
|------------|-------|
| `idempotentHint` | `true`, scope: `recipients.write` |

| Name | Type | Required |
|------|------|----------|
| `idempotencyKey` | string | yes |
| `groupId` | string | yes |
| `recipientId` | string | yes |

### `giftronaut_remove_recipient_from_group`

Drops a single membership row; the recipient remains in the recipient list. Re-removing the same pair returns `RESOURCE_NOT_FOUND` (not idempotent).

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `false`, scope: `recipients.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `groupId` | string | yes | |
| `recipientId` | string | yes | |
| `confirm` | string | yes | Must equal `"YES"` exactly. |

## Campaigns (6 tools)

A campaign is an org-wide named template for choice-card sends (product + amount + subject + sender + message + email template). Creating a campaign does NOT move funds — only `execute_campaign_order` does. All campaigns are USD 1:1 Choice Cards (branded-card campaigns are not supported via this API surface).

### `giftronaut_list_campaigns`

Paginated browse of saved campaigns with optional substring search against campaign name.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, `idempotentHint`: `true`, scope: `campaigns.read` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | no | 1-based, default 1. |
| `pageSize` | integer | no | 1–100, default 20. |
| `search` | string | no | Substring match against campaign name. |

### `giftronaut_get_campaign`

Fetch a single campaign with all 13 fields. Cross-org access returns `RESOURCE_NOT_FOUND`.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, scope: `campaigns.read` |

| Name | Type | Required |
|------|------|----------|
| `campaignId` | string | yes |

### `giftronaut_create_campaign`

Save a new campaign. Validates the choice-card product exists and the amount is within its min/max balance band. Resolves `templateId` to a server-side `templateSeq`.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `true`, scope: `campaigns.write` |

| Name | Type | Required | Description                                                          |
|------|------|----------|----------------------------------------------------------------------|
| `idempotencyKey` | string (≤64) | yes | 24h cache for safe retry.                                            |
| `name` | string (≤100) | yes | Campaign display name.                                               |
| `cardType` | string | yes | Must be `"CHOICE_CARD"`.                                             |
| `productId` | string | yes | Choice card product ID (`CHOICE_CARD_DESIGN_SEQ`).                   |
| `amount` | decimal | yes | Per-recipient USD (positive whole number).                           |
| `subject` | string (≤200) | yes | Email subject line.                                                  |
| `senderName` | string (≤100) | yes | Display sender name.                                                 |
| `message` | string (≤2000) | no | Plain-text message body.                                             |
| `templateId` | string | no | UUID of an email template; null = use org default at execute time.   |
| `refundOption` | boolean | no | `false` (default) = 180-day expiry; `true` = 30-day with 50% refund. |

### `giftronaut_update_campaign`

PUT-semantics replacement. Omitted optional fields revert to defaults; fetch via `get_campaign` first to preserve values. Does NOT accept an idempotency key.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `false`, scope: `campaigns.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `campaignId` | string | yes | |
| All other fields | — | yes (required mirror of `create_campaign`) | `idempotencyKey` is omitted. |

### `giftronaut_delete_campaign`

Soft delete (sets `IS_DELETED=1`). Past order history that references the campaign is preserved.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `false`, scope: `campaigns.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `campaignId` | string | yes | |
| `confirm` | string | yes | Must equal `"YES"` exactly. |

### `giftronaut_execute_campaign_order`

Fire a saved campaign against either an inline `recipients[]` list OR a saved `recipientGroupId` (mutually exclusive). Moves funds. Subject to GeoIP country guard, daily USD spend cap, and balance check — insufficient balance returns `INSUFFICIENT_BALANCE` (402) with `topUpFlow` hint. Per-execute overrides (`senderName`, `subject`, `message`) replace the campaign's preset for this single execution only.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `true`, scope: `campaigns.write` + `orders.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `idempotencyKey` | string (≤64) | yes | |
| `campaignId` | string | yes | |
| `recipients` | array | conditional | Inline list of `{email, firstName?, lastName?, company?, department?, amount?}`. Required if `recipientGroupId` is null. |
| `recipientGroupId` | string | conditional | Send to all members of this saved group. Required if `recipients` is null. |
| `senderName` / `subject` / `message` | string | no | Per-execute overrides for the campaign preset. |

## Templates (5 tools)

Email templates define the visual style of a gift-card delivery email — logo image URL, button color palette, sender display name, and an optional plaintext `message` body. Color fields are hex strings in `#RRGGBB` format. The `isDefault` flag cannot be flipped via this API — managed by portal admin only.

### `giftronaut_list_templates`

Paginated list of templates. Each item carries id, name, sender, default flag, and timestamps — full details require `get_template`.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, `idempotentHint`: `true`, scope: `templates.read` |

| Name | Type | Required |
|------|------|----------|
| `page` | integer | no |
| `pageSize` | integer | no |

### `giftronaut_get_template`

Fetch a single template with all 13 fields.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, scope: `templates.read` |

| Name | Type | Required |
|------|------|----------|
| `templateId` | string | yes |

### `giftronaut_create_template`

Save a new template. Image URL is auto-populated server-side to the default Giftronaut logo (custom uploads require the portal).

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `true`, scope: `templates.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `idempotencyKey` | string (≤64) | yes | 24h cache. |
| `name` | string (≤100) | yes | |
| `senderName` | string (≤100) | yes | |
| `message` | string (≤2000) | no | Plain text. |
| `imageAlignment` | string | no | `left` \| `center` \| `right`. Default `center`. |
| `imageSize` | integer | no | Pixel size. Default 40. |
| `borderColor` | string | no | `#RRGGBB`. |
| `buttonColor` | string | no | `#RRGGBB`. |
| `buttonFontColor` | string | no | `#RRGGBB`. |

### `giftronaut_update_template`

PUT-semantics replacement; omitted optional fields revert to server defaults. The `isDefault` flag cannot be changed.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `false`, scope: `templates.write` |

| Name | Type | Required |
|------|------|----------|
| `templateId` | string | yes |
| All other fields | — | mirror `create_template` minus `idempotencyKey`. |

### `giftronaut_delete_template`

Permanent delete. Blocked by two service-level guards:

- `DEFAULT_TEMPLATE` (409) — the org's default template cannot be deleted.
- `TEMPLATE_IN_USE` (409) — at least one active (non-soft-deleted) campaign references the template. Swap the campaigns to a different template or delete them first.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `false`, scope: `templates.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `templateId` | string | yes | |
| `confirm` | string | yes | Must equal `"YES"` exactly. |

## Draft / Execute (2 tools)

The Draft/Execute pattern is a hybrid 2-step confirmation flow that protects users from LLM mistakes on value-moving operations. Use it whenever you want a human (or supervising agent) to review a proposed order before funds move.

### `giftronaut_draft_order`

Create a draft. **No funds move.** Returns a `draftId` and `expiresAt` (typically 1 hour).

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `false`, `destructiveHint`: `false`, `idempotentHint`: `true`, scope: `orders.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `idempotencyKey` | string | yes | |
| `toolName` | string | yes | `branded_card` \| `choice_card` \| `reward_link`. |
| `payloadJson` | string | yes | JSON-serialized payload that the execute step will use. |
| `amountUsd` | decimal | no | Estimated USD spend. Strongly recommended for SpendCap visibility. |

### `giftronaut_execute_order`

Atomically transition the draft from `OPEN` to `EXECUTED` (`SELECT FOR UPDATE`) **and place the real order synchronously** — the success response carries the created `orderId` (use it with `giftronaut_get_order`). If order placement fails, the draft stays `OPEN` so you can fix the payload and retry (`DRAFT_PAYLOAD_INVALID` explains what to correct). Fails with `DRAFT_ACCESS_DENIED` for cross-org access, `DRAFT_EXPIRED` after TTL, `DRAFT_ALREADY_EXECUTED` on double-execute attempts.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `true`, `idempotentHint`: `true`, scope: `orders.write` |

| Name | Type | Required |
|------|------|----------|
| `draftId` | string | yes |
| `idempotencyKey` | string | yes |

## CSV (2 tools)

Pipeline for bulk recipient import. Always call `parse_recipients_csv` first to validate rows and detect duplicates, then feed `parsedRecipients` into `batch_create_recipients`. CSV cells are sanitized for prompt-injection patterns (role markers, `<|im_start|>`-style tokens) and trimmed before validation.

### `giftronaut_parse_recipients_csv`

Read-only preview. Parses a UTF-8 CSV string, validates each row, and returns `{ totalRows, validRows, invalidRows, duplicateInFile, duplicateInAddressBook, errorRows, parsedRecipients }`. Headers recognized (case-insensitive): `email` (required), `firstName` / `first_name` / `이름`, `lastName` / `last_name` / `성`, `company` / `회사`, `department` / `부서`.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `true`, `idempotentHint`: `true`, scope: `recipients.read` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `csvText` | string | yes | UTF-8 CSV text. BOM is stripped automatically; the replacement character `U+FFFD` is rejected as `UNSUPPORTED_ENCODING`. |
| `filePath` | string | no | Caller-side label only. Informational; **not read from disk** by the server. |

Limits (configurable via env): `maxBytes` 1 MB, `maxRows` 5000, `maxColumns` 20. Exceeding any returns `CSV_TOO_LARGE` / `CSV_TOO_MANY_ROWS` / `CSV_TOO_MANY_COLUMNS` with `{limit, actual}` details.

### `giftronaut_batch_create_recipients`

Bulk write. Accepts up to **100** parsed rows per call and returns per-row results: `{ totalRows, createdCount, failedCount, created[{rowIndex,recipientId,email}], failed[{rowIndex,email,code,reason}] }`. Duplicate emails are detected up-front against the recipient list and surface as `DUPLICATE_EMAIL`.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `true`, scope: `recipients.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `idempotencyKey` | string (≤64) | yes | Idempotent across the entire batch — re-sending replays the exact same per-row result map. |
| `recipients` | array | yes | List of `RecipientCsvParsedRow` `{ email, firstName?, lastName?, company?, department? }`. Empty array returns `BATCH_EMPTY`. |

## Credentials (1 tool)

### `giftronaut_create_api_credential`

**Creates sandbox credentials for REST API use only.** The created credential **cannot be used to connect MCP** — MCP is production-only, and production credentials must be created in the developer portal (Apps → Create new app → Production; active immediately for MCP delegation). Attempting to create a production credential through this tool returns `MCP_CREDENTIAL_PROD_BLOCKED`.

| Annotation | Value |
|------------|-------|
| `destructiveHint` | `false`, `idempotentHint`: `false`, `openWorldHint`: `false`, scope: same as caller |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | yes | Human-readable label (e.g. `"My Slack bot"`). |
| `description` | string | no | Optional. |
| `scopes` | array<string> | yes | Requested scopes (e.g. `["orders.read", "balance.read"]`). The result intersects them with the parent AppClient's allowed scopes. |

Returns `{ clientId, clientSecret, scopes, environment, createdAt }`. **`clientSecret` is shown once — never re-fetched.** The audit aspect masks it as `****` in the `audit_events` row before write, so the plaintext never appears in logs.

Bootstrap guard: only callable when the acting parent AppClient is a RFC 7591 DCR client (`MCP_BOOTSTRAP_ONLY` otherwise — additional credentials must be created in the portal).

## Order Limits (1 tool)

### `giftronaut_request_order_limit_increase`

Request an increase to the organization's daily and monthly order spending limits (USD). The new limit is **not applied immediately** — the request is queued for operations-team review (approval typically within 1–2 business days). The caller supplies the current daily/monthly limits as a confirmation check; if they don't match the server's records the request is rejected with `STALE_LIMIT_SNAPSHOT` (409) so the agent can refresh and retry. Each requested limit must exceed the current one.

| Annotation | Value |
|------------|-------|
| `readOnlyHint` | `false`, `destructiveHint`: `false`, `idempotentHint`: `true`, `openWorldHint`: `true`, scope: `orders.write` |

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `idempotencyKey` | string (≤64) | yes | Reuse to safely retry. 24h cache window. |
| `currentDailyLimitUsd` | decimal | yes | Current daily limit in USD, as shown to the user (confirmation check). |
| `currentMonthlyLimitUsd` | decimal | yes | Current monthly limit in USD, as shown to the user (confirmation check). |
| `requestedDailyLimitUsd` | decimal | yes | Requested new daily limit (must exceed current daily limit). |
| `requestedMonthlyLimitUsd` | decimal | yes | Requested new monthly limit (must exceed current monthly limit). |

Returns `{ status: "PENDING_REVIEW", message }`. Mirrors `POST /api/v1/orders/limit-increase-requests` — same validation and Slack approval flow.

## Common Behaviors

### Idempotency Keys

Tools marked `idempotentHint=true` accept an `idempotencyKey`. The 4-tuple `(org_id, env, tool_name, key)` is stored for **24 hours** and the cached response is replayed on retry — even across crashes.

### SpendCap

Every value-moving tool (`create_*_order`, `execute_order`) checks the org's daily USD spend cap before invoking the underlying service. Exceeding it returns:

```json
{ "isError": true, "code": "DAILY_SPEND_CAP_EXCEEDED", "status": 429 }
```

### Tool Kill-Switch

Operators can disable individual tools per environment. Disabled tools return:

```json
{ "isError": true, "code": "TOOL_DISABLED", "status": 503 }
```

### Low-balance hint <a id="low-balance-hint"></a>

Balance and order tools (`get_balance`, `create_*_order`, `draft_order`, `execute_order`) attach an advisory `hints` payload to the SUCCESS envelope when the org's available USD balance falls below `giftronautapi.balance.low-balance-threshold-usd` (default **$50**, configurable via `BALANCE_LOW_THRESHOLD_USD` env var). The shape is:

```json
{
  "data": { /* tool payload */ },
  "meta": { /* ... */ },
  "hints": {
    "suggestedNextActions": [
      "Your available balance is $42.50 USD, below the $50 warning threshold. Consider topping up before placing additional orders.",
      "To top up via ACH bank transfer, call giftronaut_request_ach_top_up (production-only, $100 minimum, 1-3 business day approval). Idempotency key required.",
      "To top up via credit card, the user must sign in to the portal at https://portal.giftronaut.com/marketing/sign-in, then open 'Account Balance' in the left navigation, click 'ADD FUNDS +', and choose 'Credit Card'. Card payments require interactive 3DS / fraud-check and cannot be completed through MCP."
    ],
    "canCompleteNow": true,
    "topUpFlow": "balance.top_up"
  }
}
```

Key properties:
- **Soft signal**: `canCompleteNow=true` — the just-completed tool call DID succeed. This is a forward-looking nudge.
- **Two paths surfaced**: ACH (callable directly via `giftronaut_request_ach_top_up`) and credit card (interactive only, portal-bound).
- **Omitted when healthy**: the `hints` field is `null` (and excluded from JSON via `@JsonInclude(NON_NULL)`) when the balance is above the threshold, so legacy clients see the same response shape as before.
- **Distinct from `INSUFFICIENT_BALANCE` error**: that is a HARD block on a value-moving operation (HTTP 402, `canCompleteNow=false`). This hint is purely informational.

### Rate Limits

MCP is production-only, so production limits apply:

| Bucket | Limit |
|--------|-------|
| Per credential | 60/min |
| Per tool (sub-bucket) | 10/min |
| DCR registration | 5/min/IP |

Exceeding any bucket returns HTTP 429 with `Retry-After`.
