# Transfer between Statira wallets

Statira merchants can send cleared wallet funds directly to other active, approved Statira merchants. Transfers are free and both wallets must use the same currency. A successful transfer is immediately available to the recipient.

## Send or receive

1. Open **Wallet** and copy your **Statira wallet ID**. Share it with the merchant sending you funds. This ID is different from a bank account number.
2. To send, choose **Send to a Statira wallet**, enter the recipient's wallet ID, amount, and optional narration.
3. Review the recipient's business name, wallet ID, amount, and zero fee. Confirm only when these details are correct.
4. Keep the `WTR_…` reference shown on completion. Both merchants can find the transfer in their wallet ledger.

Sending requires the existing wallet payout permission. You cannot transfer to your own wallet, spend reserved funds, exceed your available balance, or send to an inactive or unapproved merchant.

If the connection is interrupted, use **Retry same transfer**. The page preserves the request in the current browser tab, including after a reload. A retry retrieves or completes that same transfer; it does not create a second transfer. Do not start another transfer in another tab or browser until the original outcome is confirmed.

Transfers are available in the merchant dashboard and through the API. Automatic reversals are not included.


## Third-party application integration

Call these endpoints from your server using `X-Public-Key` and `X-Secret-Key`. Never embed a secret key in browser or mobile application code. Use keys for the selected Statira environment; save wallet IDs with their environment and merchant association.

| Method and path | API-key permission | Purpose |
| --- | --- | --- |
| `GET /api/v1/gateway/wallet` | `wallet.view` | Retrieve your wallet ID, currency, and balances |
| `GET /api/v1/gateway/wallet/transfers/recipient/{walletId}` | `wallet.payout` | Confirm the recipient's business name and currency |
| `POST /api/v1/gateway/wallet/transfers` | `wallet.payout` | Send funds |
| `GET /api/v1/gateway/wallet/transfers/{reference}` | `wallet.view` | Retrieve a completed transfer involving your wallet |

### Save the wallet ID

Save `data.walletId` from the wallet response in your application's merchant record. `data.id` remains available and contains the same value for compatibility. This is the Statira wallet identifier, not a bank account number or a processor wallet ID.

Relevant response fields:

```json
{
  "data": {
    "id": "11111111-1111-4111-8111-111111111111",
    "walletId": "11111111-1111-4111-8111-111111111111",
    "merchantId": "22222222-2222-4222-8222-222222222222",
    "currencyCode": "NGN",
    "availableBalanceMinor": 100000
  }
}
```

### Confirm the recipient and send

Recipient lookup returns `data.walletId`, `data.businessName`, and `data.currencyCode`. Show the recipient's name and amount to the person authorizing the payment.

Send this body to `POST /api/v1/gateway/wallet/transfers`, with a unique `Idempotency-Key` header (1–120 characters):

```json
{
  "recipientWalletId": "33333333-3333-4333-8333-333333333333",
  "amountMinor": 50000,
  "narration": "Invoice 123"
}
```

`50000` is ₦500.00 for NGN. There is no transfer fee. The sender is always the merchant authenticated by the API keys; callers cannot select another sender.

A successful response contains these `data` fields:

```json
{
  "reference": "WTR_44444444-4444-4444-8444-444444444444",
  "senderWalletId": "11111111-1111-4111-8111-111111111111",
  "recipientWalletId": "33333333-3333-4333-8333-333333333333",
  "amountMinor": 50000,
  "currencyCode": "NGN",
  "narration": "Invoice 123",
  "status": "SUCCESS"
}
```

Save the transfer reference and both wallet IDs. After a timeout, retry with the **same body and Idempotency-Key**. A completed request returns its original receipt without moving money again. Reusing the key with different details returns HTTP 409. Keys are scoped to the sending wallet, including across different API keys belonging to the same merchant.

Either participating merchant can verify the reference. Other merchants receive HTTP 404. No wallet-transfer webhook is emitted in this version; use the successful response or verification endpoint to confirm completion.

## API contract and rollout

Contract version: 1.0.0 (25 September 2026). Wallet transfers are supported in the merchant dashboard and the API. Use the test environment before integrating with live funds.

- Test base URL: `https://api.dev.paywithstatira.com` (TEST keys).
- Production base URL: `https://api.paywithstatira.com` (LIVE keys).
- Machine-readable contract: [OpenAPI 3.0 JSON](wallet-transfers.openapi.json). Import it into Postman or an OpenAPI-compatible tool.
- API keys must be active, have the permissions listed above, and allow the requesting server's IP address.
- The gateway routes require both `X-Public-Key` and `X-Secret-Key`; a dashboard bearer token or `X-Ecommerce-Key` does not authenticate these routes.

### Request fields

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `recipientWalletId` | UUID string | Yes | Existing Statira wallet belonging to another merchant |
| `amountMinor` | Integer | Yes | Greater than zero; no decimals; must fit available balance and wallet limits |
| `narration` | String or null | No | Maximum 140 characters; normalized by trimming; omitted/null becomes an empty string |
| `Idempotency-Key` header | String | Yes | Nonblank, maximum 120 characters; leading/trailing whitespace is trimmed |

