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

# Value-Added Services

> Bulk SMS, USD virtual cards, and gift cards, every non-bill service Hyparrow resells, with the pricing rules that apply to each.

## Overview

Alongside [bill payments](/bills), Hyparrow resells three value-added services. All
three debit the same Hyparrow wallet, follow the same envelope, and use the same
API keys.

| Service                  | Base route                            | Priced in                | Status                   |
| ------------------------ | ------------------------------------- | ------------------------ | ------------------------ |
| Bulk SMS                 | `/sms/*`                              | NGN                      | Live                     |
| USD wallet               | `/usd-wallet/*`                       | NGN in, USD held         | Live                     |
| USD virtual cards        | `/usd-cards/*`                        | USD, from the USD wallet | Live                     |
| Gift card purchase       | `/gift-cards`, `/gift-cards/purchase` | USD, charged in NGN      | Live                     |
| Gift card trading (sell) | `/gift-cards/trades/*`                | n/a                      | **Unavailable upstream** |

<Note>
  **Base URL:** `https://api.hyparrow.cloud/api/v1`

  **Auth:** every request requires `X-API-Key` and `X-API-Secret` headers.

  **Sandbox:** point at `https://sandbox.hyparrow.cloud/api/v1` with your `pk_test_`
  key. Amounts ending in `.01` force a failure and `.99` force a pending result, so
  you can exercise the refund paths without real money.
</Note>

<Warning>
  **USD cards are a separate product from [virtual cards](/virtual-cards).** The
  `/virtual-cards` endpoints issue NGN cards through a different provider and keep
  local records. The `/usd-cards` endpoints below issue USD cards held entirely by
  the upstream provider. The two share no state, and a `cardId` from one is
  meaningless to the other.
</Warning>

## Response envelope

Every endpoint on this page returns the standard Hyparrow shape. A request that
reaches the provider and is declined returns HTTP 200 with `success: false`, the
wallet is refunded automatically in that case.

```json theme={null}
{
  "success": true,
  "data": { },
  "error": ""
}
```

***

## Bulk SMS

<Warning>
  **Promotional use only.** This channel uses a shared sender ID with no
  registration step, which is what makes it cheap. Three consequences you must
  design around:

  * Messages deliver only between **08:00 and 20:00**.
  * Numbers on the **DND register may never receive**.
  * The sender header shown to the recipient **is not guaranteed** to be the one you
    requested.

  Do not route OTPs, password resets, or transaction alerts through it.
</Warning>

### How billing works

SMS is billed per **unit**, where one unit is one message *part* to one recipient.
A single-part message to 500 recipients is 500 units; a two-part message to the
same list is 1,000 units.

Part length depends on the characters used:

| Encoding           | First part | Each part when concatenated |
| ------------------ | ---------- | --------------------------- |
| GSM-7 (plain text) | 160 chars  | 153 chars                   |
| UCS-2              | 70 chars   | 67 chars                    |

A **single** non-GSM character switches the entire message to UCS-2. The usual
culprits are smart quotes pasted from a document (`'` rather than `'`), emoji, and
the Naira sign `₦`. A 90-character message costs one unit as plain text and two if
it contains one curly apostrophe.

### Quote before you send

`POST /sms/quote` prices a campaign without sending it. Use it to show the real
cost in your UI before the merchant commits.

```bash Quote a campaign theme={null}
curl -X POST https://api.hyparrow.cloud/api/v1/sms/quote \
  -H "X-API-Key: $HYPARROW_KEY" \
  -H "X-API-Secret: $HYPARROW_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Your order is ready for pickup.",
    "recipients": ["08012345678", "08087654321"]
  }'
```

```json Response theme={null}
{
  "success": true,
  "data": {
    "recipients": 2,
    "messageParts": 1,
    "billableUnits": 2,
    "unitPriceKobo": 800,
    "chargeKobo": 1600,
    "encoding": "gsm7",
    "note": ""
  },
  "error": ""
}
```

When `encoding` comes back as `ucs2`, the `note` field explains why and what to
change, surface it to the merchant rather than silently charging double.

### Send

`POST /sms/send` charges the wallet and dispatches.

<ParamField body="senderName" type="string" required>
  Sender header, **11 characters maximum**. Longer values are rejected rather than
  silently truncated by the carrier.
</ParamField>

<ParamField body="message" type="string" required>
  The message body.
</ParamField>

<ParamField body="recipients" type="string[]">
  Recipient phone numbers. Duplicates are removed before billing.
</ParamField>

<ParamField body="phoneNumbers" type="string">
  Comma-separated alternative to `recipients`, for lists coming straight out of a
  spreadsheet. You may send either or both.
</ParamField>

<ParamField body="requestRef" type="string">
  Your own reference. One is generated if you omit it.
