Skip to content

Core concepts

Every value-moving call in the MYRT API creates an order, and every order has the same shape. This page explains that shape and the rules the operation guides rely on. Read it once before you mint, redeem or transfer.

Orders

A mint, a redeem and a transfer are all orders. They are created by different routes, but they serialise to one object. You receive it as the 201 body of every create call, as the 200 body of every read, and as each entry of transactions[] on GET /v1/transactions (planned). Webhook payloads carry a subset of the same fields under data.

json
{
  "ok": true,
  "referenceId": "acme:mint:2026-09-10:00417",
  "status": "payment_pending",
  "phase": "PAYMENT_PENDING",
  "direction": "MINT",
  "chainId": 1,
  "network": "Ethereum",
  "tokenAddress": "0x...",
  "walletAddress": "0xAbC0000000000000000000000000000000000001",
  "amounts": {
    "myr": "1000.00",
    "myrt": "1000.00",
    "feeMyr": "1.00",
    "amountRaw": "1000000000",
    "decimals": 6
  },
  "blockchain": { "txHash": null, "blockNumber": null },
  "timestamps": { "createdAt": "2026-09-10T04:12:33.918Z", "updatedAt": null }
}
FieldTypeDescription
referenceIdstringThe handle you chose on creation. See identifiers
statusstringBusiness state. Drive your logic from it. See order lifecycle
phasestringOperational detail for progress display and support tooling
directionstringMINT, REDEEM or TRANSFER. See direction
chainIdnumberThe chain the order settles on
networkstringDisplay name of that chain
tokenAddressstringThe MYRT contract on that chain
walletAddressstringThe wallet involved. Its meaning depends on direction
amountsobjectDecimal strings, never numbers. See amounts
blockchain.txHashstring or nullTransaction hash. null until broadcast
blockchain.blockNumberstring or nullBlock that included the transaction. null until broadcast
timestamps.createdAtstringISO 8601 UTC. See timestamps
timestamps.updatedAtstring or nullnull until the first update

Every response body also carries ok. On success it is true and the order fields sit beside it. On failure it is false and the body carries error instead. Codes are listed on the errors page.

Order lifecycle

status is the business truth. phase is operational detail. Branch on status. Show phase to operators and in progress displays, and nowhere else.

Status

statusMeaningTerminal
payment_pendingMint recorded, fiat not yet confirmedno
pendingAccepted, awaiting settlementno
processingSubmitted, awaiting on-chain confirmationno
completedConfirmed on chain. Finalyes
failedNo value moved. Finalyes
cancelledCancelled before settlementyes

No status values will be added in v1. Changing the meaning of a status value is a breaking change and happens only in a new major version. See versioning.

Phase

phase values currently emitted:

  • ORDER_CREATED
  • PAYMENT_PENDING
  • PENDING_APPROVAL
  • AML_SCREENING
  • BROADCASTING
  • ON_CHAIN_CONFIRMED
  • COMPLETED
  • FAILED

A cancelled order reports phase: "FAILED" while its status is cancelled. Read status to tell the two apart.

Handle unknown phases

New phase values may be added without notice. Never fail an order, or a parser, on a phase you do not recognise. status is the field that stays fixed.

A mint, from creation to completion

  1. You POST /v1/mints with fiatConfirmed: false. The order is recorded with status: "payment_pending" and phase: "PAYMENT_PENDING". No MYRT is issued.
  2. Your MYR settles. You post the same referenceId again with fiatConfirmed: true, on a key holding mint:execute. The order moves to pending: accepted, awaiting settlement.
  3. The order is submitted for settlement and moves to processing. blockchain.txHash and blockchain.blockNumber stay null until the transaction is broadcast.
  4. The chain reaches its confirmation count and the order moves to completed. Sandbox waits for 3 confirmations, live for 15. Read the count from confirmations on /v1/config.

A txHash alone is not final. Credit nothing until status is completed.

A mint created with fiatConfirmed: true is submitted for settlement straight away. Redeems and transfers are submitted on creation, so their 201 response already reports status: "pending" and phase: "ORDER_CREATED". payment_pending applies to mints only.

A mint that has not yet been submitted for settlement can be cancelled (planned) and then reports cancelled. One that has already been submitted returns 409 ORDER_NOT_CANCELLABLE. See the mint guide.

Poll the read endpoint at a sensible interval, or use webhooks once they ship. Either way, read the order back before you credit anyone.

Direction

direction says which way value moves. It decides what walletAddress and the amounts mean, and which route returns the order.

directionWhat happenswalletAddressCreateRead
MINTMYR in, MYRT issued to a walletThe recipientPOST /v1/mintsGET /v1/mints/{referenceId}
REDEEMMYRT burned from a wallet, MYR paid to the customer's bank accountThe wallet MYRT is burned fromPOST /v1/redeemsGET /v1/redeems/{referenceId}
TRANSFERMYRT moved to a recipient on the customer's behalfThe recipientPOST /v1/transfersGET /v1/transfers/{referenceId}

Each read route returns only its own direction. GET /v1/transactions/{referenceId} returns an order of any direction and needs transaction:read. An order that exists but has the wrong direction for the route, or that another integration created, returns 404 NOT_FOUND, the same as an order that does not exist.

Amounts

Amounts on requests and on the order are decimal strings, never JSON numbers. decimals is always 6.

