Skip to main content

Refunds

Refunds​

Refund a wallet-collected payin order (full or partial) in three write steps:

  1. Quote Refund — price the refund; no pending lock.
  2. Prepare Refund — lock the quote and receive param_json.
  3. Authorize Refund — stamp param_json once. Morph broadcasts. There is no public submit API.

Send optional refund_amount on quote for a partial refund. Omit it to refund the original order amount, not the remaining refundable balance. After one successful partial refund, omitting refund_amount is rejected with 90211 — refund the remainder by quoting with Get Refund Info refundable_amount.

Wallet-collected orders (collect_mode=wallet) derive the destination from the original payer. Address-collected orders (collect_mode=hd_address) do not: quote must send refund_to_address. On any other order that field is ignored.

Read endpoints:

  • List Refunds — page through refunds; poll by refund_id here.
  • Get Refund Info — ledger for one payin platform_order_id (refundable / refunded totals). Not a lookup by refund_id.

Stamp param_json as described in Merchant stamp. sign_mode must be waas.

All requests must be authenticated with your API Key. Do not send merchant_id.

Pick a payin order with status=success and remaining refundable balance (List Orders, Get Refund Info). Already fully refunded orders return 90210.

Base URL​

https://pay-openapi.morph.network/pro/merchant/payout/v1

Endpoint paths in this page are relative to that base URL.

Response envelope​

{
"error_code": 0,
"msg": "success",
"data": {},
"trace": "..."
}
note

Business success or failure is determined by error_code (error_code == 0 means success), not by the HTTP status code alone. See Error Codes.

Quote Refund​

POST /refunds/quote

Read-only pricing. No refund row, no pending slot. You may call this repeatedly.

Each quote creates a new refund_id and a new cashier soft order. Sending a previous refund_id / cashier_refund_txn_id does not reuse the session. To refresh the price, quote again and use the latest refund_id on prepare and authorize.

Request fields​

FieldTypeRequiredDescription
platform_order_idstringYesOriginal payin order (ord_…). Must be paid and still refundable.
refund_amountstringNoAmount for this refund. Plain decimal, at most 8 decimal places, must be positive. Cap is order amount − successful refunds; over the cap returns 90211. Omit to refund the original order amount (not the remainder). Do not send an empty string.
refund_to_addressstringHD ordersDestination for an address-collected payin (collect_mode=hd_address). Required on those orders; ignored otherwise. Same-chain refunds use the receiving-chain address format; cross-chain refunds use the payer-chain format. Must not be the order's collect address. A missing or invalid address returns 90214. EVM addresses are lowercased before they are forwarded.
refund_idstringNoFrom a previous quote. Currently does not reuse the session.
cashier_refund_txn_idstringNoSame as refund_id — optional; currently ignored for session reuse.

Success response​