The amount is a signed 64-bit integer on the server. JavaScript clients must also stay within `Number.MAX_SAFE_INTEGER` when using ordinary JSON numbers. Do not send `fee`, `currencyCode`, or a sender ID: the currency and sender come from the wallets and the authenticated key. Transfers debit and credit exactly `amountMinor`.

### Response envelope

All four endpoints return HTTP **200** on success, including creation and idempotent replay. The `data` object is the wallet, recipient, or receipt shown above. A completed receipt has `status: "SUCCESS"`; this API does not return a pending transfer receipt.

```json
{
  "success": true,
  "message": "Wallet transfer completed",
  "data": {
    "reference": "WTR_44444444-4444-4444-8444-444444444444",
    "senderWalletId": "11111111-1111-4111-8111-111111111111",
    "recipientWalletId": "33333333-3333-4333-8333-333333333333",
    "amountMinor": 50000,
    "currencyCode": "NGN",
    "narration": "Invoice 123",
    "status": "SUCCESS"
  },
  "meta": {},
  "correlationId": "example-correlation-id"
}
```

### Errors and recovery

| HTTP status | Meaning / examples | Handling |
| --- | --- | --- |
| 400 | Invalid amount, unavailable recipient, self-transfer, different currencies, inactive/unapproved merchant, insufficient funds, invalid JSON or UUID | Correct the request or account condition. Do not automatically switch to a new request after an uncertain earlier attempt. |
| 401 | Invalid/inactive/revoked keys or keys for another environment | Fix authentication and use the correct environment. |
| 403 | Missing permission or requesting IP not whitelisted | Correct key permissions or the server IP whitelist. |
| 404 | Transfer reference does not exist or is not visible to this merchant | Check the reference, merchant and environment. |
| 409 | Idempotency key reused for different recipient, amount or normalized narration; other data conflicts may also use this status | Recover the original request. Do not replace its key to bypass the conflict. |
| 5xx / timeout / disconnected response | Outcome not confirmed to your application | Retry the same POST with the original key and body. If you have the reference, verify it using GET. |

Example business-error envelope:

```json
{
  "success": false,
  "message": "Insufficient wallet balance",
  "data": null,
  "meta": {},
  "correlationId": "example-correlation-id"
}
```

Validation errors may include details in `meta`. HTTP status is authoritative; there is no `statusCode` field in this envelope. Preserve `correlationId` for support and diagnostics. Proxies can return non-JSON error responses, so handle those as well.

### Example server-side request

Set `STATIRA_BASE_URL`, `STATIRA_PUBLIC_KEY` and `STATIRA_SECRET_KEY` in your server environment. Use a new key for each intended business transfer and persist it **before** making the first request:

```bash
curl --request POST "$STATIRA_BASE_URL/api/v1/gateway/wallet/transfers" \
  --header "X-Public-Key: $STATIRA_PUBLIC_KEY" \
  --header "X-Secret-Key: $STATIRA_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: invoice-123-wallet-transfer-1' \
  --data '{"recipientWalletId":"33333333-3333-4333-8333-333333333333","amountMinor":50000,"narration":"Invoice 123"}'
```

### Developer acceptance checks

- Retrieve and save the sender wallet ID; obtain the recipient wallet ID from that merchant.
- Confirm the recipient name and matching currency before authorizing a transfer.
- Send a TEST transfer and verify the reference using both participating merchants' keys.
- Repeat the same request/key and confirm the receipt is unchanged and no second transfer is created.
- Reuse that key with a different amount and expect 409.
- Check rejection of self-transfer, zero/negative amounts, insufficient funds, and unauthorized keys.
- Confirm an unrelated merchant cannot read the receipt.
- Simulate a lost response and recover with the original key/body rather than initiating a new transfer.


## E-commerce connection keys

The same transfer flow is available using `X-Ecommerce-Key: <opaqueKey>`:

- GET `/api/v1/ecommerce/wallet` — save `data.walletId` (`wallet.view`).
- GET `/api/v1/ecommerce/wallet/transfers/recipient/{walletId}` — confirm the recipient (`wallet.payout`).
- POST `/api/v1/ecommerce/wallet/transfers` — send the same body and `Idempotency-Key` as the gateway API (`wallet.payout`).
- GET `/api/v1/ecommerce/wallet/transfers/{reference}` — retrieve the receipt (`wallet.view`).

E-commerce requests inherit the connected merchant API key’s permissions, IP restrictions, and environment. The sender is derived from the authenticated connection, never from a merchant ID in your request. Transfers remain free and use the same atomic balance and duplicate-request safeguards. Idempotency keys are shared at the sender-wallet level across gateway and e-commerce routes.

See [Generate and use e-commerce keys](ecommerce-keys.md) for key generation and connection setup.
