Skip to content

Webhooks: order events

MYRT sends an HTTP POST with a JSON body to your endpoint when a mint, redeem or transfer changes state.

Planned

This is part of the v1 contract and is documented ahead of release. It is not yet served in production. Build against it only once this notice is gone.

Until webhooks ship, poll the read endpoints, for example GET /v1/transactions/{referenceId}, to follow an order.

Registering an endpoint

Register an HTTPS endpoint with your account manager. You receive a webhook secret in return. The secret is shown once, so store it in your secrets store at registration time. It is the key you use to verify every delivery.

DANGER

Treat the webhook secret like an API key. Never put it in browser code, a mobile binary, or a public repository.

Events

Eventdata.statusMeaning
mint.pendingpendingThe mint was accepted and is awaiting settlement
mint.completedcompletedThe mint is confirmed on chain. Final
mint.failedfailedTerminal. No value moved
redeem.pendingpendingThe redeem was accepted and is awaiting settlement
redeem.completedcompletedThe burn is confirmed and the MYR payout has been initiated
redeem.failedfailedTerminal. No value moved
transfer.completedcompletedThe transfer is confirmed on chain. Final
transfer.failedfailedTerminal. No value moved

redeem.completed does not mean the customer has received MYR. Bank settlement time is outside the API. The status values are described in order lifecycle.

Payload

json
{
  "id": "evt_01J8ZQK4M7XN2R",
  "type": "mint.completed",
  "createdAt": "2026-09-10T04:19:02.441Z",
  "data": {
    "referenceId": "acme:mint:2026-09-10:00417",
    "status": "completed",
    "direction": "MINT",
    "amounts": { "myr": "1000.00", "myrt": "1000.00", "decimals": 6 },
    "blockchain": { "txHash": "0x...", "blockNumber": "20481922" }
  }
}
FieldTypeDescription
idstringEvent id, unique per event. Deduplicate on it
typestringOne of the events above
createdAtstringISO 8601 UTC time the event was created
data.referenceIdstringThe referenceId you supplied on the order
data.statusstringThe order status the event reports
data.directionstringMINT, REDEEM or TRANSFER
data.amountsobjectmyr, myrt and decimals, with the same values as amounts on the order. See amounts
data.blockchainobjecttxHash and blockNumber, with the same values as on the order

data is a subset of the order object. Read the order back for the full object, including phase, walletAddress and amountRaw.

Verifying the signature

Every delivery carries one signature header:

http
X-MYRT-Signature: t=<unix seconds>,v1=<hex>

t is a Unix timestamp in seconds. v1 is HMAC-SHA256 over the string {t}.{raw request body}, keyed with your webhook secret and hex encoded, so it is always 64 characters.

Verify every delivery in this order, and do nothing with the body until it passes:

  1. Take the request body as raw bytes. Do not parse, re-serialise or re-encode it first. A single changed byte changes the signature.
  2. Split the header on , and read t and v1.
  3. Reject the delivery if |now - t| is more than 300 seconds. If you see rejections here, check your clock against GET /v1/system/time.
  4. Compute HMAC-SHA256 over t, a . and the raw body, keyed with the secret, and hex encode it.
  5. Compare the result with v1 using a constant-time comparison. Reject on any mismatch.
  6. Only then parse the JSON.

Both verifiers below take the secret as an argument and return true only when every step passes. The optional now argument exists so you can run the test vector below.

ts
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

/**
 * rawBody  the request body exactly as received, before JSON parsing
 * header   the value of the X-MYRT-Signature header
 * secret   your webhook secret
 * now      current Unix time in seconds; override it only to run the test vector
 */
