Skip to content

Transfers: move MYRT on-chain

A transfer moves MYRT to a recipient wallet address on behalf of one of your customers. You supply the recipient, the amount and the customerId the transfer is debited against. The transfer is settled from MYRT treasury on the customer's behalf, and the recipient sees a standard ERC-20 Transfer.

Transfer is the highest-risk endpoint

A successful call moves MYRT to an address you supplied. Hold transfer:execute on a dedicated key, restrict that key to an IP allowlist, and alert on every use. A request from outside the allowlist returns 403 IP_NOT_ALLOWED. Never put a key in browser code, a mobile binary, or a public repository.

POST/v1/transferstransfer:create

Two scopes are required

Creating a transfer also submits it for settlement, so POST /v1/transfers needs both transfer:create and transfer:execute on the same key. A key with only transfer:create receives 403 SCOPE_DENIED. See scopes.

bash
curl -X POST https://sandbox-api.myrt.money/v1/transfers \
  -H "Authorization: Bearer $MYRT_API_KEY" \
  -H "Idempotency-Key: 9b2d0f6e-3c1a-4e8f-9d7b-5a6c4e2f1b0a" \
  -H "Content-Type: application/json" \
  -d '{
    "referenceId": "acme:transfer:2026-09-10:00031",
    "amountMyrt": "250.00",
    "walletAddress": "0xAbC0000000000000000000000000000000000001",
    "customerId": "cus_9f2a3b4c"
  }'
ts
import { randomUUID } from "node:crypto";

