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:
{
"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"
}
}
}| Field | Stability |
|---|---|
error.code | Stable. This is what your integration branches on |
error.message | For humans. It may be reworded at any time; never parse it |
error.meta | Present 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
| Code | HTTP | Meaning | Action |
|---|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed, unknown, revoked, expired or wrong-environment key. The message is always Invalid or expired API key. and never says which condition failed | Check that the key was issued for the host you are calling and has not been revoked or expired. Do not retry |
IP_NOT_ALLOWED | 403 | Source address is not in the key's IP allowlist | Send the request from an address on the key's allowlist. Do not retry from the same address |
SCOPE_DENIED | 403 | Key lacks a required scope, or account:read is used without a data processing agreement on file | Use a key that holds every scope the call needs. See scopes |
RATE_LIMITED | 429 | The rate-limit bucket for this key is exhausted | Wait for Retry-After seconds, or until X-RateLimit-Reset, then retry with the same Idempotency-Key. Do not tight-loop. See best practices |
BAD_REQUEST | 400 | Request failed validation not covered by a more specific code, for example an invalid list filter | Read message, fix the request, and send it again. Do not retry unchanged |
INVALID_JSON | 400 | Body is not a JSON object | Send a JSON object with Content-Type: application/json |
IDEMPOTENCY_KEY_REQUIRED | 400 | Idempotency-Key is missing on a POST, or longer than 255 characters | Send an Idempotency-Key of 1 to 255 characters, one UUID per logical operation. See idempotency |
IDEMPOTENCY_CONFLICT | 409 | Idempotency-Key reused with a different body, or referenceId reused with different details | A retry must send the identical body. A new operation needs a new Idempotency-Key and a new referenceId |
INVALID_AMOUNT | 400 | Amount is not a positive decimal string with at most 6 decimal places | Send the amount as a string, for example "500.00". See amounts |
INVALID_WALLET_ADDRESS | 400 | Not a valid EVM address | Send a checksummed EVM address |
INVALID_REFERENCE_ID | 400 | referenceId fails the format rule, on reads as well as writes | Use 8 to 100 characters from letters, digits, ., _, - and :. See reference IDs |
NOT_FOUND | 404 | No such order or party, or it was not created by your integration, or the order exists but is the wrong direction for this route | Check the referenceId or customerId and the route. Do not retry |
CUSTOMER_NOT_VERIFIED | 403 | The customer has not cleared verification | Do not retry until the customer's verification is cleared. See accounts |
POLICY_REJECTED | 400 | Generic issuance policy rejection with no more specific code | Read message for the reason. Do not retry unchanged |
AMOUNT_NEGATIVE_OR_ZERO | 400 | Amount is zero or negative | Send a positive amount |
AMOUNT_BELOW_FLOOR | 400 | Mint below RM 10.00 | Mint at least RM 10.00 |
AMOUNT_ABOVE_REDEEM_CEILING | 400 | Single redemption above RM 1,000,000 | Keep a single redeem order at or below RM 1,000,000 |
FOREIGNER_LIMIT_EXCEEDED | 400 | Non-resident customer above RM 100,000 in a single transaction | Keep each mint or redeem for a non-resident customer at or below RM 100,000 |
INSUFFICIENT_BALANCE | 400 | The wallet holds less MYRT than the redemption asks for | Compare meta.requestedMyrt with meta.walletBalanceMyrt. Check the wallet with balances before redeeming |
MONTHLY_WITHDRAWAL_LIMIT_EXCEEDED | 400 | The redemption would exceed the customer's monthly withdrawal limit | Read meta.monthlyLimit and meta.monthlyCommitted. Do not retry until the remaining limit covers the amount |
TRANSFER_PER_TX_CAP_EXCEEDED | 400 | Transfer above the per-transaction cap | Keep each transfer at or below meta.perTxCapMyrt. Read the cap from meta; it may be lowered |
TRANSFER_DAILY_CAP_EXCEEDED | 400 | Transfer would exceed the key's daily cap for the current UTC day | Read meta.dailyCapMyrt and meta.committedTodayMyrt. Lower the amount or wait for the next UTC day |
RECIPIENT_NOT_PERMITTED | 403 | Transfer recipient failed screening | Do not retry with the same recipient |
ORDER_NOT_CANCELLABLE | 409 | The order has already been submitted for settlement | Do not retry. Read the order and wait for its final status |
SETTLEMENT_FAILED | 502 | Settlement submission failed | Retry with backoff, the same Idempotency-Key and the identical body |
SERVER_ERROR | 500 | Unexpected error | Retry 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.
| Code | meta fields |
|---|---|
INSUFFICIENT_BALANCE | walletAddress, chainId, requestedMyrt, walletBalanceMyrt |
MONTHLY_WITHDRAWAL_LIMIT_EXCEEDED | monthlyLimit, monthlyCommitted, requestedAmount |
TRANSFER_PER_TX_CAP_EXCEEDED | requestedMyrt, committedTodayMyrt, perTxCapMyrt, dailyCapMyrt |
TRANSFER_DAILY_CAP_EXCEEDED | requestedMyrt, committedTodayMyrt, perTxCapMyrt, dailyCapMyrt |
SERVER_ERROR | correlationId |
INSUFFICIENT_BALANCE:requestedMyrtandwalletBalanceMyrtare MYRT amounts as decimal strings. Parse them with a decimal library; never compare amount strings. See redeem.MONTHLY_WITHDRAWAL_LIMIT_EXCEEDED:monthlyLimit,monthlyCommittedandrequestedAmountare numbers in MYR.TRANSFER_PER_TX_CAP_EXCEEDEDandTRANSFER_DAILY_CAP_EXCEEDED: the caps are enforced server-side and are not configurable per request. Read them frommetarather 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.
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.