data includes refund_id (rfd_*), cashier_refund_txn_id (BGW cashier id, shaped like P… — not csh_), refund_amount (this quote's amount), refund_from_* / refund_to_*, service_fee, payer_total_amount, quote_expire_at. There is no WaaS hash on quote.

An expired quote fails prepare with 90212 — quote again.

{
"error_code": 0,
"msg": "success",
"data": {
"refund_id": "rfd_...",
"cashier_refund_txn_id": "P...",
"refund_amount": "10.00",
"refund_from_network": "morph",
"refund_from_token_symbol": "USDT",
"refund_from_token_address": "0xToken...",
"refund_from_address": "0xabc1...def0",
"refund_to_network": "morph",
"refund_to_token_symbol": "USDT",
"refund_to_token_address": "0xToken...",
"refund_to_address": "0xPayer...",
"service_fee": {
"amount": "0.20",
"chain": "morph",
"currency": "USDT",
"token_address": "0xToken..."
},
"payer_total_amount": "10.20",
"quote_expire_at": 1782777900000
},
"trace": "..."
}

Prepare Refund​

POST /refunds/prepare

Requires header X-Idempotency-Key (≤128 characters).

Repeating prepare rebuilds BGW unsigned txs and changes items[].ref. Authorize must use the latest prepare response.

refund_intent.refund_from_address locates the hosted paying wallet. It must belong to you (90112 otherwise).

Request fields​

FieldTypeRequiredDescription
refund_idstringYesFrom the latest quote.
refund_intentobjectYesEcho the quote snapshot (refund_amount, refund_from_*, refund_to_*, service_fee, payer_total_amount).

If the downstream confirm returns a different refund_id (the order already has an in-flight refund), prepare returns 90213 and does not issue param_json.

Success response​

FieldDescription
refund_id / cashier_refund_txn_idEcho; cashier id is the BGW P… value to send on authorize
param_json / param_hashStamp param_json bound to waas_timestamp (see Merchant stamp). param_hash is not the signed bytes
items[]{ref, chain, param_indexes[], raw_txs?}. ref is the BGW payment tx id. Unsigned paymentTxs are not returned
securityFour risk-control headers

param_indexes are the positions of that ref inside param_json.items. EVM/SOL: one index per item. TRX: one ref may cover several sign items. Echo them on authorize.

raw_txs is TRX-only: the unsigned tx object or array. You must echo it; its shape is what gets broadcast.

Authorize Refund​

POST /refunds/authorize

One merchant_sign for the whole batch. items must match the latest prepare (count and ref set).

Request fields​

FieldTypeRequiredDescription
refund_idstringYesFrom prepare.
cashier_refund_txn_idstringYesFrom prepare (P…). Do not invent a csh_ id.
param_jsonstringYesByte-for-byte echo of prepare.
merchant_signstringYesEd25519 over the raw 32-byte SHA-256 of param_json, a vertical bar, and waas_timestamp. Base64. Empty → 90606. See Merchant stamp.
waas_timestampstringYes13-digit Unix milliseconds folded into merchant_sign. Morph sends this value as X-Waas-Timestamp. Missing or not 13 digits → 90000. Outside ±60 seconds → 90604.
items[]object[]YesEcho each prepare item: ref, chain, param_indexes, and TRX raw_txs.
source_addressstringYesSame as refund_intent.refund_from_address.
sign_modestringYeswaas.
{
"refund_id": "rfd_...",
"cashier_refund_txn_id": "P...",
"param_json": "{\"items\":[...]}",
"merchant_sign": "base64...",
"waas_timestamp": "1700000000000",
"source_address": "0xabc1...def0",
"sign_mode": "waas",
"items": [
{ "ref": "tx_...", "chain": "morph", "param_indexes": [0] }
]
}

Success response​

data includes refund_id and status (typically refund_pending). Poll List Refunds until refund_status is success or failed. Do not poll Get Refund Info with refund_id — that endpoint requires the original payin platform_order_id and otherwise returns 90201.

Refund terminal status is not sent on the payin order webhook.

List Refunds​

GET /refunds

Returns your refund records in reverse-chronological order by default, with cursor-based pagination. Omit refund_status to return refunds in every status.

Query parameters​

ParameterTypeRequiredDescription
refund_statusstringNoOne of pending, success, failed; omit to return all statuses.
startstringNoRange start. Accepted but not yet effective downstream; reserved for forward compatibility. Max 32 characters.
endstringNoRange end. Accepted but not yet effective downstream; reserved for forward compatibility. Max 32 characters.
sort_bystringNoSort field. Accepted but not yet effective downstream; reserved for forward compatibility. Max 32 characters.
orderstringNoSort direction. One of asc, desc (default). Accepted but not yet effective downstream.
cursorstringNoPagination cursor; omit for the first page. Max 512 characters.
limitintegerNoPage size. 1–100, default 20 when omitted.
note

Any value outside the ranges/enums above is rejected with error_code: 90000 before the request reaches order-service — you will not get a partial or best-effort match. start/end/sort_by are validated for shape but the downstream refund list does not yet filter/sort by them.

Request example​

GET /pro/merchant/payout/v1/refunds?refund_status=success&limit=20

Success response​

{
"error_code": 0,
"msg": "success",
"data": {
"items": [
{
"refund_id": "rfd_xxx",
"platform_order_id": "ord_xxx",
"refund_amount": "10.00",
"payer_total_amount": "10.20",
"service_fee": "0.20",
"refund_from_network": "ETH",
"refund_from_token_symbol": "USDT",
"refund_to_network": "ETH",
"refund_to_token_symbol": "USDT",
"refund_tx_hash": "0x...",
"status": "success",
"created_at": 1780000000000,
"updated_at": 1780000001000
}
],
"next_cursor": null
},
"trace": "..."
}

To fetch the next page, pass the returned next_cursor on the next request. An empty or missing next_cursor means there are no more results.

note

The fields shown above are the stable query-side fields. Each items[] entry may carry additional refund detail fields; treat the refund record itself as the source of truth for those.

Get Refund Info​

GET /refunds/info

Aggregate refund ledger for one payin order: order amount, refundable balance, refunded total, and per-refund rows. Use refundable_amount as refund_amount on the next quote when you want to refund the remainder.

Query parameters​

ParameterTypeRequiredDescription
platform_order_idstringYesOriginal payin order id (ord_…). Not a refund_id.

Omitting platform_order_id, or passing a refund_id / unknown id, returns 90201.

Request example​

GET /pro/merchant/payout/v1/refunds/info?platform_order_id=ord_xxx

Success response​

{
"error_code": 0,
"msg": "success",
"data": {
"platform_order_id": "ord_xxx",
"order_amount": "10.00",
"refundable_amount": "0.00",
"refunded_amount": "10.00",
"refunds": [
{
"refund_id": "rfd_xxx",
"status": "success",
"refund_amount": "10.00",
"payer_total_amount": "10.20",
"tx_hash": "0x..."
}
]
},
"trace": "..."
}

Errors​

error_codeMeaningDescription
0Success—
90000Invalid parametersMissing field, sign_mode not waas, missing or overlong X-Idempotency-Key, or a query enum/range error.
90001UnauthorizedAuthentication failed or merchant identity is missing.
90112Wallet not foundrefund_from_address / source_address is not a hosted wallet on your account.
90201Order not foundrefunds/info platform_order_id is not a payin order on your account (including if you passed a refund_id).
90207Insufficient balanceHosted wallet cannot cover refund principal plus fees.
90210Refund not allowedOrder not success, already fully refunded, or otherwise not refundable.
90211Invalid refund amountAmount format is invalid, exceeds remaining refundable balance, or refund_amount was omitted after a successful partial refund.
90212Quote expiredQuote again, then prepare with the new refund_id.
90213Pending refund existsThe order already has an in-flight refund, or prepare echoed a different refund_id. No param_json is issued.
90309Security check failedRisk-control headers on prepare, or transaction / stamp verification on authorize.
90604Invalid merchant stampStamp failed, or echoed param_json / TRX raw_txs do not match prepare.
90605Origin key missingBind an origin key in Console first.
90606Missing merchant stampmerchant_sign empty.
80000 – 80099System errorRetry later; include trace if it persists.

See the Error Codes reference for details.