--- title: Scanner API tags: [api, scanner, payment] created: 2026-05-30 --- # Scanner API HTTP API exposed by the AMN Pay Scanner microservice. Default port: `8080`. All endpoints except `/health` require `Authorization: Bearer ` 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 ``` - Uses constant-time comparison to prevent timing attacks. - Returns `401 {"error":"unauthorized"}` on failure. - `/health` is 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 | Override chain default confirmation count (0 = use chain default) | **Example request:** ```json { "intentId": "a1b2c3d4-...", "chainId": 56, "tokenAddress": "0x55d398326f99059ff775485246999027b3197955", "destination": "0xAbCd1234...", "amount": "10000000000000000000", "callbackUrl": "https://api.amn.gg/api/payment/scanner-callback", "callbackSecret": "abc123...", "confirmations": 12 } ``` **Response `200 OK`:** ```json { "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" } } ``` **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`:** ```json { "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`:** ```json { "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`:** ```json { "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:** ```json { "intentId": "a1b2c3d4-...", "paymentReference": "0x1a2b3c4d5e6f7a8b", "txHash": "0xdeadbeef...", "blockNumber": 39000010, "amount": "10000000000000000000", "token": "0x55d398326f99059ff775485246999027b3197955", "chainId": 56, "status": "confirmed" } ``` **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: ```js const expected = createHmac('sha256', callbackSecret).update(rawBody).digest('hex'); if (!timingSafeEqual(Buffer.from(received), Buffer.from(expected))) reject(); ``` --- ## Data models ### Intent object ```json { "intentId": "string", "chainId": 56, "chainType": "evm", "tokenAddress": "0x...", "destination": "0x...", "amount": "10000000000000000000", "paymentReference": "0x1a2b3c4d", "topicRef": "0xdeadbeef...", "status": "pending | confirming | confirmed | expired | webhook_failed", "confirmationsRequired": 12, "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.