</ParamField>

```bash Send a campaign theme={null}
curl -X POST https://api.hyparrow.cloud/api/v1/sms/send \
  -H "X-API-Key: $HYPARROW_KEY" \
  -H "X-API-Secret: $HYPARROW_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "senderName": "HemPay",
    "message": "Your order is ready for pickup.",
    "phoneNumbers": "08012345678,08087654321",
    "requestRef": "SMS_1001"
  }'
```

```json Response theme={null}
{
  "success": true,
  "data": {
    "service": "bulk_sms",
    "reference": "SMS_1001",
    "recipients": 2,
    "billableUnits": 2,
    "chargeKobo": 1600,
    "status": "success",
    "deliveryWindow": "08:00-20:00",
    "dndWarning": "Numbers on the DND register may not receive this message."
  },
  "error": ""
}
```

***

## USD virtual cards

Dollar-denominated Visa and Mastercard virtual cards, paid for from your **USD
wallet**. Convert Naira into that wallet first (`POST /usd-wallet/quote` then
`/usd-wallet/convert`), then create and fund cards from the dollars you hold.

A card action that exceeds your USD balance is rejected before anything is
charged, with the exact shortfall in the response.

<Note>
  Hyparrow does **not** store cards. The provider owns the `cardId` and the balance.
  Persist the `cardId` from the creation response, if you lose it, the only way back
  is your own transaction history.
</Note>

### Fees

`GET /usd-cards/fees` returns the live schedule, so you never hardcode these.

| Fee                   | Amount                                             |
| --------------------- | -------------------------------------------------- |
| Card creation         | \$5                                                |
| Card funding          | 1% of the funded amount                            |
| Withdrawal to wallet  | 1% of the withdrawn amount                         |
| Cross-border purchase | \$1 + 3.5%                                         |
| Insufficient balance  | \$1                                                |
| Minimum funding       | \$5                                                |
| Minimum withdrawal    | \$5                                                |
| NGN conversion spread | 2.5%, applied when you convert into the USD wallet |

<Warning>
  **The cross-border fee is charged after the fact, not at funding time.** The card
  settles in USD. When the cardholder buys from a merchant priced in another
  currency, a UK site charging GBP, a Nigerian merchant charging NGN, the card
  network converts and charges for it. That fee is deducted from the **card
  balance** once the purchase settles.

  If the balance can't cover it, the attempt fails and the \$1 insufficient-balance
  fee applies instead. Fund cards with headroom above the intended spend, and warn
  merchants that a card funded to exactly the purchase price will often decline.
</Warning>

### Create a card

The issuer needs a full billing identity for the cardholder, not just a name and a
KYC number. Every field below marked required is enforced before your USD wallet
is touched, so an incomplete cardholder costs you a `400`, never a charge.

<ParamField body="firstName" type="string" required />

<ParamField body="lastName" type="string" required />

<ParamField body="phone" type="string" required>Cardholder phone number.</ParamField>
<ParamField body="email" type="string" required>Cardholder contact email.</ParamField>
<ParamField body="dob" type="string" required>Date of birth, `YYYY-MM-DD`. An ISO 8601 timestamp is accepted and truncated to the date.</ParamField>
<ParamField body="address" type="string" required>Street address.</ParamField>
<ParamField body="state" type="string" required>State or region.</ParamField>

<ParamField body="city" type="string" required />

<ParamField body="kycType" type="string" required>`NIN` or `BVN`.</ParamField>
<ParamField body="kycNo" type="string" required>The ID document number.</ParamField>
<ParamField body="amount" type="number" required>First funding amount in USD. Minimum \$5.</ParamField>
<ParamField body="brand" type="string">`VISA` (default) or `MASTERCARD`.</ParamField>

```bash Create and fund a card theme={null}
curl -X POST https://api.hyparrow.cloud/api/v1/usd-cards \
  -H "X-API-Key: $HYPARROW_KEY" \
  -H "X-API-Secret: $HYPARROW_SECRET" \
  -H "Idempotency-Key: card-create-8f21" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Yusuf",
    "lastName": "Ibrahim",
    "phone": "08031234567",
    "email": "yusuf@example.com",
    "dob": "1995-04-12",
    "address": "24 Adeola Odeku Street",
    "state": "Lagos",
    "city": "Victoria Island",
    "kycType": "NIN",
    "kycNo": "12345678901",
    "amount": 10,
    "brand": "VISA"
  }'
```

```json Response theme={null}
{
  "success": true,
  "data": {
    "service": "usd_card_create",
    "reference": "CARD1754...",
    "cardId": "vc-123456",
    "brand": "VISA",
    "fundedUSD": 10,
    "creationFeeUSD": 5,
    "fundingFeeUSD": 0.1,
    "totalUSD": 15.1,
    "chargeKobo": 2321500,
    "fxRate": 1500.0
  },
  "error": ""
}
```