const res = await fetch("https://sandbox-api.myrt.money/v1/transfers", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MYRT_API_KEY}`,
    "Idempotency-Key": randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    referenceId: "acme:transfer:2026-09-10:00031",
    amountMyrt: "250.00",
    walletAddress: "0xAbC0000000000000000000000000000000000001",
    customerId: "cus_9f2a3b4c",
  }),
});
const order = await res.json();
if (!order.ok) throw new Error(`${order.error.code}: ${order.error.message}`);
python
import os, uuid, requests

res = requests.post(
    "https://sandbox-api.myrt.money/v1/transfers",
    headers={
        "Authorization": f"Bearer {os.environ['MYRT_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "referenceId": "acme:transfer:2026-09-10:00031",
        "amountMyrt": "250.00",
        "walletAddress": "0xAbC0000000000000000000000000000000000001",
        "customerId": "cus_9f2a3b4c",
    },
    timeout=30,
)
order = res.json()
if not order["ok"]:
    raise RuntimeError(f"{order['error']['code']}: {order['error']['message']}")

Request fields

FieldTypeRequiredNotes
referenceIdstringyesYour unique handle for this transfer. See reference IDs
amountMyrtstringyesPositive decimal, at most 6 decimal places
walletAddressstringyesRecipient address. Must be a valid EVM address
customerIdstringyesThe account the transfer is debited against. A customer linked to your integration
chainIdnumbernoDefaults to the active chain. See network configuration

A customerId that is not linked to your integration returns 404 NOT_FOUND, the same as one that does not exist. A customer whose verification is not cleared returns 403 CUSTOMER_NOT_VERIFIED.

Re-posting a referenceId that already exists on a transfer returns the existing order. A replay of the same Idempotency-Key and body returns the original response with X-MYRT-Idempotent-Replay: true; nothing is created twice. On a timeout, retry with the identical key and body. See idempotency.

Ceilings

Two ceilings apply to every transfer. Both are enforced server-side and cannot be adjusted per request.

CeilingLimitRejection
Per transaction50,000 MYRT400 TRANSFER_PER_TX_CAP_EXCEEDED
Per key, per UTC day250,000 MYRT400 TRANSFER_DAILY_CAP_EXCEEDED

The daily total is counted per key. Failed and cancelled transfers do not count toward it.

Both rejections carry the same four fields in error.meta:

FieldMeaning
requestedMyrtThe amount this request asked to move
committedTodayMyrtThe amount already committed on this key today
perTxCapMyrtThe per-transaction ceiling in force
dailyCapMyrtThe daily ceiling in force

TIP

Read the ceilings from meta.perTxCapMyrt and meta.dailyCapMyrt rather than hardcoding them. They may be lowered.

Recipient screening

The recipient address is screened when you create the transfer. A recipient that fails screening returns 403 RECIPIENT_NOT_PERMITTED. Do not retry the request: only 429, 502, 503 and 504 are safe to retry, never another 4xx.

Response

201 Created

json
{
  "ok": true,
  "referenceId": "acme:transfer:2026-09-10:00031",
  "status": "pending",
  "phase": "ORDER_CREATED",
  "direction": "TRANSFER",
  "chainId": 1,
  "network": "Ethereum",
  "tokenAddress": "0x...",
  "walletAddress": "0xAbC0000000000000000000000000000000000001",
  "amounts": {
    "myr": null,
    "myrt": "250.00",
    "feeMyr": "0",
    "amountRaw": "250000000",
    "decimals": 6
  },
  "blockchain": { "txHash": null, "blockNumber": null },
  "timestamps": { "createdAt": "2026-09-10T04:12:33.918Z", "updatedAt": null }
}

For a transfer, walletAddress is the recipient. amounts.myrt is the amount moved, amounts.myr is null and amounts.feeMyr is "0". amounts.amountRaw is the exact on-chain integer, myrt scaled by 10^6, and is the value to compare with the Transfer event.

Amounts are decimal strings and are not zero-padded: feeMyr is "0" here, and 1000 and 1000.00 are the same value. Parse them with a decimal library and never compare amount strings. See amounts.

status and phase are explained in core concepts. Drive your business logic from status. New phase values may be added without notice, so handle unknown values.

Reading status

GET/v1/transfers/{referenceId}transfer:read

Returns the same object with updated status, phase and blockchain.txHash. A transfer reaches completed only after the chain's confirmation count, so do not treat a transaction hash as final on its own. failed is terminal and no value moved.

This route returns only transfer orders. An order of another direction, or one that another integration created, returns 404 NOT_FOUND, the same as an order that does not exist. To read an order of any direction, use the transactions reference.

bash
curl https://sandbox-api.myrt.money/v1/transfers/acme:transfer:2026-09-10:00031 \
  -H "Authorization: Bearer $MYRT_API_KEY"

Polling works. An order rarely changes within seconds, so poll at a sensible interval. Webhooks deliver transfer.completed and transfer.failed once they are released. On any event, read the order back with the API before you credit anyone.

Errors you should handle

CodeHTTPWhen
INVALID_AMOUNT400amountMyrt is not a positive decimal with at most 6 decimal places
AMOUNT_NEGATIVE_OR_ZERO400amountMyrt is zero or negative
INVALID_WALLET_ADDRESS400walletAddress is not a valid EVM address
INVALID_REFERENCE_ID400referenceId fails the format rule
TRANSFER_PER_TX_CAP_EXCEEDED400Above the per-transaction ceiling. Read the caps from meta
TRANSFER_DAILY_CAP_EXCEEDED400Would exceed the key's daily ceiling. Read the caps from meta
POLICY_REJECTED400Generic policy rejection. Read message
SCOPE_DENIED403The key lacks transfer:create or transfer:execute
CUSTOMER_NOT_VERIFIED403The customer has not cleared verification
RECIPIENT_NOT_PERMITTED403The recipient failed screening
NOT_FOUND404customerId is not linked to your integration, or does not exist
IDEMPOTENCY_CONFLICT409Same Idempotency-Key, different body
SETTLEMENT_FAILED502Settlement submission failed. Retry with the same Idempotency-Key

Retry 429, 502, 503 and 504 with backoff and the same Idempotency-Key. Never retry another 4xx.

The full catalogue is on the errors page. The complete list for these endpoints is on the transfers reference.

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