Authentication
Authentication
Every request to the Morph Payments API must be signed with your API Key using HMAC-SHA256. This lets Morph confirm the request genuinely came from you and that its body was not altered in transit. Requests with a missing or invalid signature are rejected.
Base URL
All requests are sent to the production gateway:
https://pay-openapi.morph.network
Append the endpoint path to the base URL, for example:
https://pay-openapi.morph.network/pro/merchant/payin/v1/payment-links
https://pay-openapi.morph.network/pro/merchant/payin/v1/invoices
After the gateway authenticates your API Key, it resolves your merchant identity
and injects Merchant-Id downstream. You do not need to — and should not —
send Merchant-Id or merchant_id yourself.
Getting your API credentials
Apply for credentials at https://account.morph.network/. You receive a pair of values:
- APIKey — sent in plaintext with the
x-api-keyrequest header. - APISecret — used only to compute the signature locally. Never put it in a request or commit it to a code repository.
Request headers
Every request must include the following headers:
| Header | Description |
|---|---|
x-api-key | Your APIKey. |
x-api-timestamp | Call time as a Unix millisecond timestamp, generated by the client. Requests more than ±10 minutes from server time are rejected. |
x-api-signature | The request signature, computed as described below. Requests with an invalid signature are rejected. |
Content-Type | Always application/json. |
How the signature is computed
Building the signing content
The signed content is a JSON object containing the following fields:
| Key | Value |
|---|---|
apiPath | The request path without any query string, for example /pro/merchant/payin/v1/payment-links. |
body | The raw JSON string you send (keys inside the body do not need to be sorted). |
x-api-key | Your APIKey. |
x-api-timestamp | The millisecond timestamp. |
<query params> | Each query parameter as its own key (these endpoints are POST + JSON and usually have none). |
The keys of the content object must be serialized in ascending alphabetical
order. Most languages satisfy this with an ordered map plus a sort; Go's
json.Marshal(map[string]string) outputs keys in ascending order by default.
Generating the signature
- Serialize the content object above into a JSON string.
- Compute its HMAC-SHA256, keyed with your APISecret.
- Base64-encode the result to produce the
x-api-signaturevalue.
The body used for signing must be byte-for-byte identical to the body you
actually send: decide the body text first, then sign it, then send that same
text. The x-api-timestamp used for signing must also match the header value
exactly, and apiPath must exclude any query string.
Code examples
Go
func signature(path, apiKey, apiSecret, timestamp string, query map[string]string, body string) string {
contentMap := make(map[string]string)
contentMap["x-api-key"] = apiKey
contentMap["x-api-timestamp"] = timestamp
contentMap["apiPath"] = path
for key, value := range query {
contentMap[key] = value
}
contentMap["body"] = body
content, _ := json.Marshal(contentMap) // Go outputs map keys in ascending order
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write(content)
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
Node.js
function getSignature(apiPath, body, apiKey, apiSecret, timeStamp, queryParams = {}) {
const content = {
apiPath,
body,
"x-api-key": apiKey,
"x-api-timestamp": timeStamp,
};
for (const [k, v] of Object.entries(queryParams || {})) content[k] = String(v);
const sortedKeys = Object.keys(content).sort();
const sorted = Object.fromEntries(sortedKeys.map((k) => [k, content[k]]));
const payload = JSON.stringify(sorted);
return crypto.createHmac("sha256", apiSecret).update(payload).digest("base64");
}
curl
curl -X POST 'https://pay-openapi.morph.network/pro/merchant/payin/v1/payment-links' \
-H 'Content-Type: application/json' \
-H 'x-api-key: <APIKey>' \
-H 'x-api-timestamp: 1782777600000' \
-H 'x-api-signature: <base64 signature>' \
-d '{"amount":"100.00","receiving_network":"morph","receiving_token_symbol":"USDT","receiving_token_address":"0xe7cd86e13ac4309349f30b3435a9d337750fc82d","receiving_address":"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}'
HTTP status codes
| Status | Description |
|---|---|
| 200 | success |
| 400 | Bad Request |
| 403 | Forbidden (not allowlisted, or invalid signature) |
| 429 | Too many requests (rate limited) |
Business success or failure is determined by the error_code field in the
response body (error_code == 0 means success), not by the HTTP status code
alone. See the Error Codes reference for details.