<Tip>
  Card creation and funding both move money and are not idempotent upstream. Send an
  `Idempotency-Key` header so a retried request cannot issue or fund twice.
</Tip>

### Your cards

`GET /usd-cards` lists every card you have been charged to create, newest first,
derived from your USD ledger. Topupmate cannot enumerate what it issued, so the
charge is the record; each creation writes its card id onto the debit that paid
for it. You do not need to keep a list of your own.

Balance and status are not included. They belong to the provider, are read live
per card, and a copy here would be a stale figure shown as spendable. A row under
`unrecorded` is a creation charged for before its id reached the ledger: a real
card you own whose id must come from the provider's dashboard, matched by
reference.

### When you lose the response

Issuance is not idempotent and Topupmate cannot list the cards it has issued, so a
creation whose response you never received is the one failure you cannot retry
your way out of: the card may be real and charged for, and a second attempt buys
a second card.

Do not retry. Ask what the reference bought:

```bash Resolve a creation you never got an answer for theme={null}
curl https://api.hyparrow.cloud/api/v1/usd-cards/by-reference/CARD1754... \
  -H "X-API-Key: $HYPARROW_KEY" \
  -H "X-API-Secret: $HYPARROW_SECRET"
```

```json Response theme={null}
{
  "success": true,
  "data": {
    "reference": "CARD1754...",
    "charged": true,
    "cardId": "vc-123456",
    "brand": "VISA",
    "amountUSD": 15.1
  },
  "error": ""
}
```

| `charged` | `cardId` | What it means                                              |
| --------- | -------- | ---------------------------------------------------------- |
| `false`   | empty    | Nothing was debited. Safe to retry.                        |
| `true`    | set      | The card exists. Store the id; do not retry.               |
| `true`    | empty    | Money moved and no card id was recorded. Raise it with us. |

This is a read. It is safe to call as often as you like, and safe to call on any
timeout, `502`, or dropped connection.

### Card lifecycle

| Action                     | Endpoint                               |
| -------------------------- | -------------------------------------- |
| List the cards you created | `GET /usd-cards`                       |
| Get credentials + balance  | `GET /usd-cards/{cardId}`              |
| List transactions          | `GET /usd-cards/{cardId}/transactions` |
| Fund                       | `POST /usd-cards/{cardId}/fund`        |
| Withdraw to wallet         | `POST /usd-cards/{cardId}/withdraw`    |
| Freeze                     | `POST /usd-cards/{cardId}/freeze`      |
| Unfreeze                   | `POST /usd-cards/{cardId}/unfreeze`    |
| Terminate                  | `POST /usd-cards/{cardId}/terminate`   |
| Replace a terminated card  | `POST /usd-cards/{cardId}/replace`     |

`GET /usd-cards/{cardId}/transactions` accepts `startDate`, `endDate`
(`YYYY-MM-DD`) and `page`. Omit the dates and it returns the trailing 30 days.

Funding and withdrawal both take `{ "amount": <usd>, "requestRef": "..." }`.
Withdrawal deducts its 1% fee from the withdrawn amount and credits the remainder
to your NGN wallet at the credit-side rate.

**Terminating a card credits any residual balance back to your wallet** with no
withdrawal fee, since the closure forced the balance out rather than the merchant
choosing to withdraw it.

***

## Gift cards

Purchasable gift cards across many countries and brands, delivered to a recipient
by email.

### Two currencies per product

Every product has a **sender** currency (what you're quoted and charged in, always
USD) and a **recipient** currency (what the card is denominated in, EUR for an
Italian product, GBP for a UK one). Quote from the sender side. The `amount` you
send at purchase is the face value in the recipient currency.

### Denomination rules

Products are either `FIXED` or `RANGE`:

* **`FIXED`**, only the listed `denominations` are purchasable. Any other amount
  is rejected before your wallet is touched.
* **`RANGE`**, any amount between `minAmount` and `maxAmount`.

### Browse the catalog

`GET /gift-cards` requires at least one of `countryCode` or `productName`, the
unfiltered catalog runs to hundreds of products and is not returned.

```bash Browse US gift cards theme={null}
curl "https://api.hyparrow.cloud/api/v1/gift-cards?countryCode=US" \
  -H "X-API-Key: $HYPARROW_KEY" \
  -H "X-API-Secret: $HYPARROW_SECRET"
```

