Skip to content
SiloraSilora

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=ORGA

Requirement levels

LevelMeaning
REQUIREDOmit it and the payment fails validation with 400.
CONDITIONALRequired when condition holds. The condition is expressed against the payload and the amount, so you can evaluate it yourself before sending.
RECOMMENDEDAccepted without it, but the outcome is measurably worse — slower beneficiary-side reconciliation, more manual intervention.
OPTIONALSupported, no consequence either way.
NOT_SUPPORTEDSending 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. Send If-None-Match and take the 304 — 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.cutOff and cutOffTimezone are the same-day settlement deadline. Past it, expect TM01 and a next-business-day settlement date.