FieldMeaning
myrThe MYR side of the order. Depends on direction. null on a transfer
myrtThe MYRT side of the order
feeMyrThe fee recorded on the order, in MYR
amountRawmyrt scaled by 10^6, as an integer string. The exact on-chain value
decimalsAlways 6

What myr, myrt and feeMyr mean per direction:

directionmyrmyrtfeeMyr
MINTThe MYR you fundedEquals myr. Issuance is 1:1The payment rail charge recorded on the order. It does not reduce myrt
REDEEMThe net MYR paid out after feeMyrThe MYRT burnedThe charge myr is net of
TRANSFERnullThe MYRT moved"0"

Fees are quoted server-side and returned on the order. Partners do not compute fees.

A request amount (amountMyr on a mint, amountMyrt on a redeem or transfer) must be a positive decimal string with at most 6 decimal places. Anything else returns 400 INVALID_AMOUNT.

Amount strings are not zero-padded. 1000 and 1000.00 are the same value, and either can appear. A balance read returns "balance": "212.4" for the same amount that an error meta reports as "212.40". Never compare amount strings. Parse them with a decimal library, or compare amountRaw as integers. To match an order against an ERC-20 Transfer event, compare amountRaw with the event's value.

Do not use floating point for money

JSON numbers, and the default number type in most languages, are IEEE-754 doubles. A double cannot represent most decimal fractions exactly, and the error compounds across additions and comparisons. Keep amounts as strings until you hand them to a decimal type, or do integer arithmetic on amountRaw. Never parse an amount as a float, and never sum floats to reconcile.

Customers and corporates

customerId is an opaque identifier that MYRT assigns when a customer is linked to your integration, for example cus_9f2a3b4c. It is not a MYRT account id, an email address, or anything a customer could be recognised by. Linking happens during onboarding with your account manager, not through the API.

Every value-moving call carries customerId: mint, redeem and transfer.

ConditionResult
customerId is not linked to your integration, or does not exist404 NOT_FOUND. The two cases are indistinguishable
The customer has not cleared verification403 CUSTOMER_NOT_VERIFIED on mint, redeem and transfer

Corporates use corporateId, for example cor_71bd5e29. A corporateId appears only on GET /v1/corporates/{corporateId}.

Reading verification status for a customer or a corporate needs account:read, which is granted only under a signed data processing agreement. Without one the call returns 403 SCOPE_DENIED, even if the key was issued with the scope. Both account endpoints are documented ahead of release. Neither returns names, contact details, identity document numbers or addresses; the customerId is the only handle you hold. See accounts and corporates and the accounts reference.

Identifiers

Four identifiers appear across the API. Different parties choose them and they serve different purposes.

IdentifierChosen byFormatWhere it is used
referenceIdYou8 to 100 characters: letters, digits, ., _, -, :. Unique within your integrationThe body of every create call. The path of every read. Reconciliation. data.referenceId on webhook events
Idempotency-KeyYou1 to 255 characters. One UUID per logical operationA request header on every POST. Not part of the order object
Event idMYRTFor example evt_01J8ZQK4M7XN2REvery webhook payload. Deduplicate deliveries on it
txHashThe chain0x...blockchain.txHash on the order and on webhook payloads, once the transaction is broadcast

The difference matters when a call is retried. Sending the same Idempotency-Key with the same body replays the original status and body with X-MYRT-Idempotent-Replay: true, and nothing is created twice. The same key with a different body returns 409 IDEMPOTENCY_CONFLICT. Generate one UUID per logical operation, not per HTTP attempt, and reuse it on a timeout.

A referenceId outlives the request. For a mint or a transfer, posting an existing referenceId again returns the existing order. For a redeem, reusing a referenceId with different details returns 409 IDEMPOTENCY_CONFLICT. A referenceId that fails the format rule returns 400 INVALID_REFERENCE_ID, on reads as well. Store the referenceId before you send the request, so a timeout never leaves you with an order you cannot find. The full rules are under idempotency and reference IDs.

Two more values are safe to quote to support: the key id, the 16 hexadecimal characters in the third segment of your key, and meta.correlationId from a 500 SERVER_ERROR.

Timestamps

timestamps.createdAt and timestamps.updatedAt are ISO 8601 in UTC, for example 2026-09-10T04:12:33.918Z. createdAt is set once. updatedAt is null until the order changes for the first time, so treat null as "never updated", not as an error.

The same format is used for createdAt on webhook events, for verifiedAt on account reads (null unless verified), and for the createdAfter and createdBefore filters on GET /v1/transactions.

Two values are Unix time in seconds rather than ISO 8601: the X-RateLimit-Reset response header and the t component of the X-MYRT-Signature webhook header.

Your clock matters. Your webhook verifier should reject an event whose t is more than 300 seconds from your own time, so a skewed clock rejects valid events. GET /v1/system/time takes no key and returns serverTime with timezone: "UTC". Use it to detect skew. See server time.

Environments

  • Sandbox, https://sandbox-api.myrt.money/v1, keys start with myrt_sandbox_: a testnet deployment with test fiat rails. No real money moves.
  • Live, https://api.myrt.money/v1, keys start with myrt_live_: issues on Ethereum mainnet.

A key works only in the environment it was issued for. A sandbox key on the live host, or the reverse, returns 401 UNAUTHORIZED. The two environments settle on different chains, so never hardcode a chain id or a token address. Read them from /v1/config and /v1/tokens on the host you are calling. Base URLs and the setup sequence are on getting started. Chain details are on network configuration.

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