export function verifyMyrtWebhook(
  rawBody: Buffer,
  header: string | undefined,
  secret: string,
  now: number = Math.floor(Date.now() / 1000),
): boolean {
  if (!header) return false;

  const parts = new Map<string, string>();
  for (const pair of header.split(",")) {
    const i = pair.indexOf("=");
    if (i > 0) parts.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim());
  }
  const t = parts.get("t");
  const v1 = parts.get("v1");
  if (!t || !v1) return false;

  const timestamp = Number(t);
  if (!Number.isInteger(timestamp)) return false;
  if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.`)
    .update(rawBody)
    .digest("hex");

  const expectedBytes = Buffer.from(expected, "utf8");
  const receivedBytes = Buffer.from(v1, "utf8");
  if (expectedBytes.length !== receivedBytes.length) return false;
  return timingSafeEqual(expectedBytes, receivedBytes);
}
python
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify_myrt_webhook(raw_body: bytes, header: str, secret: str, now=None) -> bool:
    """
    raw_body  the request body exactly as received, before JSON parsing
    header    the value of the X-MYRT-Signature header
    secret    your webhook secret
    now       current Unix time in seconds; set it only to run the test vector
    """
    if not header:
        return False

    parts = {}
    for pair in header.split(","):
        key, sep, value = pair.partition("=")
        if sep:
            parts[key.strip()] = value.strip()
    t = parts.get("t")
    v1 = parts.get("v1")
    if not t or not v1 or not t.isdigit():
        return False

    if now is None:
        now = int(time.time())
    if abs(now - int(t)) > TOLERANCE_SECONDS:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        t.encode("utf-8") + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected.encode("utf-8"), v1.encode("utf-8"))

Test vector

Use this vector to check your verifier before you register an endpoint.

InputValue
Secretwhsec_0123456789abcdef0123456789abcdef
t1789013942

Raw body, one line, exactly these bytes:

json
{"id":"evt_01J8ZQK4M7XN2R","type":"mint.completed","createdAt":"2026-09-10T04:19:02.441Z","data":{"referenceId":"acme:mint:2026-09-10:00417","status":"completed","direction":"MINT","amounts":{"myr":"1000.00","myrt":"1000.00","decimals":6},"blockchain":{"txHash":"0x0000000000000000000000000000000000000000000000000000000000000001","blockNumber":"20481922"}}}

Expected header:

http
X-MYRT-Signature: t=1789013942,v1=3d8aab65751aa498030f2f0f701b2d0dd00cd5ca264f3c44504fa73891c878dd

A verifier that accepts this vector and rejects the same body with any byte changed is correct. Pass 1789013942 as now when you run it: the t in the vector is fixed, so the real clock would fail the 300 second tolerance check. The txHash here is written out in full rather than as 0x... because the signature covers the exact bytes.

Delivery rules

RuleWhat it means for you
Acknowledge within 5 secondsRespond 2xx as soon as you have stored the event. Queue the work and process it asynchronously
RetriesA delivery that is not acknowledged with a 2xx is retried with exponential backoff for up to 24 hours
At-least-onceThe same event can arrive more than once. Deduplicate on id
No ordering guaranteeEvents can arrive out of order. Do not infer state from the sequence; the order's status is the current truth
Read back before creditingTreat a webhook as a hint. On any event, read the order with the API before you credit anyone

GET /v1/transactions/{referenceId} returns any order you created, whatever its direction, and needs transaction:read. See the transactions reference.

bash
curl https://sandbox-api.myrt.money/v1/transactions/acme:mint:2026-09-10:00417 \
  -H "Authorization: Bearer $MYRT_API_KEY"

Act on the status in that response, not on the event.

Failure handling

If your endpoint is down, every delivery in that window fails and MYRT retries each one with exponential backoff for up to 24 hours. After 24 hours an event is no longer retried. Plan for both cases.

  1. Endpoint back within 24 hours. Expect a burst of retried events, out of order and possibly duplicated. Deduplicate on id, read each order back, and act on its status.
  2. Longer outage, or any doubt. Reconcile against the API. List your orders with GET /v1/transactions (planned), or walk the referenceId values you stored before sending and read each one with GET /v1/transactions/{referenceId}.

Store every referenceId before you send the order; it is your handle for every read. Reconciliation should be routine rather than an incident response, so run it daily. See best practices.

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