Skip to content
SiloraSilora

When things go wrong

Errors

Every error is application/problem+json (RFC 9457), extended with the ISO 20022 reason code. Error handling is written once and works everywhere.

The shape

json
{
  "type": "https://docs.silorapay.ai/errors/validation-failed",
  "title": "Validation failed",
  "status": 400,
  "detail": "3 fields failed validation for corridor US-IN",
  "correlationId": "01J8KX9P2R7M4Q",
  "isoReasonCode": "FF01",
  "errors": [ ... ]
}

Always log correlationId. It is echoed from your X-Correlation-Id when you send one, threaded through every internal hop, and it is the first thing Silora support will ask for.

400 — validation, with the rule that failed

Each entry points at the field with a JSON Pointer, and — where a corridor rule produced it — at the requirements document that explains why. A developer never has to guess which rule they broke.

json
{
  "type": "https://docs.silorapay.ai/errors/validation-failed",
  "title": "Validation failed",
  "status": 400,
  "detail": "3 fields failed validation for corridor US-IN",
  "correlationId": "01J8KX9P2R7M4Q",
  "isoReasonCode": "FF01",
  "errors": [
    { "pointer": "/purpose", "code": "FF05",
      "detail": "Required for US-IN. Use a code from IN_RBI_PURPOSE.",
      "requirementsUrl": "/v1/corridors/US-IN/requirements" },
    { "pointer": "/creditor/address/postCode", "code": "RR03",
      "detail": "Structured creditor address is mandatory for this corridor under CBPR+" },
    { "pointer": "/instructedAmount/value", "code": "FF01",
      "detail": "Amount must be a decimal string, not a JSON number" }
  ]
}

422 — well-formed but rejected

A 422 always carries isoReasonCode. When routing is the cause, routingDecision.evaluated shows every provider considered and why each one did not match — which turns "the payment failed" into "prefund the Bridge USD account".

json
{
  "type": "https://docs.silorapay.ai/errors/payment-rejected",
  "title": "Payment rejected",
  "status": 422,
  "detail": "No capable provider for corridor US-IN in USD at this amount",
  "correlationId": "01J8KX9P2R7M4Q",
  "isoReasonCode": "AG01",
  "routingDecision": {
    "preference": "AUTO", "tier": 3, "providerId": null,
    "evaluated": [
      { "providerId": "bridge", "matched": false, "reason": "Insufficient prefunding" },
      { "providerId": "xbs",    "matched": false, "reason": "Corridor not enabled" }
    ]
  }
}

Status codes

CodeMeaningRetry?
200Read, or a synchronous action completed
201Resource created — beneficiary, webhook endpoint, document
202Accepted for processing. Not settlement.
400Malformed, or failed schema / corridor / CBPR+ validation — FF01No. Fix the payload.
401Bad key, signature, or timestamp skewNo. See Authentication.
403Key lacks the scope, or tenant mismatchNo.
404Unknown uetr or resourceNo.
409Idempotency-key reuse with a different body, or an invalid state transitionNo. Investigate first.
422Well-formed but rejected — always carries isoReasonCodeOnly after remediation.
429Rate limited — see the RateLimit-* headersYes, after RateLimit-Reset.
503Provider unreachable — AB06 / AB07Yes, with the same Idempotency-Key.

ISO reason codes

isoReasonCode is an ExternalStatusReason1Code, validated against the quarterly ISO code set rather than a hardcoded enum. The ones you will actually see:

CodeMeaningTypical remediation
AC03Invalid creditor accountCorrect the account, run Confirmation of Payee, submit a new payment.
AC04Account closedObtain new account details from the payee.
AC06Account blockedPayee-side. Contact the beneficiary.
AM02Amount exceeds a limitCheck limits on the corridor requirements.
AM04Insufficient fundsPrefund the provider account. See GET /ledger/accounts.
AG01Forbidden, or no capable providerRead routingDecision.evaluated.
RR03Missing creditor addressSend a structured address — CBPR+ requires it.
RR04Regulatory reasonUsually a missing regulatoryReporting block.
CURRUnsupported currencyCheck the capability matrix for the corridor.
TM01Past cut-offResubmit for the next value date, or accept T+1.
DT01Invalid date, or expired quoteRe-quote and resubmit.
DUPLDuplicateCheck whether an earlier attempt succeeded before retrying.
ED05Settlement failedProvider-side. Retry or reroute.
FF05Invalid local instrument or purpose codeTake the value from the corridor code list.
AB06 / AB07Provider timeout or offlineRetry with the same Idempotency-Key.

Idempotency

Three identities, deliberately distinct. Conflating them is how a retry becomes a second payment.

IdentityCarrierScope
The paymentuetrWhole life, across retries and re-routes
The requestIdempotency-Key headerOne HTTP call. Replay returns the original response.
The provider attemptInternal, per attemptOne submission to one provider
  • Replaying a key with an identical body returns the original response plus Idempotency-Replayed: true.
  • Replaying a key with a different body returns 409. That is a bug in your retry logic, not a transient fault.
  • Keys are retained 24 hours.
  • endToEndId is not an idempotency key — customers legitimately reuse it on a resubmitted instruction.
typescript
const idempotencyKey = randomUUID();   // minted once, outside the retry loop
const payload = JSON.stringify(paymentRequest);

for (let attempt = 0; attempt < 4; attempt++) {
  const response = await siloraFetch('POST', '/payments', payload, idempotencyKey);

  if (response.status === 202) return response.json();

  // 503 and 429 are the only retryable statuses. Everything else is terminal.
  if (response.status !== 503 && response.status !== 429) {
    throw new SiloraProblem(await response.json());
  }

  await sleep(250 * 2 ** attempt);   // 250ms, 500ms, 1s, 2s
}

throw new Error('Provider unavailable after 4 attempts');
A retry that is safe because the key and the body are both stable.