7.0 KiB
title, tags, created
| title | tags | created | |||
|---|---|---|---|---|---|
| Scanner API |
|
2026-05-30 |
Scanner API
HTTP API exposed by the AMN Pay Scanner microservice. Default port: 8080.
All endpoints except /health require Authorization: Bearer <SCANNER_API_KEY> when the key is configured in the environment (production). In dev mode (key not set) all requests are allowed.
Base URL (dev): http://localhost:8080
Authentication
Authorization: Bearer <SCANNER_API_KEY>
- Uses constant-time comparison to prevent timing attacks.
- Returns
401 {"error":"unauthorized"}on failure. /healthis explicitly excluded from auth — always open.
POST /intents
Register a new payment intent. The scanner will watch the specified chain for a matching transfer and call back to callbackUrl when confirmed.
Request body (application/json):
| Field | Type | Required | Notes |
|---|---|---|---|
intentId |
string | yes | Caller-supplied unique ID (UUID recommended) |
chainId |
integer | yes | Numeric chain ID (e.g. 56, 137, 728126428) |
tokenAddress |
string | yes | Token contract address. EVM/Tron: lowercase 0x hex. TON: exact base64url or raw format |
destination |
string | yes | Receiving wallet address. EVM/Tron: 0x hex. TON: base64url |
amount |
string | yes | Amount in smallest unit (wei / token decimals) as a base-10 integer string |
callbackUrl |
string | yes | URL the scanner POSTs to on confirmation |
callbackSecret |
string | yes | HMAC key for X-AMN-Signature verification |
confirmations |
integer | no | Requested confirmation count. The scanner raises it to the chain acceptance floor if lower. |
Example request:
{
"intentId": "a1b2c3d4-...",
"chainId": 56,
"tokenAddress": "0x55d398326f99059ff775485246999027b3197955",
"destination": "0xAbCd1234...",
"amount": "10000000000000000000",
"callbackUrl": "https://api.amn.gg/api/payment/amn-scanner/webhook",
"callbackSecret": "abc123...",
"confirmations": 200
}
Response 200 OK:
{
"intentId": "a1b2c3d4-...",
"paymentReference": "0x1a2b3c4d5e6f7a8b",
"checkoutBlock": {
"destination": "0xabcd1234...",
"tokenAddress": "0x55d398326f99059ff775485246999027b3197955",
"tokenSymbol": "USDT",
"decimals": 18,
"chainId": 56,
"proxyAddress": "0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9",
"paymentReference": "0x1a2b3c4d5e6f7a8b",
"feeAmount": "0",
"feeAddress": "0x000000000000000000000000000000000000dEaD",
"amountWei": "10000000000000000000"
}
}
Confirmation floor: Built-in accepted thresholds are currently BSC 200, Ethereum 50, Polygon 300, Arbitrum 2400, Base 300, Tron 200, and TON 120. Callers may raise a requirement but cannot lower an intent below the chain floor.
Idempotency: If intentId already exists the existing intent's checkout block is returned (no error).
Error cases:
| Status | Body | Cause |
|---|---|---|
| 400 | {"error":"intentId is required"} |
Missing field |
| 400 | {"error":"amount must be a positive integer string (base-10 wei)"} |
Non-numeric or zero amount |
| 400 | {"error":"unsupported chainId: 999"} |
Chain not in supported-chains.json |
| 500 | {"error":"internal error"} |
DB write failure |
GET /intents/{intentId}
Fetch the current state of a payment intent.
Response 200 OK: Full Intent object (see Data Models below).
callbackSecret is excluded from the response regardless of auth state.
Error cases:
| Status | Body | Cause |
|---|---|---|
| 404 | {"error":"intent not found"} |
Unknown intentId |
GET /scanner/status
Returns scan progress for all verified chains.
Response 200 OK:
{
"chains": [
{
"chainId": 56,
"name": "BSC",
"chainType": "evm",
"lastScannedBlock": 39000000,
"chainHead": 39000015,
"lag": 15,
"pendingIntents": 3
},
{
"chainId": 728126428,
"name": "TRX",
"chainType": "tron",
"lastScannedBlock": 1748500000000,
"chainHead": 1748500015000,
"lag": 15000,
"pendingIntents": 1
},
{
"chainId": 1100,
"name": "TON",
"chainType": "ton",
"lastScannedBlock": 1748500000,
"chainHead": 1748500015,
"lag": 15,
"pendingIntents": 0
}
]
}
Note on lag units: For EVM and Tron chains, lag is in blocks (or ms-timestamp difference). For TON, lag is in seconds (Unix timestamps).
POST /admin/webhooks/retry
Immediately trigger a re-delivery attempt for all webhook_failed intents. Normally the scanner retries automatically every WEBHOOK_RETRY_HOURS; this endpoint forces an immediate pass.
Response 200 OK:
{ "queued": 2 }
Each retry is dispatched in a separate goroutine. Success resets the intent status to confirmed and records webhook_delivered_at.
GET /health
Health check. No authentication required.
Response 200 OK:
{ "status": "ok", "time": "2026-05-30T12:00:00Z" }
Used by Docker HEALTHCHECK and upstream load balancers / Gatus monitoring.
Webhook delivery (outbound)
When an intent is confirmed the scanner POSTs to callbackUrl:
Headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-AMN-Signature |
hex(HMAC-SHA256(body, callbackSecret)) |
X-AMN-Delivery-ID |
intentId |
X-AMN-Retry |
true (only on manual retry via /admin/webhooks/retry) |
Body:
{
"intentId": "a1b2c3d4-...",
"paymentReference": "0x1a2b3c4d5e6f7a8b",
"txHash": "0xdeadbeef...",
"blockNumber": 39000010,
"confirmations": 200,
"amount": "10000000000000000000",
"token": "0x55d398326f99059ff775485246999027b3197955",
"chainId": 56,
"status": "confirmed"
}
confirmations is the accepted confirmation count. Once the intent is confirmed, the scanner caps this value at confirmationsRequired; it does not keep reporting a live, ever-growing block count.
Retry schedule (on non-2xx or network error): 5 s → 30 s → 2 min → 10 min → 1 h → webhook_failed.
The backend should verify X-AMN-Signature to reject forged callbacks:
const expected = createHmac('sha256', callbackSecret).update(rawBody).digest('hex');
if (!timingSafeEqual(Buffer.from(received), Buffer.from(expected))) reject();
Data models
Intent object
{
"intentId": "string",
"chainId": 56,
"chainType": "evm",
"tokenAddress": "0x...",
"destination": "0x...",
"amount": "10000000000000000000",
"paymentReference": "0x1a2b3c4d",
"topicRef": "0xdeadbeef...",
"status": "pending | confirming | confirmed | expired | webhook_failed",
"confirmationsRequired": 200,
"txHash": null,
"logIndex": null,
"blockNumber": null,
"confirmations": 0,
"salt": "hex64chars",
"webhookDeliveredAt": null,
"createdAt": "2026-05-30T10:00:00Z",
"updatedAt": "2026-05-30T10:00:00Z"
}
Note: callbackUrl and callbackSecret are present in the DB but callbackSecret is always omitted from API responses.