Aicscan Pay

Aicscan Pay collects USDT payments without ever holding the funds: the payer sends the amount to a deposit address that belongs to the invoice, and the funds then go straight to your payout address. No API endpoint moves funds.

The Pay API follows the same rules as the rest of API v1 (authentication, pagination, errors, versioning), with the differences below.

Enable Pay and get keys#

  1. In the client area, Pay tab, the owner enables Pay. Two-factor authentication is required for every Pay action.
  2. Test mode is available right away: create a test key.
  3. For live mode, start the business verification (KYB). live keys are issued only once the KYB is approved.
  4. Declare your signer wallet and your payout address: for each open chain, the tab returns the EIP-712 RegisterMerchant message to sign with that wallet. Nothing is sent on-chain at this stage.
Key Prefix Environment
Test aic_test_… testnets, no KYB
Live aic_live_… mainnets, after KYB

Scopes: pay:read (reads), pay:write (invoices, webhooks), pay:refund (refund preparation). Every Pay scope includes pay:read. An Alerts key gets 403 insufficient_scope on Pay endpoints, and a Pay key on Alerts endpoints. A Pay key does not depend on the workspace's Alerts plan (no plan_required, no daily quota); the per-second rate limit applies.

Every object belongs to the environment of the key that created it: a test invoice is not found (404) with a live key.

Idempotency-Key is required on every Pay POST (400 idempotency_key_required). Use one UUID per operation and send the same value when you retry: the invoice is never created twice. See Idempotency.

Create an invoice#

bash
curl https://aicscan.io/v1/invoices \
  -H "Authorization: Bearer $AICSCAN_PAY_KEY" \
  -H "Idempotency-Key: 6f1c2d0e-8b7a-4c55-9e2f-1a3b5c7d9e0f" \
  -H "Content-Type: application/json" \
  -d '{"currency":"EUR","price_e6":"25000000","chain_ids":[56,728126428],"external_ref":"cmd-1042","ttl_seconds":3600}'
Field Meaning
currency USDT, or an enabled display currency (USD, EUR, XOF… see GET /v1/rates).
price_e6 Price in millionths of the currency, as a string: "25000000" = 25.00. No more decimals than the currency has (XOF: none).
chain_ids Chains offered to the payer: one deposit address per chain.
variant A (default): one address per invoice. B: shared address and slightly unique amount, one chain only.
ttl_seconds Validity, 3,600 s by default, from 900 s to 7 days. Use a long one if your customers pay from an exchange.
external_ref, description, metadata Your reference (unique per environment), a label, a free JSON object.

A currency rate is frozen at creation (price.fx_rate_e6, fx_source, fx_as_of) and the amount due is rounded in the payer's favour. Token amounts are always strings in native units, with their readable form:

JSON
{ "amount": "27012345", "amount_formatted": "27.012345", "decimals": 6 }

The response (201) gives, for each chain, the deposit address (deposit_address, base58 T… on TRON), the amount due and, on EVM chains, an EIP-681 link (payment_uri) to put in a QR code.

Common refusals: chain_not_available, amount_below_minimum, amount_above_cap, duplicate_external_ref (409), unique_amount_exhausted (409, variant B: every unique value is taken), currency_suspended (409: the USDT/USD guard suspended currency-priced invoices; USDT-priced invoices are not affected), kyb_required (403, live).

Invoice statuses#

Status Meaning
waiting Created, nothing received.
confirming Payment seen in a block, confirmations in progress.
paid Amount received within tolerance, confirmed.
underpaid Less than due received: a top-up is still possible until expiry.
overpaid More than due received: refund the excess if needed.
expired Expired without a complete payment.
paid_late Paid after expiry (or after cancellation): honour it or refund it.
canceled Canceled by you before any payment (POST /v1/invoices/{id}/cancel).
held Payment from a sanctioned address: manual decision.

Several transfers to the same address add up. Tiny amounts (dust) are ignored. An invoice's detail (GET /v1/invoices/{id}) lists its payments, its sweeps to your payout address and its refunds.

Pay webhooks#

Declare an endpoint with POST /v1/webhook_endpoints (https:// only; private and local addresses are refused). The whsec_… secret is returned once. Deliveries follow exactly the mechanism of Alerts webhooks: JSON POST, X-Event-Id, X-Timestamp and X-Signature: sha256=HMAC(secret, "{X-Timestamp}.{raw body}") headers, rejection beyond 300 seconds, only a 2xx response counts as success, retries for 72 hours.

JSON
{
  "id": "evt_7850b21f0ab248a5e16ddedf2c6626e9",
  "type": "invoice.paid",
  "api_version": "1",
  "environment": "live",
  "livemode": true,
  "created_at": "2026-11-20T10:05:00Z",
  "data": {
    "object": { "id": "0193a1d0-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "object": "invoice", "status": "paid" },
    "payment": { "amount": "27012345", "amount_formatted": "27.012345", "decimals": 6, "chain_id": 56,
                 "tx_hash": "0x…", "log_index": 3, "block_number": 45123456, "confirmations": 15 }
  }
}

Types: invoice.created, invoice.confirming, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.paid_late, invoice.canceled, invoice.reverted, invoice.held, sweep.succeeded, sweep.failed, refund.succeeded, refund.pending, merchant.kyb_updated… The id (X-Event-Id) is identical on every retry: use it to ignore a duplicate. After a chain reorganization, invoice.reverted cancels a payment; a new invoice.paid follows if it is included again.

  • POST /v1/webhook_endpoints/{id}/test sends a webhook_endpoint.test event to that endpoint only.
  • GET /v1/events and GET /v1/events/{id} give the log and the delivery status; POST /v1/events/{id}/resend queues an event again.

Refund#

A refund is a transfer from your own wallet. POST /v1/refunds (scope pay:refund) returns the transaction to sign (transaction.to = token contract, transaction.data = transfer(to, amount)); Aicscan never sends it.

The sender address of a payment is never offered by default: it is often an exchange wallet, and a refund sent there would be lost. Put in to_address the address the payer gave you. The exchange_address warning flags a known exchange address, origin_address an address that sent a payment of the invoice.

The platform fee is not refunded. The amount is capped at the amount received minus refunds already prepared.

Test mode#

test keys work on testnets, with the same API and the same webhooks ("environment": "test", "livemode": false). Nothing you create in test appears in live, and the other way round. Check that an address is one of your deposit addresses with GET /v1/deposit_addresses/{address}/verify.

Full reference: Pay: invoices, refunds, webhooks.