Refunds
Refunds
Refund a wallet-collected payin order (full or partial) in three write steps:
- Quote Refund — price the refund; no pending lock.
- Prepare Refund — lock the quote and receive
param_json. - Authorize Refund — stamp
param_jsononce. Morph broadcasts. There is no publicsubmitAPI.
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_idhere. - Get Refund Info — ledger for one payin
platform_order_id(refundable / refunded totals). Not a lookup byrefund_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": "..."
}
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
| Field | Type | Required | Description |
|---|---|---|---|
platform_order_id | string | Yes | Original payin order (ord_…). Must be paid and still refundable. |
refund_amount | string | No | Amount 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_address | string | HD orders | Destination 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_id | string | No | From a previous quote. Currently does not reuse the session. |
cashier_refund_txn_id | string | No | Same 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
| Field | Type | Required | Description |
|---|---|---|---|
refund_id | string | Yes | From the latest quote. |
refund_intent | object | Yes | Echo 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
| Field | Description |
|---|---|
refund_id / cashier_refund_txn_id | Echo; cashier id is the BGW P… value to send on authorize |
param_json / param_hash | Stamp 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 |
security | Four 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
| Field | Type | Required | Description |
|---|---|---|---|
refund_id | string | Yes | From prepare. |
cashier_refund_txn_id | string | Yes | From prepare (P…). Do not invent a csh_ id. |
param_json | string | Yes | Byte-for-byte echo of prepare. |
merchant_sign | string | Yes | Ed25519 over the raw 32-byte SHA-256 of param_json, a vertical bar, and waas_timestamp. Base64. Empty → 90606. See Merchant stamp. |
waas_timestamp | string | Yes | 13-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[] | Yes | Echo each prepare item: ref, chain, param_indexes, and TRX raw_txs. |
source_address | string | Yes | Same as refund_intent.refund_from_address. |
sign_mode | string | Yes | waas. |
{
"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
| Parameter | Type | Required | Description |
|---|---|---|---|
refund_status | string | No | One of pending, success, failed; omit to return all statuses. |
start | string | No | Range start. Accepted but not yet effective downstream; reserved for forward compatibility. Max 32 characters. |
end | string | No | Range end. Accepted but not yet effective downstream; reserved for forward compatibility. Max 32 characters. |
sort_by | string | No | Sort field. Accepted but not yet effective downstream; reserved for forward compatibility. Max 32 characters. |
order | string | No | Sort direction. One of asc, desc (default). Accepted but not yet effective downstream. |
cursor | string | No | Pagination cursor; omit for the first page. Max 512 characters. |
limit | integer | No | Page size. 1–100, default 20 when omitted. |
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.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
platform_order_id | string | Yes | Original 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_code | Meaning | Description |
|---|---|---|
0 | Success | — |
90000 | Invalid parameters | Missing field, sign_mode not waas, missing or overlong X-Idempotency-Key, or a query enum/range error. |
90001 | Unauthorized | Authentication failed or merchant identity is missing. |
90112 | Wallet not found | refund_from_address / source_address is not a hosted wallet on your account. |
90201 | Order not found | refunds/info platform_order_id is not a payin order on your account (including if you passed a refund_id). |
90207 | Insufficient balance | Hosted wallet cannot cover refund principal plus fees. |
90210 | Refund not allowed | Order not success, already fully refunded, or otherwise not refundable. |
90211 | Invalid refund amount | Amount format is invalid, exceeds remaining refundable balance, or refund_amount was omitted after a successful partial refund. |
90212 | Quote expired | Quote again, then prepare with the new refund_id. |
90213 | Pending refund exists | The order already has an in-flight refund, or prepare echoed a different refund_id. No param_json is issued. |
90309 | Security check failed | Risk-control headers on prepare, or transaction / stamp verification on authorize. |
90604 | Invalid merchant stamp | Stamp failed, or echoed param_json / TRX raw_txs do not match prepare. |
90605 | Origin key missing | Bind an origin key in Console first. |
90606 | Missing merchant stamp | merchant_sign empty. |
80000 – 80099 | System error | Retry later; include trace if it persists. |
See the Error Codes reference for details.