```json Response theme={null}
{
  "success": true,
  "count": 2,
  "data": [
    {
      "productId": "18285",
      "productName": "Walmart USA US",
      "countryCode": "US",
      "senderCurrency": "USD",
      "recipientCurrency": "USD",
      "denominationType": "RANGE",
      "minAmount": 5,
      "maxAmount": 100,
      "available": true
    },
    {
      "productId": "14971",
      "productName": "Google Play Italy",
      "countryCode": "IT",
      "senderCurrency": "USD",
      "recipientCurrency": "EUR",
      "denominationType": "FIXED",
      "denominations": [
        { "amount": 5, "priceUSD": 6.23, "markupUSD": 0.18 },
        { "amount": 10, "priceUSD": 12.47, "markupUSD": 0.36 }
      ],
      "available": true
    }
  ],
  "error": ""
}
```

<Note>
  `priceUSD` on each denomination is the **final price**, inclusive of the provider's
  sender fee and Hyparrow's markup. It is what your wallet will be charged, converted
  to NGN. Display that number, never compute your own.
</Note>

### Purchase

<Warning>
  **Echo the filter you browsed with.** The upstream catalog is paginated, 2,383
  products at 200 per page, so a product can only be located inside the slice it was
  listed in. Send back the same `countryCode` (or `productName`) you used on
  `GET /gift-cards`. Omit it and only products on the first page of the global
  catalog can be resolved, which is under 10% of them.
</Warning>

```bash Buy a gift card theme={null}
curl -X POST https://api.hyparrow.cloud/api/v1/gift-cards/purchase \
  -H "X-API-Key: $HYPARROW_KEY" \
  -H "X-API-Secret: $HYPARROW_SECRET" \
  -H "Idempotency-Key: gc-9911" \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "18285",
    "countryCode": "US",
    "amount": 50,
    "units": 1,
    "email": "recipient@example.com",
    "sender": "HemPay",
    "requestRef": "GC_PURCHASE_101"
  }'
```

The product is re-fetched and validated before your wallet is touched. A face value
that isn't valid for a `FIXED` product, or falls outside a `RANGE` product's
limits, is rejected with a `400` and **no charge**, rather than failing upstream
after the debit.

The redeem code and instructions are emailed to `email` and echoed in
`providerResponse` on the success payload.

### Gift card trading is unavailable

The sell-side endpoints (`GET`/`POST /gift-cards/trades`,
`POST /gift-cards/trades/respond`, `GET /gift-cards/trades/{reference}`) are
implemented and routed, but the upstream provider currently has the trade service
switched off. They return **HTTP 503** with a clear message and charge nothing.

Do not surface a sell flow in your UI until this page says otherwise. Gift card
*purchase* is unaffected.

***

## Route index

Every Topupmate-backed endpoint Hyparrow exposes, in one place. Dashboard
equivalents exist under `/dashboard/*` with JWT auth instead of API keys.

### Bills, see [Bill Payments](/bills)

| Method | Route                                     |
| ------ | ----------------------------------------- |
| `GET`  | `/bills/topup/services?service=<catalog>` |
| `POST` | `/bills/topup`                            |
| `POST` | `/bills/topup/validate`                   |
| `POST` | `/bills/topup/exampin`                    |
| `POST` | `/bills/topup/rechargepin`                |
| `POST` | `/bills/topup/datapin`                    |
| `GET`  | `/bills/status`                           |

### Bulk SMS

| Method | Route        |
| ------ | ------------ |
| `POST` | `/sms/quote` |
| `POST` | `/sms/send`  |

### USD virtual cards

| Method | Route                              |
| ------ | ---------------------------------- |
| `GET`  | `/usd-wallet`                      |
| `POST` | `/usd-wallet/quote`                |
| `POST` | `/usd-wallet/convert`              |
| `GET`  | `/usd-wallet/transactions`         |
| `GET`  | `/usd-cards/fees`                  |
| `POST` | `/usd-cards`                       |
| `GET`  | `/usd-cards/{cardId}`              |
| `GET`  | `/usd-cards/{cardId}/transactions` |
| `POST` | `/usd-cards/{cardId}/fund`         |
| `POST` | `/usd-cards/{cardId}/withdraw`     |
| `POST` | `/usd-cards/{cardId}/freeze`       |
| `POST` | `/usd-cards/{cardId}/unfreeze`     |
| `POST` | `/usd-cards/{cardId}/terminate`    |
| `POST` | `/usd-cards/{cardId}/replace`      |

### Gift cards

| Method | Route                                    |
| ------ | ---------------------------------------- |
| `GET`  | `/gift-cards?countryCode=&productName=`  |
| `POST` | `/gift-cards/purchase`                   |
| `GET`  | `/gift-cards/trades` *(503)*             |
| `POST` | `/gift-cards/trades` *(503)*             |
| `POST` | `/gift-cards/trades/respond` *(503)*     |
| `GET`  | `/gift-cards/trades/{reference}` *(503)* |
