> ## 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.

# Recipients and destinations setup and configuration

> Rate limits shared across eleven operations, what Sakawave stores, and designing around a 24-hour window.

## Credentials

API key only, as everywhere in the Business API. Every recipient and destination is scoped to the key's business; there is no cross-business lookup and no way to reference another business's recipient.

## Rate limits

All eleven operations share **120 requests per minute**, counted once against your business and once against the key. The limit is declared on the controller, so a payout run competes with itself: creating a recipient, adding a detail, and resolving a destination all draw on the same allowance, as do transfers and FX quotes on the same controller.

A payout batch therefore costs roughly:

| Pattern | Calls per payee |
| - | -: |
| Saved recipient, already exists | 1 (destination) + 1 (transfer) |
| Saved recipient, first time | 3 + 1 |
| Inline | 1 + 1 |

At 120 per minute, a run of 500 saved payees needs about eight and a half minutes of steady pacing. Build the pacing in; do not discover it as `429`s.

## What Sakawave stores

| Value | At rest | Ever returned |
| - | - | - |
| `fields` — account numbers, IBANs, phone numbers | Encrypted | No. Only `maskedIdentifier`, the last four characters |
| `accountHolderName`, `institutionName` | Plain | Yes |
| Verified account name | Plain | Yes, as the destination `displayName` |
| Recipient name, email, address | Plain | Yes |
| Which provider settles a route | Yes | No |

Two consequences for your side of the integration:

1. **You cannot read routing values back.** If your UI must show a customer the account number they saved, keep your own copy under your own controls — Sakawave will not return it.
2. **Correcting a value means replacing it.** There is no update endpoint. Add a new payment detail and delete the old one.

## The 24-hour window

A destination expires 24 hours after creation and is consumed by the transfer that spends it. Both produce the same error:

```json theme={null}
{
  "statusCode": 409,
  "message": "The transfer destination has expired. Verify it again.",
  "code": "transfer_destination_expired"
}
```

<Warning>
  An expired destination and an already-spent one are indistinguishable. If your retry logic re-sends a transfer with a stored `destinationId`, a genuine duplicate and a stale destination will look the same. Resolve a fresh destination for every attempt, and rely on the transfer's own `externalReference` for idempotency.
</Warning>

The pattern that works:

```ts theme={null}
async function payOut(recipientId: string, amount: string, reference: string) {
  // Fresh destination per attempt. They are cheap and single-use by design.
  const destination = await post(`/v1/transfer-destinations/recipients/${recipientId}`, {});
  return post('/v1/transfers', {
    destinationId: destination.id,
    externalReference: reference,   // this is what makes the retry safe
    // …amounts
  });
}
```

The one that does not: storing a `destinationId` on your payout row and reusing it on retry.

## Defaults

Every recipient with any payment details has exactly one default.

* The **first** detail added becomes the default whatever `isDefault` says.
* Setting `isDefault` on a later one moves the default.
* Deleting the default **promotes another automatically**; you cannot end up with details and no default.
* There is no way to clear a default.

Creating a destination without naming a `paymentDetailId` uses the default. If your recipients hold several details, name the one you mean rather than depending on which is current.

## No events

Recipients, payment details, and destinations emit no webhooks. Nothing tells you a destination expired, and nothing tells you a detail became provider-locked. Both surface as errors at the point of use.

## Legacy provider-locked details

A payment detail whose routing values live at the provider rather than with Sakawave carries `providerLocked: true`. Sakawave tries to resolve it when you use it; when it cannot, you get `409 payment_detail_provider_locked` and the only fix through this API is to delete the detail and add it again with its routing fields.

Check for it proactively if you hold long-lived recipients:

```bash theme={null}
curl --silent --url "$SAKAWAVE_BASE_URL/v1/transfer-recipients" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
| jq '[.[] | {id, displayName, locked: [.paymentDetails[] | select(.providerLocked)] | length} | select(.locked > 0)]'
```

## Configuration checklist

* [ ] `fields` built from route descriptors at runtime, never hardcoded
* [ ] A fresh destination resolved per transfer attempt, never reused
* [ ] Transfer idempotency carried by `externalReference`, not by the destination
* [ ] Your own copy kept of any routing value your UI must display
* [ ] Payout batches paced under 120 calls per minute across all eleven operations
* [ ] `displayName` from the destination shown for approval, since verification may have changed it
* [ ] A check for `providerLocked` details if your recipients predate the current routing model


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