RMO Developers

Errors

Every error from /v1/* follows a single envelope shape. Decline codes follow ISO-8583 conventions so they map cleanly to existing processor flows.

The canonical envelope

Every non-2xx response (and every declined 200 response on the auth endpoints) carries an error object — or, for declines, a top-level reasonCode + reasonMessage. Both use the same numeric / string codes.

{
  "error": {
    "code":    "54",                 // stable machine identifier
    "message": "Card expired",        // human-readable
    "field":   "panToken",            // optional, when validation
    "details": { ... }                // optional context
  }
}

Always switch on error.code (or reasonCode) for your handling logic — messages are subject to wording changes. Codes are stable.

HTTP status mapping

Statuserror.codeMeaning
400bad_requestMalformed request — missing required header, invalid JSON, etc.
400missing_fieldA required body field is missing. error.field names it.
400invalid_fieldA body field fails validation. error.field names it.
401unauthorizedMissing / unknown X-API-Key or invalid bearer.
401invalid_signatureHMAC mismatch or timestamp drift > 300s (signing-enabled tenants).
403forbiddenTenant not active, or source IP outside the allowlist.
403(no body)API Gateway rejection — key not in usage plan or daily quota exhausted.
404not_foundResource doesn’t exist or isn’t owned by this tenant.
409conflictGeneric state conflict.
409invalid_stateMutation attempted on an authorization in the wrong state (e.g. capturing a declined auth).
409fully_capturedTrying to capture more than the remaining hold.
409nothing_to_reverseTrying to reverse an already-fully-reversed authorization.
409idempotency_raceIdempotency-Key collision under concurrent submit. Retry once.
42214Invalid panToken — token not found, inactive, or wrong type.
429rate_limitedTenant rate limit exceeded.
500server_errorInternal error. Retry-safe with the same Idempotency-Key.

Decline codes (ISO-8583)

When an authorization is declined, status: "declined" is returned with a reasonCode. These are the same numeric values defined in ISO-8583 plus a handful of RMO-specific ones.

CodeTitleWhen it fires
14Invalid card numberpanToken not found, inactive, or wrong type.
54Expired card / codeCard past expiration, or payment code past expiresAt.
61Exceeds limitLimit-engine breach (daily cap, per-tx cap, etc.), or amount > payment-code maxAmount.
62RestrictedCard status isn’t Active; or payment code is revoked / locked to another merchant.
65Exceeds withdrawal frequencyVelocity-limit breach.
75PIN tries exceeded / invalid PINWrong PIN on payment-code redemption. After lockoutThreshold bad attempts, the code is locked.

Reversal reason codes

When you reverse an authorization, you can set reason in the request body. Default is RV02.

CodeMeaning
RV01Merchant-initiated reversal (no longer needed).
RV02Customer-requested void / refund.
RV03Fraud / chargeback reversal.

Handling patterns

Distinguish declines from errors

A decline is a 200 response with status: "declined" — the network reached a decision and you should show the customer the reason. An error is a 4xx/5xx with an error object — your request itself was malformed or your tenant misconfigured. Don’t conflate the two.

Always retry 5xx with the same Idempotency-Key

5xx responses are retry-safe as long as you reuse the exact Idempotency-Key. If we already committed the side effect, you’ll get the original response back; if not, your retry creates it cleanly.

Never retry 4xx without changing the request

A 400 / 401 / 403 / 404 / 409 will always produce the same result on retry — fix the request first. 429 is the exception: back off then retry with the same key.

Always read error.field on validation failures

It tells you exactly which body field needs to change. Surface it directly to whatever called your code — this turns ambiguous “something went wrong” bugs into precise field-level errors.

Where to next

Authentication → Card authorizations → OpenAPI spec →