Developer Docs
Accept crypto programmatically: create an invoice, then poll for payment
events. All requests go to https://api.amanpay.app.
Authentication
Every request is authenticated with a session key — an ephemeral secp256k1 key whose signature identifies the owner. For each call you send two headers:
x-session-nonce— a millisecond timestamp (Date.now()).x-session-signature— an EIP-191 personal-sign oversess:{nonce}:{sha256(body)}. For empty bodies, hash the empty string.
No accounts, no API tokens — the signature alone proves ownership. Set
x-encryption: none for plaintext JSON bodies.
Create an invoice
Creates a new invoice and returns the deposit address the buyer should pay to, along with a
hosted paymentUrl you can redirect them to.
Request
POST /invoice
x-session-nonce: <millisecond timestamp>
x-session-signature: <EIP-191 sig over sess:{nonce}:{sha256(body)}>{
"value": "100",
"chainId": 1,
"description": "Hosting plan XLarge",
"acceptedTokens": [
"1:0xdac17f958d2ee523a2206206994597c13d831ec7"
],
"deadlineSecs": 86400
}| Field | Type | Required | Notes |
|---|---|---|---|
value | string | yes | Amount in vsCurrency (USD by default). Use "0" for tip-jar mode — any deposit matching acceptedTokens marks the invoice paid. |
chainId | number | yes | EVM/TVM chain id of the deposit network. |
description | string | yes | Free-form merchant copy displayed on checkout. |
acceptedTokens | string[] | yes | Whitelist of "{chainId}:{tokenAddress}" entries (≥ 1). Native coin uses the zero address. |
deadlineSecs | number | no | Validity window from creation. Defaults to 90 hours. |
data | object | no | Free-form merchant metadata (line items, references). Defaults to {}. |
address | string | no | Pre-derived deposit address. When omitted, the server allocates a fresh smart-wallet index for the owner. |
token | string | no | Preferred display token. Defaults to acceptedTokens[0]. |
Response
201 Created
{
"guid": "29f3b386-7c4a-4f6e-9d2b-1a8e3c5f7d09",
"createdAt": 1730393820,
"deadline": 1730480220,
"status": "init",
"value": "100",
"token": "1:0xdac17f958d2ee523a2206206994597c13d831ec7",
"chainId": 1,
"data": {},
"description": "Hosting plan XLarge",
"address": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
"owner": "0xMerchantAddress…",
"paidAmount": "0",
"payments": [],
"acceptedTokens": ["1:0xdac17f958d2ee523a2206206994597c13d831ec7"],
"paymentUrl": "https://app.amanpay.app/?invoice=29f3b386-7c4a-4f6e-9d2b-1a8e3c5f7d09"
}status lifecycle: init → partialPaid → paid on payment, or
init → expired once deadline lapses without sufficient deposits.
Examples
import { Wallet, sha256, toUtf8Bytes } from 'ethers';
const SESSION_PRIVATE_KEY = '0x...';
const body = JSON.stringify({
value: '100',
chainId: 1,
description: 'Hosting plan XLarge',
acceptedTokens: ['1:0xdac17f958d2ee523a2206206994597c13d831ec7'],
deadlineSecs: 86400,
});
const nonce = Date.now();
const hash = sha256(toUtf8Bytes(body)).slice(2);
const sig = await new Wallet(SESSION_PRIVATE_KEY)
.signMessage(`sess:${nonce}:${hash}`);
const r = await fetch('https://api.amanpay.app/invoice', {
method: 'POST',
headers: {
'x-session-nonce': String(nonce),
'x-session-signature': sig,
'x-encryption': 'none',
},
body,
});
const { guid, paymentUrl } = await r.json();from eth_account import Account
from eth_account.messages import encode_defunct
from hashlib import sha256
import json, time, requests
SESSION_PRIVATE_KEY = '0x...'
body = json.dumps({
'value': '100',
'chainId': 1,
'description': 'Hosting plan XLarge',
'acceptedTokens': ['1:0xdac17f958d2ee523a2206206994597c13d831ec7'],
'deadlineSecs': 86400,
})
nonce = int(time.time() * 1000)
body_hash = sha256(body.encode()).hexdigest()
sig = Account.sign_message(
encode_defunct(text=f'sess:{nonce}:{body_hash}'),
private_key=SESSION_PRIVATE_KEY,
).signature.hex()
resp = requests.post('https://api.amanpay.app/invoice', data=body, headers={
'x-session-nonce': str(nonce),
'x-session-signature': sig,
'x-encryption': 'none',
})
print(resp.json()['paymentUrl'])SESSION_PRIVATE_KEY=0x...
BODY='{"value":"100","chainId":1,"description":"Hosting plan XLarge","acceptedTokens":["1:0xdac17f958d2ee523a2206206994597c13d831ec7"],"deadlineSecs":86400}'
NONCE=$(($(date +%s%N) / 1000000))
HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
SIG=$(cast wallet sign --private-key $SESSION_PRIVATE_KEY "sess:$NONCE:$HASH")
curl -X POST https://api.amanpay.app/invoice \
-H "x-session-nonce: $NONCE" \
-H "x-session-signature: $SIG" \
-H "x-encryption: none" \
-d "$BODY"Poll for events
Drains pending invoice-state-change events for the authenticated owner. Each call returns events queued since the last successful drain — once consumed, events are removed from the queue. Suitable for serverless cron jobs and any client that can't hold a long-lived WebSocket open.
POST /poll/events
x-session-nonce: <millisecond timestamp>
x-session-signature: <EIP-191 sig over sess:{nonce}:{sha256("")}>The body is empty. The session signature alone identifies the owner — no
ownerAddress field is required.
Response
200 OK
{
"ownerAddress": "0xMerchantAddress…",
"events": [
{
"guid": "29f3b386-7c4a-4f6e-9d2b-1a8e3c5f7d09",
"createdAt": 1730393820,
"deadline": 1730480220,
"status": "paid",
"value": "100",
"token": "1:0xdac17f958d2ee523a2206206994597c13d831ec7",
"chainId": 1,
"data": { "description": "Hosting plan XLarge", "sku": "XL-2024" },
"description": "Hosting plan XLarge",
"address": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
"owner": "0xMerchantAddress…",
"paidAmount": "100",
"payments": [
{
"txHash": "0x4d5e6f…2d3e4f",
"logIndex": 0,
"chainId": "1",
"token": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"from": "0xfeedbeef…0000beef",
"amount": "100",
"vsValue": 100.0,
"timestamp": 1730393900
}
],
"acceptedTokens": ["1:0xdac17f958d2ee523a2206206994597c13d831ec7"]
}
]
}Each event is the full Invoice record at the time of the change. There's no
separate "delta" payload: compare status against your locally stored copy to
decide what changed.
Examples
import { Wallet, sha256 } from 'ethers';
const SESSION_PRIVATE_KEY = '0x...';
const nonce = Date.now();
const hash = sha256('0x').slice(2); // sha256 of empty body
const sig = await new Wallet(SESSION_PRIVATE_KEY)
.signMessage(`sess:${nonce}:${hash}`);
const r = await fetch('https://api.amanpay.app/poll/events', {
method: 'POST',
headers: {
'x-session-nonce': String(nonce),
'x-session-signature': sig,
'x-encryption': 'none',
},
});
const { events } = await r.json();
for (const ev of events ?? []) console.log(ev.guid, ev.status);from eth_account import Account
from eth_account.messages import encode_defunct
from hashlib import sha256
import time, requests
SESSION_PRIVATE_KEY = '0x...'
nonce = int(time.time() * 1000)
body_hash = sha256(b'').hexdigest()
sig = Account.sign_message(
encode_defunct(text=f'sess:{nonce}:{body_hash}'),
private_key=SESSION_PRIVATE_KEY,
).signature.hex()
resp = requests.post('https://api.amanpay.app/poll/events', headers={
'x-session-nonce': str(nonce),
'x-session-signature': sig,
'x-encryption': 'none',
})
for ev in resp.json()['events']:
print(ev['guid'], ev['status'])SESSION_PRIVATE_KEY=0x...
NONCE=$(($(date +%s%N) / 1000000))
HASH=$(printf '' | openssl dgst -sha256 -hex | awk '{print $2}')
SIG=$(cast wallet sign --private-key $SESSION_PRIVATE_KEY "sess:$NONCE:$HASH")
curl -X POST https://api.amanpay.app/poll/events \
-H "x-session-nonce: $NONCE" \
-H "x-session-signature: $SIG" \
-H "x-encryption: none"Polling cadence
The server holds events in-memory per session key — there's no long-poll window. Drive the loop yourself; once every 2–5 seconds is the recommended range for active payment flows. Idle merchants can poll less often without losing events: each event is stored until consumed, capped by the operator's configured retention.
POST /invoice call creates a real invoice on your registered
merchant address. While testing, prefix the description with something like
[sandbox] so you can recognize and ignore them in your dashboard.