Payment Service Request Signing
If you choose not to use an API Key, you can authenticate your requests using EIP-191 signatures.
Required Headers (Signature Mode)
x-depay-timestampx-depay-signature
Timestamp Rules
- Timestamp must be within ±5 minutes of server time.
- Timestamp cannot be reused for the same owner address.
Replay key format:
- (
ownerAddress,timestamp)
Signed Message
The signature input is always:
DePay API Request Signature
<CANONICAL_JSON>
Canonical JSON shape:
{
"action": "invoice.create",
"ownerAddress": "0x...",
"timestamp": 1714200000000,
"payload": {
"chainId": 8453,
"invoiceId": "1000456",
"amount": "12.5",
"webhookUrl": "https://merchant.example.com/webhooks/invoice"
}
}
:::warning Canonical JSON must be byte-identical
The signed string is serialized without extra whitespace and with the key order
shown above (action, ownerAddress, timestamp, payload). JSON.stringify
in JavaScript already produces this form; in Python use
json.dumps(obj, separators=(",", ":")). Omit optional payload fields entirely
instead of sending null — an omitted key and a null key produce different
signatures.
:::
Action Values
invoice.createinvoice.statusinvoice.deleteinvoice.listfee.set-metafee.set-payout-meta
Signing Helper
- TypeScript
- Python
- cURL
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(
process.env.OWNER_PRIVATE_KEY as `0x${string}`,
);
export async function sign(
action: string,
payload: Record<string, unknown>,
): Promise<{ signature: string; timestamp: number }> {
const timestamp = Date.now();
const canonical = JSON.stringify({
action,
ownerAddress: account.address,
timestamp,
payload,
});
const signature = await account.signMessage({
message: `DePay API Request Signature\n${canonical}`,
});
return { signature, timestamp };
}
With ethers v6 instead of viem:
import { Wallet } from "ethers";
const wallet = new Wallet(process.env.OWNER_PRIVATE_KEY as string);
export async function sign(
action: string,
payload: Record<string, unknown>,
): Promise<{ signature: string; timestamp: number }> {
const timestamp = Date.now();
const canonical = JSON.stringify({
action,
ownerAddress: wallet.address,
timestamp,
payload,
});
const signature = await wallet.signMessage(
`DePay API Request Signature\n${canonical}`,
);
return { signature, timestamp };
}
import json
import os
import time
from typing import Any
from eth_account import Account
from eth_account.messages import encode_defunct
account = Account.from_key(os.environ["OWNER_PRIVATE_KEY"])
def sign(action: str, payload: dict[str, Any]) -> tuple[str, int]:
"""Return (signature, timestamp_ms) for a Payment Service request."""
timestamp = int(time.time() * 1000)
canonical = json.dumps(
{
"action": action,
"ownerAddress": account.address,
"timestamp": timestamp,
"payload": payload,
},
separators=(",", ":"),
)
message = encode_defunct(text=f"DePay API Request Signature\n{canonical}")
signed = account.sign_message(message)
return signed.signature.hex(), timestamp
Install with pip install eth-account requests.
curl -X POST '<BASE_URL>/integration/invoices' \
-H 'Content-Type: application/json' \
-H 'x-depay-timestamp: <TIMESTAMP_MS>' \
-H 'x-depay-signature: <REQUEST_SIGNATURE>' \
-d '{
"ownerAddress": "<OWNER_ADDRESS>",
"chainId": 8453,
"amount": "12.5"
}'
Generating a signature from a shell requires a signing library, so cast from
Foundry is the shortest path:
TIMESTAMP=$(($(date +%s) * 1000))
OWNER=$(cast wallet address --private-key "$OWNER_PRIVATE_KEY")
PAYLOAD='{"chainId":8453,"invoiceId":"1000456","amount":"12.5"}'
CANONICAL="{\"action\":\"invoice.create\",\"ownerAddress\":\"$OWNER\",\"timestamp\":$TIMESTAMP,\"payload\":$PAYLOAD}"
SIGNATURE=$(cast wallet sign --private-key "$OWNER_PRIVATE_KEY" \
"DePay API Request Signature
$CANONICAL")
curl -X POST "$BASE_URL/integration/invoices" \
-H 'Content-Type: application/json' \
-H "x-depay-timestamp: $TIMESTAMP" \
-H "x-depay-signature: $SIGNATURE" \
-d "{\"ownerAddress\":\"$OWNER\",$(echo "$PAYLOAD" | sed 's/^{//')"
Integration Notes
- Regenerate the signature whenever the timestamp changes — signature and timestamp always travel as a pair.
- The signed
payloadmust match what you actually send. ForPOSTrequests that is the body minusownerAddress; forGET/DELETEit is the query params minusownerAddress. - Keep the action value aligned with the endpoint intent (see Action Values).