Skip to content

Errors

Every error response carries a stable code. Branch on the code, not on the message. This page lists every code, the HTTP status it arrives with, and the action to take.

Error envelope

Every response body has ok: true or ok: false. Check ok before reading any other field. When it is false, the body has this shape:

json
{
  "ok": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Wallet has insufficient MYRT balance for redemption.",
    "meta": {
      "walletAddress": "0xAbC0000000000000000000000000000000000001",
      "chainId": 1,
      "requestedMyrt": "500.00",
      "walletBalanceMyrt": "212.40"
    }
  }
}
FieldStability
error.codeStable. This is what your integration branches on
error.messageFor humans. It may be reworded at any time; never parse it
error.metaPresent only where this page documents it. See error metadata

New codes may be added without a version change; see versioning. Treat a code you do not recognise as a failure, and retry it only if the HTTP status is one listed under retrying.

Error codes

CodeHTTPMeaningAction
UNAUTHORIZED401Missing, malformed, unknown, revoked, expired or wrong-environment key. The message is always Invalid or expired API key. and never says which condition failedCheck that the key was issued for the host you are calling and has not been revoked or expired. Do not retry
IP_NOT_ALLOWED403Source address is not in the key's IP allowlistSend the request from an address on the key's allowlist. Do not retry from the same address
SCOPE_DENIED403Key lacks a required scope, or account:read is used without a data processing agreement on fileUse a key that holds every scope the call needs. See scopes
RATE_LIMITED429The rate-limit bucket for this key is exhaustedWait for Retry-After seconds, or until X-RateLimit-Reset, then retry with the same Idempotency-Key. Do not tight-loop. See best practices
BAD_REQUEST400Request failed validation not covered by a more specific code, for example an invalid list filterRead message, fix the request, and send it again. Do not retry unchanged
INVALID_JSON400Body is not a JSON objectSend a JSON object with Content-Type: application/json
IDEMPOTENCY_KEY_REQUIRED400Idempotency-Key is missing on a POST, or longer than 255 charactersSend an Idempotency-Key of 1 to 255 characters, one UUID per logical operation. See idempotency
IDEMPOTENCY_CONFLICT409Idempotency-Key reused with a different body, or referenceId reused with different detailsA retry must send the identical body. A new operation needs a new Idempotency-Key and a new referenceId
INVALID_AMOUNT400Amount is not a positive decimal string with at most 6 decimal placesSend the amount as a string, for example "500.00". See amounts
INVALID_WALLET_ADDRESS400Not a valid EVM addressSend a checksummed EVM address
INVALID_REFERENCE_ID400referenceId fails the format rule, on reads as well as writesUse 8 to 100 characters from letters, digits, ., _, - and :. See reference IDs
NOT_FOUND404No such order or party, or it was not created by your integration, or the order exists but is the wrong direction for this routeCheck the referenceId or customerId and the route. Do not retry
CUSTOMER_NOT_VERIFIED403The customer has not cleared verificationDo not retry until the customer's verification is cleared. See accounts
POLICY_REJECTED400Generic issuance policy rejection with no more specific codeRead message for the reason. Do not retry unchanged
AMOUNT_NEGATIVE_OR_ZERO400Amount is zero or negativeSend a positive amount
AMOUNT_BELOW_FLOOR400Mint below RM 10.00Mint at least RM 10.00
AMOUNT_ABOVE_REDEEM_CEILING400Single redemption above RM 1,000,000Keep a single redeem order at or below RM 1,000,000
FOREIGNER_LIMIT_EXCEEDED400Non-resident customer above RM 100,000 in a single transactionKeep each mint or redeem for a non-resident customer at or below RM 100,000
INSUFFICIENT_BALANCE400The wallet holds less MYRT than the redemption asks forCompare meta.requestedMyrt with meta.walletBalanceMyrt. Check the wallet with balances before redeeming
MONTHLY_WITHDRAWAL_LIMIT_EXCEEDED400The redemption would exceed the customer's monthly withdrawal limitRead meta.monthlyLimit and meta.monthlyCommitted. Do not retry until the remaining limit covers the amount
TRANSFER_PER_TX_CAP_EXCEEDED400Transfer above the per-transaction capKeep each transfer at or below meta.perTxCapMyrt. Read the cap from meta; it may be lowered
TRANSFER_DAILY_CAP_EXCEEDED400Transfer would exceed the key's daily cap for the current UTC dayRead meta.dailyCapMyrt and meta.committedTodayMyrt. Lower the amount or wait for the next UTC day
RECIPIENT_NOT_PERMITTED403Transfer recipient failed screeningDo not retry with the same recipient
ORDER_NOT_CANCELLABLE409The order has already been submitted for settlementDo not retry. Read the order and wait for its final status
SETTLEMENT_FAILED502Settlement submission failedRetry with backoff, the same Idempotency-Key and the identical body
SERVER_ERROR500Unexpected errorRetry with backoff. If it persists, contact support and quote meta.correlationId

