Skip to main content

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
note

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-key request 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:

HeaderDescription
x-api-keyYour APIKey.
x-api-timestampCall time as a Unix millisecond timestamp, generated by the client. Requests more than ±10 minutes from server time are rejected.
x-api-signatureThe request signature, computed as described below. Requests with an invalid signature are rejected.
Content-TypeAlways application/json.

How the signature is computed

Building the signing content

The signed content is a JSON object containing the following fields:

KeyValue
apiPathThe request path without any query string, for example /pro/merchant/payin/v1/payment-links.
bodyThe raw JSON string you send (keys inside the body do not need to be sorted).
x-api-keyYour APIKey.
x-api-timestampThe 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

  1. Serialize the content object above into a JSON string.
  2. Compute its HMAC-SHA256, keyed with your APISecret.
  3. Base64-encode the result to produce the x-api-signature value.
caution

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

StatusDescription
200success
400Bad Request
403Forbidden (not allowlisted, or invalid signature)
429Too many requests (rate limited)
note

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.