Invoices & Payment Links
Invoices & Payment Links
The Morph Payments API exposes these endpoints for collecting a payment:
- Create Payment Link — a shareable checkout URL for a fixed amount.
- Create Invoice — a payment link plus payer details, a due date, and optional line items.
- Get / update an existing Payment Link or Invoice. What you can change
depends on
edit_statefrom the detail response. - Download Invoice PDF — a download URL for the invoice document.
All requests must be authenticated with your
API Key. The gateway resolves your merchant identity from the API Key, so you do
not send merchant_id in the body or query — it is ignored if present.
Base URL
https://pay-openapi.morph.network/pro/merchant/payin/v1
Endpoint paths in this page are relative to that base URL.
Response envelope
Every response is wrapped in a common 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 the
Error Codes reference for the full list.
Validation rules
Before a payment resource is created, the request is validated. If any check fails, the corresponding error code is returned and nothing is created:
- Receiving token & chain —
(receiving_network, receiving_token_symbol, receiving_token_address)must match a supported, active entry in the Supported Tokens & Networks list. An unsupported chain returns90111; a token that does not match the chain returns90120. - Receiving wallet —
receiving_addressmust be a receiving wallet you have configured at https://account.morph.network/ under your merchant account, and it must be bound toreceiving_network. An address that is not yours returns90112; a wallet not bound to that chain returns90111. - Payer allowlist —
support_networksandsupport_tokenslimit which chains and tokens the customer can pick on Hosted Checkout. They come from Paying currencies, not from your receiving catalog.- Omit both (or send empty arrays) to allow every paying-catalog chain and token. The API expands the pair before create; pay-service never stores a blank allowlist.
- Tokens only —
support_networksis filled from the distinctsupport_tokens[].networkvalues. - Networks only —
support_tokensis filled from the paying catalog for those chains. A selected chain with no catalog token returns90000. - Both set — forwarded as-is. Each token must match the paying catalog
(
90111/90120). The two fields must be a consistent pair (support_networksequals the distinct token networks).
Create Payment Link
POST /payment-links
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Payment amount. |
receiving_network | string | Yes | Primary receiving chain. |
receiving_token_symbol | string | Yes | Primary receiving token symbol. |
receiving_token_address | string | Yes | Primary receiving token contract address. |
receiving_address | string | Yes | Receiving address — must be a wallet configured at account.morph.network and bound to the chain. |
description | string | No | Description. |
name | string | No | Display name. Omit to leave it unset. Invoices do not have this field. |
return_url | string | No | HTTPS URL the Hosted Checkout success page uses for “Back to Merchant”. See Return URL. |
support_networks | string[] | No | Checkout chains. Omit with support_tokens to allow the full paying catalog. |
support_tokens | object[] | No | Checkout tokens (network / address / symbol). Must form a pair with support_networks when either is set. |
Request example
{
"amount": "100.00",
"receiving_network": "morph",
"receiving_token_symbol": "USDT",
"receiving_token_address": "0xe7cd86e13ac4309349f30b3435a9d337750fc82d",
"receiving_address": "0xdef2...7890",
"description": "July subscription",
"name": "July subscription",
"return_url": "https://merchant.example.com/paid",
"support_networks": ["morph"],
"support_tokens": [
{
"network": "morph",
"address": "0xe7cd86e13ac4309349f30b3435a9d337750fc82d",
"symbol": "USDT"
}
]
}
Success response
{
"error_code": 0,
"msg": "success",
"data": {
"payin_id": "plink_...",
"payment_link_url": "https://s.short/abc",
"payment_link_long_url": "https://pay.example.com/checkout/eyJ...",
"status": "ACTIVE",
"name": "July subscription",
"description": "July subscription",
"receiving_source_type": "tee",
"return_url": "https://merchant.example.com/paid"
},
"trace": "..."
}
Create does not return edit_state or success_order_count. Those are on
Get Payment Link. receiving_source_type is tee or
external, taken from the receiving wallet you sent; the request body has no
such field. Create always returns both checkout URLs.
payment_link_long_url is {checkout base}/checkout/{token}.
payment_link_url is the URL returned by the short-link service
(data.shortUrl[0].url). Pay does not choose that host or path. The two URLs
are equal only when the short-link service is unset or the call fails.
Omitted name, description, and return_url come back as JSON null.
Create Invoice
POST /invoices
An invoice takes every Payment Link field except name, plus the
invoice-specific fields payer, due_date, and items.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Payment amount. |
receiving_network | string | Yes | Primary receiving chain. |
receiving_token_symbol | string | Yes | Primary receiving token symbol. |
receiving_token_address | string | Yes | Primary receiving token contract address. |
receiving_address | string | Yes | Receiving address — must be a wallet configured at account.morph.network and bound to the chain. |
description | string | No | Description. |
support_networks | string[] | No | Checkout chains. Omit with support_tokens to allow the full paying catalog. |
support_tokens | object[] | No | Checkout tokens (network / address / symbol). Must form a pair with support_networks when either is set. |
payer | object | Yes | Payer information. |
payer.name | string | Yes | Payer name. |
payer.email | string | No | Valid email address. |
due_date | int64 | Yes | Invoice due time (Unix millisecond timestamp). |
items | object[] | No | Invoice line items. |
items[].name | string | Yes | Item name. |
items[].quantity | int64 | Yes | Quantity, > 0. |
items[].unit_price | string | Yes | Unit price. |
reference_id | string | No | Your own business reference for this invoice, up to 256 characters. Echoed back in the response and in the webhook payload for the resulting order. |
return_url | string | No | Same as Create Payment Link. Not copied onto the order or the webhook. |
Only name, quantity, and unit_price are read from each items[] entry;
any other fields are discarded.
Request example
{
"amount": "299.00",
"receiving_network": "morph",
"receiving_token_symbol": "USDT",
"receiving_token_address": "0xe7cd86e13ac4309349f30b3435a9d337750fc82d",
"receiving_address": "0xdef2...7890",
"description": "Annual plan",
"payer": {
"name": "Payer Name",
"email": "[email protected]"
},
"due_date": 1782777600000,
"items": [
{
"name": "Annual plan",
"quantity": 1,
"unit_price": "299.00"
}
],
"support_networks": ["morph"],
"support_tokens": [
{
"network": "morph",
"address": "0xe7cd86e13ac4309349f30b3435a9d337750fc82d",
"symbol": "USDT"
}
],
"reference_id": "your-ref-...",
"return_url": "https://merchant.example.com/paid"
}
Success response
{
"error_code": 0,
"msg": "success",
"data": {
"invoice_id": "INV-...",
"checkout_url": "https://s.short/abc",
"checkout_long_url": "https://pay.example.com/checkout/eyJ...",
"due_date": 1782777600000,
"reference_id": "your-ref-...",
"receiving_source_type": "tee",
"return_url": "https://merchant.example.com/paid"
},
"trace": "..."
}
Create does not return status or edit_state. Those are on
Get Invoice. Create always returns both checkout_url (short)
and checkout_long_url ({checkout base}/checkout/{token}), same rules as a
Payment Link.
receiving_source_type is set the same way. An omitted return_url comes back
as JSON null.
Return URL
return_url is optional on create and update for both Payment Links and
Invoices.
| Rule | Behavior |
|---|---|
Omit, null, or "" on create | The resource is stored without a return URL. Create and detail return JSON null. |
| Value | Must be an https URL with a host, at most 2048 characters after trim. http, javascript:, ftp:, and values with no host return 90000. |
| Update | Omit to keep the stored value. "" clears it. |
| Checkout | The success page shows “Back to Merchant” and redirects there after 5 seconds. The URL is not rewritten and no order id is appended. |
| Order and webhook | return_url is not copied onto the order and is not included in webhook payloads. |
Edit state
Get and successful update responses include data.edit_state. Create does not.
Read it before you update; do not infer the lock from order counts yourself.
| Field | Description |
|---|---|
code | FULLY_EDITABLE, PARTIALLY_LOCKED (Payment Link only), or FULLY_LOCKED (Invoice only). |
editable | false only when the invoice is fully locked. |
editable_fields | Fields you may change. Empty array when nothing is editable. |
locked_fields | Fields that must stay as stored. |
lock_reason | null when unlocked. Otherwise HAS_SUCCESS_ORDER, SUBMITTED_IN_FLIGHT, PAID, or EXPIRED. |
can_duplicate | Always true. Duplicating is a new create, not an update. |
Payment Link:
| Situation | code | You can change | lock_reason |
|---|---|---|---|
| No successful order and no submitted in-flight order | FULLY_EDITABLE | Every field | null |
| At least one successful order | PARTIALLY_LOCKED | name, description, return_url, status | HAS_SUCCESS_ORDER |
| A submitted in-flight order, and no successful order | PARTIALLY_LOCKED | name, description, return_url, status | SUBMITTED_IN_FLIGHT |
HAS_SUCCESS_ORDER wins over SUBMITTED_IN_FLIGHT.
Invoice is all or nothing:
| Situation | code | lock_reason |
|---|---|---|
| Unpaid, not expired, no submitted in-flight order | FULLY_EDITABLE | null |
| Paid | FULLY_LOCKED | PAID |
| Submitted in-flight, and not paid | FULLY_LOCKED | SUBMITTED_IN_FLIGHT |
due_date is in the past, and not paid or in-flight | FULLY_LOCKED | EXPIRED |
PAID wins over SUBMITTED_IN_FLIGHT, which wins over EXPIRED. A locked
invoice rejects the whole update with 90209.
Get Payment Link
GET /payment-links/detail?payin_id=<payin_id>
Pass payin_id as a query parameter. Do not put it in the path.
Query fields
| Field | Type | Required | Description |
|---|---|---|---|
payin_id | string | Yes | Payment Link id returned by create. |
Success response
data is the stored link: payin_id, merchant_id, amount, receiving fields,
support_networks, support_tokens, name, description, return_url,
payment_link_url (the short URL only; payment_link_long_url is not on this
response), status (ACTIVE or INACTIVE), receiving_source_type
(tee for a hosted wallet, external for your own receiving address),
success_order_count, created_at, updated_at, and edit_state.
return_url is JSON null when unset. An unknown id, including one that
belongs to another merchant, returns 90201.
Update Payment Link
POST /payment-links/update
payin_id is required. Other fields are optional, with these rules:
- Omit a field to keep the stored value.
name,description, andreturn_url: omit to keep, send""to clear.support_networksandsupport_tokens: omit to keep. Send[]to clear the allowlist (create would expand an empty allowlist; update does not).- While
FULLY_EDITABLE, resendamountand everyreceiving_*from the detail. Omitting them returns90000. - While
PARTIALLY_LOCKED, omit the locked amount and receiving fields, or send the stored values unchanged. A different value returns90209.name,description,return_url, andstatuscan still change. - If
receiving_networkorreceiving_addressdiffers from the stored pair, the address is checked again (90112if it is not your wallet,90111if it is not bound to that chain). Sending the stored pair skips that check. - If you send any of
receiving_network,receiving_token_symbol, orreceiving_token_address, send all three. An incomplete or unknown token returns90120.
status here is ACTIVE or INACTIVE. Prefer
Update Payment Link status when you only want
to change status.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
payin_id | string | Yes | Payment Link id. |
amount | string | Conditional | Required on a fully editable link. |
receiving_network | string | Conditional | Required on a fully editable link. |
receiving_token_symbol | string | Conditional | Required on a fully editable link. |
receiving_token_address | string | Conditional | Required on a fully editable link. |
receiving_address | string | Conditional | Required on a fully editable link. |
name | string | No | Display name. "" clears it. |
description | string | No | Description. "" clears it. |
return_url | string | No | HTTPS return URL. "" clears it. See Return URL. |
support_networks | string[] | No | Checkout chains. [] clears the allowlist. |
support_tokens | object[] | No | Checkout tokens. [] clears the allowlist. |
status | string | No | ACTIVE or INACTIVE. |
Request example
Fully editable link, amount changed, receiving fields echoed:
{
"payin_id": "plink_...",
"amount": "0.02",
"receiving_network": "morph",
"receiving_token_symbol": "USDT",
"receiving_token_address": "0xe7cd86e13ac4309349f30b3435a9d337750fc82d",
"receiving_address": "0xdef2...7890",
"description": "amount-changed"
}
Success response
data has the same shape as Get Payment Link, with the
updated values and a fresh edit_state.
Update Payment Link status
POST /payment-links/status
Changes only ACTIVE / INACTIVE. This stays allowed when the link is
PARTIALLY_LOCKED. Any other status returns 90000.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
payin_id | string | Yes | Payment Link id. |
status | string | Yes | ACTIVE or INACTIVE. |
Request example
{
"payin_id": "plink_...",
"status": "INACTIVE"
}
Get Invoice
GET /invoices/detail?invoice_id=<invoice_id>
Pass invoice_id as a query parameter.
Query fields
| Field | Type | Required | Description |
|---|---|---|---|
invoice_id | string | Yes | Invoice id returned by create. |
Success response
data is the stored invoice: invoice_id, merchant_id, amount, receiving
fields, support_networks, support_tokens, description, payer,
due_date, items, checkout_url (the short URL only; checkout_long_url is not on this
response), reference_id, status (Pending,
Paid, or Expired), receiving_source_type (tee or external),
return_url, success_order_count (0 or 1), created_at, updated_at,
and edit_state. Invoices have no name. return_url is JSON null when
unset. An unknown id, including one that belongs to another merchant, returns
90201.
Update Invoice
POST /invoices/update
The body mirrors Create Invoice, plus required
invoice_id. While FULLY_EDITABLE you must resend amount, every
receiving_*, payer.name, and due_date. Optional fields follow the same
omit / "" / [] rules as Payment Link update.
due_date echoed from the detail skips the window check. A new due_date
must be at least 5 minutes ahead and at most 90 days ahead; otherwise the
update returns 90000 and the stored due time is unchanged.
A FULLY_LOCKED invoice returns 90209 for the whole request, including a
description-only change. Receiving-address checks match Payment Link update.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
invoice_id | string | Yes | Invoice id. |
amount | string | Yes | Payment amount. Echo the stored value if you are not changing it. |
receiving_network | string | Yes | Primary receiving chain. |
receiving_token_symbol | string | Yes | Primary receiving token symbol. |
receiving_token_address | string | Yes | Primary receiving token contract address. |
receiving_address | string | Yes | Receiving address. |
payer | object | Yes | Payer information. |
payer.name | string | Yes | Payer name. |
payer.email | string | No | Valid email address. |
due_date | int64 | Yes | Due time in Unix milliseconds. Echo the stored value to leave it unchanged. |
description | string | No | Description. "" clears it. |
return_url | string | No | HTTPS return URL. Omit to keep; "" clears it. |
items | object[] | No | Line items. Omit to keep; [] clears. |
support_networks | string[] | No | Checkout chains. |
support_tokens | object[] | No | Checkout tokens. |
reference_id | string | No | Business reference, up to 256 characters. "" clears it. |
Request example
{
"invoice_id": "INV-...",
"amount": "299.00",
"receiving_network": "morph",
"receiving_token_symbol": "USDT",
"receiving_token_address": "0xe7cd86e13ac4309349f30b3435a9d337750fc82d",
"receiving_address": "0xdef2...7890",
"payer": {
"name": "Payer Name",
"email": "[email protected]"
},
"due_date": 1782777600000,
"description": "after"
}
Success response
data has the same shape as Get Invoice.
Download Invoice PDF
GET /invoices/pdf?invoice_id=<invoice_id>
Renders one PDF for Pending, Paid, and Expired invoices and returns a download
URL. The response is not the file itself. Pending and Expired documents show
Order ID as --. A Paid document shows the successful payin order id.
Fetch download_url with a normal GET. The body starts with %PDF.
file_name is <invoice_id>.pdf.
Missing invoice_id returns 90000. An unknown invoice returns 90201.
A merchant profile without a name or login email returns 90102.
Query fields
| Field | Type | Required | Description |
|---|---|---|---|
invoice_id | string | Yes | Invoice id. |
Success response
{
"error_code": 0,
"msg": "success",
"data": {
"download_url": "https://...",
"file_name": "INV-....pdf"
},
"trace": "..."
}