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:

No accounts, no API tokens — the signature alone proves ownership. Set x-encryption: none for plaintext JSON bodies.

Create an invoice

POST/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
}
FieldTypeRequiredNotes
valuestringyesAmount in vsCurrency (USD by default). Use "0" for tip-jar mode — any deposit matching acceptedTokens marks the invoice paid.
chainIdnumberyesEVM/TVM chain id of the deposit network.
descriptionstringyesFree-form merchant copy displayed on checkout.
acceptedTokensstring[]yesWhitelist of "{chainId}:{tokenAddress}" entries (≥ 1). Native coin uses the zero address.
deadlineSecsnumbernoValidity window from creation. Defaults to 90 hours.
dataobjectnoFree-form merchant metadata (line items, references). Defaults to {}.
addressstringnoPre-derived deposit address. When omitted, the server allocates a fresh smart-wallet index for the owner.
tokenstringnoPreferred 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

POST/poll/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.

Each 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.