Rate limits and best practices
Rate limits are enforced per key in fixed one-minute windows. The rest of this page is practical advice on how to build an integration that stays correct when something goes wrong.
Rate limits
Every key draws from three buckets. Each bucket is counted on its own, per key, in fixed one-minute windows.
| Bucket | Limit | Endpoints |
|---|---|---|
| Reads | 120 per minute | Every GET except balances |
| Writes | 30 per minute | Every POST |
| Balance reads | 60 per minute | GET /v1/balances/{walletAddress} |
A fixed window does not slide. The counter for a bucket resets at the time reported in X-RateLimit-Reset, and a full allowance is available again from that moment.
Balance reads have their own, tighter bucket because every call reads the chain. Cache balances on your side for display instead of reading them on every page view. See balances.
Every response carries three headers that tell you where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute in this bucket |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Unix time in seconds when the window resets |
When a bucket is exhausted the call returns 429 RATE_LIMITED and adds Retry-After, the number of seconds to wait. Retry-After appears only on a 429.
HTTP/1.1 429 Too Many Requests
X-MYRT-API-Version: v1
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1789013580
Retry-After: 27{
"ok": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Retry after the window resets."
}
}Branch on error.code. The message is for humans and may be reworded at any time. Every code is listed on the errors page.
Handling 429
Back off until X-RateLimit-Reset. Read the header, sleep until that time, then resume. If a wait in seconds is easier for your client, use Retry-After instead.
Do not tight-loop. Retrying a 429 immediately gains nothing. The window is fixed, so no capacity returns before the reset time. Watch X-RateLimit-Remaining on every response and slow down before it reaches zero rather than after.
Spread reconciliation reads. Reconciliation shares the read bucket with status polling and every other GET your integration makes. A nightly pass fired at full speed exhausts reads for as long as it runs and starves everything else. Pace it below 120 per minute and run it at a quiet time. Once GET /v1/transactions (planned) is live, one page returns up to 100 orders for a single read, which cuts the cost of a pass considerably.
Practices that matter
Store the referenceId before you send. Generate the referenceId, write it to your own records, then make the call. If your process dies between the two you can still find the order later, because the referenceId is the handle for every read and appears in reconciliation. For a mint or a transfer, re-posting an existing referenceId returns the existing order. For a redeem, a referenceId that already exists with different details returns 409 IDEMPOTENCY_CONFLICT. See reference IDs.
Treat webhooks as hints and read the order back. Webhooks are planned. When they arrive, delivery is at least once and ordering is not guaranteed. An event tells you that something changed; it does not tell you the final state. On any event, GET the order and act on the status you read back. Never credit a customer from a webhook payload alone. Deduplicate on the event id.
Reconcile daily. Once a day, compare every order your records say is open or recently settled with what the API returns, and investigate every mismatch. Use GET /v1/transactions (planned) filtered by direction, status and createdAfter, or walk your own list of referenceId values with GET /v1/transactions/{referenceId}, which returns an order of any direction. This is how you catch an update that a webhook or your own process dropped. See transactions.
Never mint on an unconfirmed payment. Create the mint with fiatConfirmed: false when the customer initiates. Send fiatConfirmed: true only once MYR settlement is irreversible, and only from a key that holds mint:execute. MYRT is never issued ahead of fiat. Until you confirm, the order waits in payment_pending. See the mint guide.
Use decimal arithmetic everywhere and never compare amount strings. Every amount is a decimal string, never a JSON number. Strings are not zero-padded, so 1000 and 1000.00 are the same value and a string comparison will call them different. Parse every amount with a decimal library and compare the parsed values. When you match an order against an on-chain Transfer event, use amountRaw, the exact on-chain integer. decimals is always 6. See amounts.
Poll at a sensible interval. An order rarely changes within seconds. It reaches completed only after the chain's confirmation count, and a txHash alone is not final. Polling every second spends your read bucket for no gain. Prefer webhooks for settlement events once they are live and keep polling as the fallback. See the order lifecycle.
Check clock skew with /v1/system/time. The call takes no key and returns serverTime in UTC. Compare it with your own clock at startup and whenever you see signature failures. Webhook signatures carry a timestamp, and your verifier must reject any signature more than 300 seconds from now, so a skewed clock rejects valid deliveries. See server time.
Timeouts and retries
Set a client-side timeout on every call. When a POST times out you do not know whether the order was created. Retry it with the identical Idempotency-Key and the identical body. If the original went through, you get the original status and body back with X-MYRT-Idempotent-Replay: true, and nothing is created twice. If it did not, the retry creates it. Generate one UUID per logical operation, not per HTTP attempt. See idempotency.
Retry these, with backoff, and with the same Idempotency-Key on a POST:
429 RATE_LIMITED: wait forRetry-After, then resend.502: this includesSETTLEMENT_FAILED, where settlement submission failed. Resend with the same key; nothing is created twice.503and504: back off and resend.
Never retry any other 4xx. A 400, 401, 403, 404 or 409 describes a problem with the request, the key, or the state it refers to. Fix the cause before you send again. A 409 IDEMPOTENCY_CONFLICT means the same Idempotency-Key was reused with a different body; a genuinely new operation needs a new key.
A 500 SERVER_ERROR carries meta.correlationId. Retry with backoff, and if it persists, quote the correlationId to support. The meaning of every code is on the errors page.
Security
The API is server-to-server. No CORS headers are sent and browser calls are not supported. Keys belong on a server you control.
DANGER
Never put a key in browser code, a mobile binary, or a public repository.
Issue one key per system and give it the narrowest scopes. Thirteen scopes exist. A service that only shows balances needs balance:read and nothing else. A request outside the key's scopes returns 403 SCOPE_DENIED, so a narrow key limits what a leaked key can do. See scopes.
Keep execute scopes on a separate, tightly held key. mint:execute, redeem:execute and transfer:execute move value. Put mint:create on the service that takes customer orders and mint:execute on the service that confirms fiat. Redeem and transfer need create and execute on the same key, so hold each pair on a key that only your settlement service can reach.
Restrict every live key by IP. A key can be limited to a list of source IP addresses or CIDR ranges. A request from anywhere else returns 403 IP_NOT_ALLOWED. Do this for every live key.
Rotate on a schedule, and on every staff change. Rotation supports overlapping validity: issue the new key, deploy it, confirm traffic, then revoke the old one, with no downtime. A key is shown once at creation and stored only as a hash, so a lost key is rotated, not recovered. The key id, the third segment of the key and 16 hex characters long, is safe to quote to support. The secret never is. After revocation the old key returns 401 UNAUTHORIZED with the same message as every other authentication failure.
Alert on every use of transfer:execute. Transfer is the highest-risk endpoint. Hold transfer:execute on a dedicated key, restrict that key by IP, and alert on every call. The server caps transfers per transaction and per key per UTC day, and reports the caps in meta when it rejects one, but treat those caps as a backstop rather than as your monitoring. See the transfers guide.
