Skip to main content

Merchant Stamp

Merchant stamp

Payout and refund write APIs add a second signature on top of API Key HMAC. Morph's hosted wallet produces the on-chain signature. You only stamp a string Morph already prepared — param_json — with the origin key bound in the merchant Console.

There is no public submit endpoint, and no OTP / 2FA on the OpenAPI path. sign_mode must be waas.

Before you stamp

  1. Create an APIKey / APISecret at account.morph.network.
  2. Bind an origin key in the Console (SDK iframe). OpenAPI does not accept a key_id; Morph uses the origin key you selected for the merchant.
  3. Confirm the paying address is a hosted wallet under your account via GET /payout/v1/wallets.

A missing origin key on authorize returns 90605.

What you sign

Step ② of a payout (Create Payout) or a refund (Prepare Refund) returns:

  • param_json — a string. The same envelope on EVM, SOL, and TRX, even for a single item:
{"items":[{"operation":"signTransaction","param":"<unsigned payload JSON string>"}]}
  • param_hashlowercaseHex(SHA256(utf8(param_json))), 64 hex characters. Convenience only. Do not stamp param_hash.

TRX payouts put 2–3 items in param_json.items. You still stamp the whole string once. Each TRX param is only { "chain": "trx", "data": "<hex>" }. Create also returns a sibling raw_txs array (unsigned txs with raw_data). Echo raw_txs on authorize; do not fold it into param_json.

How to compute merchant_sign

Mint a 13-digit Unix millisecond timestamp, then sign param_json bound to that value. Send the same digits as waas_timestamp on authorize. Morph writes them to X-Waas-Timestamp when it calls the hosted wallet. The stamp and the header must be the same string.

waas_timestamp = "<13-digit unix milliseconds>"
digest = SHA256(utf8(param_json + "|" + waas_timestamp)) // 32 raw bytes
merchant_sign = Base64(Ed25519.Sign(originPriv, digest))

Rules:

  1. Sign the 32-byte digest, not a hex encoding of it, and not param_hash.
  2. waas_timestamp is required, exactly 13 digits. Missing or malformed values return 90000. The hosted wallet accepts the header only within ±60 seconds, so mint the timestamp when you authorize. A mismatch returns 90604.
  3. On step ③, echo param_json byte-for-byte. Do not parse and JSON.stringify, pretty-print, reorder keys, or change whitespace. A rewrite returns 90604.
  4. intent_hash on preview (if present) is a pay-service business hash. Do not stamp it.
  5. data.security (security_check / security_request_check / security_double_check / verify_result) is a risk-control echo. You do not have to verify it; integrity is the merchant stamp.

An empty merchant_sign returns 90606.

Go

msg := paramJSON + "|" + waasTimestamp
sum := sha256.Sum256([]byte(msg)) // 32 bytes
sig := ed25519.Sign(originPriv, sum[:])
merchantSign := base64.StdEncoding.EncodeToString(sig)

Node.js

const digest = crypto.createHash("sha256").update(`${paramJson}|${waasTimestamp}`, "utf8").digest();
const sig = nacl.sign.detached(digest, originPriv);
const merchantSign = Buffer.from(sig).toString("base64");

List wallets

GET /pro/merchant/payout/v1/wallets

No query parameters. Use this to confirm tenant_id / tee_mid and the hosted addresses you may pass as source_wallet.address / source_address. It does not rotate on-chain keys.

{
"error_code": 0,
"msg": "success",
"data": {
"merchant_id": "mch_...",
"tenant_id": "...",
"tee_mid": "...",
"two_fa_bound": false,
"two_fa_type": "",
"wallets": [
{ "wallet_id": "...", "chain_type": "evm", "address": "0xabc1...def0" }
]
},
"trace": "..."
}

Next steps

  • Payouts — preview → create → authorize.
  • Refunds — quote → prepare → authorize.