Error metadata

meta is present only on these codes. Every other code returns code and message alone.

Codemeta fields
INSUFFICIENT_BALANCEwalletAddress, chainId, requestedMyrt, walletBalanceMyrt
MONTHLY_WITHDRAWAL_LIMIT_EXCEEDEDmonthlyLimit, monthlyCommitted, requestedAmount
TRANSFER_PER_TX_CAP_EXCEEDEDrequestedMyrt, committedTodayMyrt, perTxCapMyrt, dailyCapMyrt
TRANSFER_DAILY_CAP_EXCEEDEDrequestedMyrt, committedTodayMyrt, perTxCapMyrt, dailyCapMyrt
SERVER_ERRORcorrelationId
  • INSUFFICIENT_BALANCE: requestedMyrt and walletBalanceMyrt are MYRT amounts as decimal strings. Parse them with a decimal library; never compare amount strings. See redeem.
  • MONTHLY_WITHDRAWAL_LIMIT_EXCEEDED: monthlyLimit, monthlyCommitted and requestedAmount are numbers in MYR.
  • TRANSFER_PER_TX_CAP_EXCEEDED and TRANSFER_DAILY_CAP_EXCEEDED: the caps are enforced server-side and are not configurable per request. Read them from meta rather than hardcoding them; they may be lowered. Failed and cancelled transfers do not count toward the daily total. See transfers.
  • SERVER_ERROR: see correlation ids.

Retrying

Retry only these HTTP statuses: 429, 502, 503 and 504. Back off between attempts. On a 429, wait for the number of seconds in Retry-After, or until the Unix time in X-RateLimit-Reset. Do not tight-loop.

Never retry any other 4xx. The request itself is wrong; fix it and send it as a new request.

When you retry a POST, send the same Idempotency-Key and the identical body. The API answers with the original status and body and sets X-MYRT-Idempotent-Replay: true; nothing is created twice. Generate one key per logical operation, not per HTTP attempt. A timeout or a dropped connection is retried the same way. See idempotency.

ts
import { randomUUID } from "node:crypto";

const RETRYABLE = new Set([429, 502, 503, 504]);
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function postWithRetry(url: string, body: unknown, attempts = 5): Promise<Response> {
  // One key per logical operation, reused on every attempt.
  const idempotencyKey = randomUUID();
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MYRT_API_KEY}`,
        "Idempotency-Key": idempotencyKey,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });
    if (!RETRYABLE.has(res.status) || attempt === attempts) return res;
    const retryAfter = Number(res.headers.get("Retry-After"));
    const waitMs = retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** (attempt - 1);
    await sleep(waitMs);
  }
}

const res = await postWithRetry("https://sandbox-api.myrt.money/v1/redeems", {
  referenceId: "acme:redeem:2026-09-10:00092",
  amountMyrt: "500.00",
  walletAddress: "0xAbC0000000000000000000000000000000000001",
  customerId: "cus_9f2a3b4c",
});
const order = await res.json();
if (!order.ok) throw new Error(`${order.error.code}: ${order.error.message}`);

Correlation ids

A 500 SERVER_ERROR carries meta.correlationId. Retry with backoff first. If the error persists, contact support and quote the correlationId from the response. It lets support find the failed request without you sharing your key.

MYRT is a 1:1 Ringgit-backed stablecoin on Ethereum.