Building a payload
Corridor requirements
Cross-border corridors differ enormously in what they require. India wants a central-bank purpose code and, above a threshold, import documents. Several corridors want the beneficiary’s date and place of birth. Mobile-money payouts want a phone number. Rather than documenting that in prose and letting you discover it through rejections, the API tells you.
One call, before you build the payload
GET /v1/corridors/US-IN/requirements?currency=USD&amount=24500&creditorType=ORGARequirement levels
| Level | Meaning |
|---|---|
REQUIRED | Omit it and the payment fails validation with 400. |
CONDITIONAL | Required when condition holds. The condition is expressed against the payload and the amount, so you can evaluate it yourself before sending. |
RECOMMENDED | Accepted without it, but the outcome is measurably worse — slower beneficiary-side reconciliation, more manual intervention. |
OPTIONAL | Supported, no consequence either way. |
NOT_SUPPORTED | Sending it is an error on this corridor. Do not populate it. |
Driving your own form from it
The response is shaped for machine consumption. A payment form that renders itself from fields never drifts from the regulation, because the regulation is what produced the response.
typescript
type Requirement = 'REQUIRED' | 'CONDITIONAL' | 'RECOMMENDED' | 'OPTIONAL' | 'NOT_SUPPORTED';
interface FieldRequirement {
path: string;
requirement: Requirement;
label?: string;
reason?: string;
pattern?: string;
maxLength?: number;
allowed?: string[];
codeList?: string;
codeListUrl?: string;
condition?: string;
}
interface CorridorRequirements {
corridor: string;
supported: boolean;
fields: FieldRequirement[];
limits: { minAmount: Amount; maxAmount: Amount; cutOff: string; cutOffTimezone: string };
providers: string[];
}
export function buildForm(requirements: CorridorRequirements) {
return requirements.fields
.filter((field) => field.requirement !== 'NOT_SUPPORTED')
.map((field) => ({
name: field.path,
label: field.label ?? humanise(field.path),
required: field.requirement === 'REQUIRED',
conditional: field.requirement === 'CONDITIONAL' ? field.condition : undefined,
hint: field.reason,
// A codeList means the values come from GET /code-lists/{listId}.
optionsUrl: field.codeListUrl,
validation: {
pattern: field.pattern ? new RegExp(field.pattern) : undefined,
maxLength: field.maxLength,
oneOf: field.allowed,
},
}));
}Code lists
When a field carries codeList, the permitted values live behind codeListUrl. They are local regulatory code sets and they change — never hardcode them.
json
GET /v1/code-lists/IN_RBI_PURPOSE
{
"listId": "IN_RBI_PURPOSE",
"authority": "Reserve Bank of India",
"updatedAt": "2026-07-01T00:00:00Z",
"codes": [
{ "code": "P0103", "name": "Import of goods" },
{ "code": "P0104", "name": "Import of services" },
{ "code": "P1006", "name": "Software consultancy" }
]
}Caching
- The response carries an
ETag. SendIf-None-Matchand take the304— requirements change when a provider or a regulation changes, not per request. - Cache per
{corridor, currency, amount band, creditorType}. Requirements are threshold-dependent, so a single cache entry per corridor is wrong. - Refresh at least daily. A stale cache produces
400s whose cause is invisible in your own logs. limits.cutOffandcutOffTimezoneare the same-day settlement deadline. Past it, expectTM01and a next-business-day settlement date.