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
}
| 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 0x0000000000000000000000000000000000000000. |
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. |
owner | string | no | The recover-from-sig flow derives owner from the session key. Provide it to assert and double-check the recovered identity. |
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.feemaker.io/?invoice=29f3b386-7c4a-4f6e-9d2b-1a8e3c5f7d09"
}
status lifecycle: init → partialPaid → paid on payment, or init → expired
once deadline lapses without sufficient deposits.
Examples
- JavaScript
- Python
- Bash
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.feemaker.io/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.feemaker.io/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.feemaker.io/invoice \
-H "x-session-nonce: $NONCE" \
-H "x-session-signature: $SIG" \
-H "x-encryption: none" \
-d "$BODY"
PoW challenge
When the server is configured with pow_difficulty > 0, a successful create
also requires a fresh challenge from POST /invoice/challenge
plus a satisfying nonce, both attached to the body. Default deployments leave
PoW disabled.
Proof-of-work challenge
POST /invoice/challenge
{ "address": "0xMerchantAddress…" }
Response:
{
"challenge": "<random hex>",
"difficulty": 18,
"expiresInSecs": 600,
"algorithm": "sha256(challenge || ':' || nonce) leading zero bits >= difficulty"
}
Find a nonce such that sha256(challenge || ':' || nonce) has at least
difficulty leading zero bits, then include challenge and nonce alongside
the rest of your POST /invoice body.