Files
nick-doc/03 - API Reference/Scanner API.md
Siavash Sameni dceaf82934 audit: 2026-05-30 full-codebase audit — report, issues, docs, runbooks
Full-codebase-audit 2026-05-30 outputs:
- Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md
- 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer).
- Scanner docs from scratch (was zero): architecture, data model, API ref, payment
  flow, operations runbook + repo README.
- Doc-sync updates across API reference, data models, flows, design system.
- Secret Rotation Runbook (08 - Operations) for the exposed credentials.
- Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js.

Issues remain status:open intentionally — the code fixes are uncommitted-then-committed
working-tree changes per repo and aren't "resolved" until merged/deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:48:04 +04:00

250 lines
6.5 KiB
Markdown

---
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 <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.
- `/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.