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

# Make a transfer

> Resolve a destination, quote or preview the transfer, create it idempotently, and monitor completion.

The safest transfer workflow persists intent before making an API call. Store your unique `externalReference`, source account, destination, amount, and currency together.

## 1. Create a destination

For an own-account transfer:

```bash theme={null}
DESTINATION=$(curl --silent --show-error --fail-with-body \
  --request POST \
  --url "$SAKAWAVE_BASE_URL/v1/transfer-destinations/own" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{
    \"accountId\": \"$DESTINATION_ACCOUNT_ID\",
    \"destinationCurrency\": \"NGN\"
  }")

DESTINATION_ID=$(printf '%s' "$DESTINATION" | jq --raw-output '.id')
```

Destinations expire. Create a new one if the returned `expiresAt` has passed.

For an external payout, first call `GET /v1/transfer-corridors?currency=KES&country=KE`. Render the returned field descriptors, then either create an inline destination or save a recipient and payment detail for reuse. Provider selection stays internal to Sakawave.

```bash theme={null}
curl --request POST \
  --url "$SAKAWAVE_BASE_URL/v1/transfer-destinations/recipients/$RECIPIENT_ID" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"paymentDetailId\": \"$PAYMENT_DETAIL_ID\"}"
```

## 2. Create an FX quote when needed

Skip this step when source and destination currencies match. Otherwise, send exactly one of `sourceAmount` or `destinationAmount`, matching `amountSide`.

```bash theme={null}
curl --request POST \
  --url "$SAKAWAVE_BASE_URL/v1/fx-quotes" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{
    \"sourceAccountId\": \"$SOURCE_ACCOUNT_ID\",
    \"destinationId\": \"$DESTINATION_ID\",
    \"sourceCurrency\": \"NGN\",
    \"destinationCurrency\": \"USD\",
    \"amountSide\": \"source\",
    \"sourceAmount\": \"2500000\"
  }"
```

Use the quote before `expiresAt`. Supply its `id` as `fxQuoteId` when previewing and creating the matching transfer.

## 3. Preview fees and total debit

```bash theme={null}
curl --request POST \
  --url "$SAKAWAVE_BASE_URL/v1/transfers/preview" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{
    \"sourceAccountId\": \"$SOURCE_ACCOUNT_ID\",
    \"destinationId\": \"$DESTINATION_ID\",
    \"sourceCurrency\": \"NGN\",
    \"destinationCurrency\": \"NGN\",
    \"amountSide\": \"source\",
    \"sourceAmount\": \"2500000\"
  }"
```

Confirm `fee`, `totalDebit`, currencies, and destination with your user before creating the transfer. Request amounts use minor units; preview amounts are returned as decimal strings.

## 4. Create the transfer

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url "$SAKAWAVE_BASE_URL/v1/transfers" \
    --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
    --header "Content-Type: application/json" \
    --header "X-Request-ID: transfer_payout_20260814_001" \
    --data "{
      \"sourceAccountId\": \"$SOURCE_ACCOUNT_ID\",
      \"destinationId\": \"$DESTINATION_ID\",
      \"sourceCurrency\": \"NGN\",
      \"destinationCurrency\": \"NGN\",
      \"amountSide\": \"source\",
      \"sourceAmount\": \"2500000\",
      \"externalReference\": \"payout_20260814_001\",
      \"narration\": \"Invoice 1001\"
    }"
  ```

  ```ts Node.js theme={null}
  const input = {
    sourceAccountId: process.env.SOURCE_ACCOUNT_ID,
    destinationId: process.env.DESTINATION_ID,
    sourceCurrency: 'NGN',
    destinationCurrency: 'NGN',
    amountSide: 'source',
    sourceAmount: '2500000',
    externalReference: 'payout_20260814_001',
    narration: 'Invoice 1001',
  };

  const response = await fetch(`${process.env.SAKAWAVE_BASE_URL}/v1/transfers`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SAKAWAVE_API_KEY}`,
      'Content-Type': 'application/json',
      'X-Request-ID': `transfer_${input.externalReference}`,
    },
    body: JSON.stringify(input),
  });
  const transfer = await response.json();
  if (!response.ok) throw new Error(`${response.status}: ${transfer.code} — ${transfer.message}`);
  console.log(transfer.id, transfer.status);
  ```

  ```python Python theme={null}
  import os
  import requests

  payload = {
      "sourceAccountId": os.environ["SOURCE_ACCOUNT_ID"],
      "destinationId": os.environ["DESTINATION_ID"],
      "sourceCurrency": "NGN",
      "destinationCurrency": "NGN",
      "amountSide": "source",
      "sourceAmount": "2500000",
      "externalReference": "payout_20260814_001",
      "narration": "Invoice 1001",
  }
  response = requests.post(
      f"{os.environ['SAKAWAVE_BASE_URL']}/v1/transfers",
      json=payload,
      headers={
          "Authorization": f"Bearer {os.environ['SAKAWAVE_API_KEY']}",
          "X-Request-ID": f"transfer_{payload['externalReference']}",
      },
      timeout=30,
  )
  transfer = response.json()
  if not response.ok:
      raise RuntimeError(f"{response.status_code}: {transfer.get('code')} — {transfer.get('message')}")
  print(transfer["id"], transfer["status"])
  ```
</CodeGroup>

Reusing `externalReference` returns the existing transfer. It does not validate that a later payload matches the original, so never reuse a reference for a different payment.

## 5. Handle the lifecycle

```mermaid theme={null}
stateDiagram-v2
    [*] --> awaiting_approval: policy requires approval
    [*] --> submitting: no approval required
    awaiting_approval --> submitting: approved in dashboard
    awaiting_approval --> rejected
    submitting --> pending
    submitting --> completed
    submitting --> failed
    pending --> completed
    pending --> failed
    completed --> reversed
```

API keys can initiate transfers but cannot approve or reject them. Handle statuses as follows:

* `awaiting_approval`: a human approver must act through an approved non-API-key flow.
* `submitting`: transient; poll with backoff if necessary. This status is not published as a webhook event.
* `pending`: accepted but not final.
* `completed`: final success unless a later reversal occurs.
* `failed`, `rejected`, `reversed`: do not mark paid; inspect `failureCode` and `failureReason`.

Use signed webhooks for state changes and periodic retrieval as a recovery path. Then [reconcile the transaction](/tutorials/reconcile-transactions).


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