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

# Routing fields and verification

> Building a payment payload from a route's field descriptors, what gets verified, and how the two field styles differ.

This is the recipients-specific integration guide. Getting `fields` right is most of the work in this service, and the API is built so you never have to know a country's banking conventions yourself.

## Never hardcode a country's fields

Every route publishes exactly what it needs. Read the descriptors and render them.

```ts theme={null}
const [corridor] = await get('/v1/transfer-corridors?currency=KES&country=KE');

for (const descriptor of corridor.route.fieldDescriptors) {
  render({
    name: descriptor.key,                 // the key to send inside `fields`
    label: descriptor.label,
    kind: descriptor.type,                // 'text' | 'select' | 'tel'
    required: descriptor.required,
    minLength: descriptor.minLength,
    maxLength: descriptor.maxLength,
    pattern: descriptor.pattern,
    placeholder: descriptor.placeholder,
    options: descriptor.options,          // present for 'select'
  });
}
```

A form built this way keeps working when a route adds a field, changes a bank list, or tightens a pattern. A form built from a hardcoded list starts failing silently — the extra field is not sent, and the payout is refused.

## The two field styles

The same routing values are sent two different ways depending on which endpoint you use. This is the single most common source of `400`s here.

| | Saved payment detail | Inline destination |
| - | - | - |
| Endpoint | `POST /v1/transfer-recipients/{id}/payment-details` | `POST /v1/transfer-destinations` |
| Where routing goes | Inside a `fields` object | **Top level**, beside `routeId` |
| Keys | Exactly the route's descriptor keys | A fixed set of known field names |
| Unknown key | `422 payment_detail_invalid`, `unrecognized_field` | `400 validation_failed` — the schema is strict |

```jsonc theme={null}
// Saved: keyed by the route's descriptors
{ "routeId": "01K…", "currency": "KES", "fields": { "bankCode": "001", "accountNumber": "0123456789" } }

// Inline: the same values, one level up
{ "type": "external", "routeId": "01K…", "currency": "KES", "bankCode": "001", "accountNumber": "0123456789" }
```

The inline shape accepts a fixed vocabulary: `accountNumber`, `routingNumber`, `sortCode`, `iban`, `swiftBic`, `bankCode`, `branchCode`, `bankInstitutionNumber`, `transitNumber`, `accountType`, `phoneNumber`, `bsb`, `institutionName`, and `country`. A route requiring something outside that list can only be used through a saved payment detail.

## What validation checks

Saved details are validated against the route's descriptors, and **every problem comes back at once**:

| `errors[].code` | Meaning |
| - | - |
| `required` | A descriptor marked required has no value |
| `too_small` | Shorter than `minLength` |
| `too_big` | Longer than `maxLength` |
| `invalid_format` | Fails the descriptor's `pattern` |
| `unrecognized_field` | A key the route does not define |
| `route_country_mismatch` | The route does not serve the country you named |

```json theme={null}
{
  "statusCode": 422,
  "message": "Payment detail validation failed.",
  "code": "payment_detail_invalid",
  "errors": [
    { "field": "accountNumber", "path": ["fields", "accountNumber"], "code": "too_small", "message": "accountNumber is too short." },
    { "field": "swiftBic", "path": ["fields", "swiftBic"], "code": "unrecognized_field", "message": "swiftBic is not accepted for this route." }
  ]
}
```

Map `errors[].field` straight back onto your form. Every entry names the field and the reason, so three bad inputs are three highlighted boxes rather than three round trips.

## Institution directories

A route with `institutionDirectoryEnabled: true` resolves institution codes to canonical names for you.

* Present the `options` from the `select` descriptor.
* Send the **code**, in `fields`.
* Sakawave fills in `institutionName` on the stored detail.

Do not ask the user to type a bank name on such a route, and do not bother sending `institutionName` yourself — it is resolved from the code.

## Country, and when it is required

`country` is required for a bank-transfer route that is not already scoped to one country. A route with `destinationCountry` set already knows; a scheme route like `sepa` or `cemac` does not.

| Situation | Result |
| - | - |
| Route has `destinationCountry` | `country` optional, and ignored if it disagrees with the route's own |
| Bank-transfer scheme route, no `country` sent | `422 payment_detail_invalid` (saved) or `409 destination_country_required` (inline) |
| `country` the route does not serve | `route_country_mismatch` |

The simplest rule: always send `country` when you know it.

## Verification changes the name

Where a route supports account lookup, Sakawave asks the provider who owns the account and **replaces the name you sent**. The destination's `displayName` is the verified name, not your `accountName`.

```jsonc theme={null}
// You sent
{ "accountName": "ada okafor" }

// You get back
{ "displayName": "ADA OKAFOR" }
```

Two things follow:

1. **Show it before confirming a payment.** A name that comes back materially different means the account number is wrong or belongs to someone else. This check costs nothing and catches both typos and misdirection.
2. **Do not assume it matches your records exactly.** Banks return their own formatting — case, ordering, middle names. Compare loosely and review the near-misses rather than failing them.

Not every route verifies. Where none is available, `displayName` is the name you sent, unchanged — so never treat its presence alone as proof of anything.

## The counterparty profile

Separately from routing, international payout rails require a profile of who is being paid:

| Saved recipient | Inline destination |
| - | - |
| `relationship` | `recipientRelationship` |
| `email` | `recipientEmail` |
| `address.line1` | `recipientAddressLine1` |
| `address.city` | `recipientCity` |
| `address.country` | `country` |

For a saved recipient these are **required at creation**, so the failure comes early. Inline they are optional to the parser and required by the rail, so the failure comes late — at destination time, as `422 recipient_profile_incomplete` listing each missing field.

Domestic NGN routes do not ask for the profile at all, which is why an integration built only against NGN can be surprised by its first international payout.

## A checklist for a correct payload

1. Read the route. Never assume its fields.
2. Render from `fieldDescriptors`, honouring `required`, lengths, `pattern`, and `options`.
3. Send codes, not names, on directory-backed routes.
4. Include `country` whenever you know it.
5. Put routing in `fields` for a saved detail, top-level for an inline destination.
6. Supply the counterparty profile on international routes.
7. Compare the returned `displayName` against what you expected before releasing money.


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