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.
/v1/transfersTwo 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.
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"
}'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}`);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
| Field | Type | Required | Notes |
|---|---|---|---|
referenceId | string | yes | Your unique handle for this transfer. See reference IDs |
amountMyrt | string | yes | Positive decimal, at most 6 decimal places |
walletAddress | string | yes | Recipient address. Must be a valid EVM address |
customerId | string | yes | The account the transfer is debited against. A customer linked to your integration |
chainId | number | no | Defaults 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.
| Ceiling | Limit | Rejection |
|---|---|---|
| Per transaction | 50,000 MYRT | 400 TRANSFER_PER_TX_CAP_EXCEEDED |
| Per key, per UTC day | 250,000 MYRT | 400 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:
| Field | Meaning |
|---|---|
requestedMyrt | The amount this request asked to move |
committedTodayMyrt | The amount already committed on this key today |
perTxCapMyrt | The per-transaction ceiling in force |
dailyCapMyrt | The 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
{
"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
/v1/transfers/{referenceId}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.
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
| Code | HTTP | When |
|---|---|---|
INVALID_AMOUNT | 400 | amountMyrt is not a positive decimal with at most 6 decimal places |
AMOUNT_NEGATIVE_OR_ZERO | 400 | amountMyrt is zero or negative |
INVALID_WALLET_ADDRESS | 400 | walletAddress is not a valid EVM address |
INVALID_REFERENCE_ID | 400 | referenceId fails the format rule |
TRANSFER_PER_TX_CAP_EXCEEDED | 400 | Above the per-transaction ceiling. Read the caps from meta |
TRANSFER_DAILY_CAP_EXCEEDED | 400 | Would exceed the key's daily ceiling. Read the caps from meta |
POLICY_REJECTED | 400 | Generic policy rejection. Read message |
SCOPE_DENIED | 403 | The key lacks transfer:create or transfer:execute |
CUSTOMER_NOT_VERIFIED | 403 | The customer has not cleared verification |
RECIPIENT_NOT_PERMITTED | 403 | The recipient failed screening |
NOT_FOUND | 404 | customerId is not linked to your integration, or does not exist |
IDEMPOTENCY_CONFLICT | 409 | Same Idempotency-Key, different body |
SETTLEMENT_FAILED | 502 | Settlement 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.
