# Confirmations: agent integration guide

Confirmations lets merchants register their own cold-wallet addresses and create
exact-amount payment intents. It watches on-chain transfers to those addresses and
POSTs a webhook after confirmation. Funds go directly to the recipient;
Confirmations never custodies funds or needs private keys. Public donation
challenges and donation signup send funds directly to the EFF.

Intended production base URL: `https://api.confirmations.info`. For self-hosting,
use your own host. Examples use `$BASE`; set `$KEY` to your returned API key.
Amounts, IDs and timestamps below are illustrative; JSON field names and types
match the API. Always pay the exact amount returned by your own request.

```sh
BASE=https://api.confirmations.info
KEY=YOUR_API_KEY
```

## Authentication and discovery

Send `X-API-Key: $KEY` on endpoints tagged `addresses`, `payments`, or `account`.
Endpoints tagged `public`, `challenges`, and `agents` need no authentication.
Every response carries a generated UUID in `X-Request-Id`.

```sh
curl -s "$BASE/plans"
curl -s "$BASE/signup/options"
```

`GET /plans` returns an array of `PlanOut` objects:
```json
[{"name":"starter","monthly_event_limit":100,"monthly_price_usd":0},{"name":"growth","monthly_event_limit":10000,"monthly_price_usd":49},{"name":"scale","monthly_event_limit":100000,"monthly_price_usd":199}]
```

Default tiers: starter is free with 100 events/month and a $5 minimum donation;
growth is $49/month with 10,000 events/month and a $49 minimum donation;
scale is $199/month with 100,000 events/month and a $199 minimum donation.
The starter minimum is configurable. `GET /signup/options` returns
`SignupOptionsOut` (nested `SignupOptionsChain` and `SignupOptionsPlan`):
```json
{"min_donation_usd":5,"chains":[{"chain":"bitcoin","address":"3LTu6uavQ4A3kgDauZipyGqcHQEUSVe2so","symbol":"BTC","auto_verify":true},{"chain":"ethereum","address":"0x1ca9EB2a5C213d417269134b80111F57e1644105","symbol":"ETH","auto_verify":true},{"chain":"binance","address":"0x375d784041BD851D68Ddd7719646e52C80c6F496","symbol":"BNB","auto_verify":true}],"plans":[{"name":"starter","monthly_event_limit":100,"monthly_price_usd":0,"min_donation_usd":5},{"name":"growth","monthly_event_limit":10000,"monthly_price_usd":49,"min_donation_usd":49},{"name":"scale","monthly_event_limit":100000,"monthly_price_usd":199,"min_donation_usd":199}]}
```

Merchant address/payment chains: `bitcoin`, `ethereum`, `base`, `arbitrum`,
`optimism`, `polygon`, `bnb`, `avalanche`, `linea`, `zksync`.
Donation signup and challenges accept only `bitcoin`, `ethereum`, `binance`;
`binance` is the donation API name for the chain named `bnb` in merchant APIs.

## 1. Obtain an API key

`SignupIn`: `method` is required (`donation` or `subscribe`); `chain`,
`usd_amount`, and `email` default to null; `plan` defaults to `starter`.
When supplied, `usd_amount` must be positive. Donation requires chain and amount.
Subscribe requires `growth` or `scale` and immediately creates an active account;
the current endpoint does not collect billing details or process a charge.

Donation request (`SignupIn`):
```sh
curl -s -X POST "$BASE/signup" -H 'Content-Type: application/json' \
  -d '{"method":"donation","chain":"ethereum","usd_amount":"5.00","plan":"starter","email":"ops@example.com"}'
```
Response (`DonationIntentOut`):
```json
{"intent_id":"11111111-1111-4111-8111-111111111111","chain":"ethereum","address":"0x1ca9EB2a5C213d417269134b80111F57e1644105","amount_native":"0.002","amount_smallest":"2000000000000000","usd_amount":"5.00","expires_at":"2026-09-09T13:00:00+00:00"}
```
Send exactly the returned native amount to the returned address before expiry
(default `SIGNUP_INTENT_TTL=3600` seconds), then poll:
```sh
INTENT_ID=11111111-1111-4111-8111-111111111111
curl -s "$BASE/signup/$INTENT_ID"
```
Response (`IntentStatusOut`):
```json
{"intent_id":"11111111-1111-4111-8111-111111111111","status":"PENDING","chain":"ethereum","amount_native":"0.002","tx_hash":null,"expires_at":"2026-09-09T13:00:00+00:00"}
```
Signup intent statuses: `PENDING`, `CONFIRMED`, `CLAIMED`, `EXPIRED`.
Once `CONFIRMED`, claim once (no request body):
```sh
curl -s -X POST "$BASE/signup/$INTENT_ID/claim"
```
Response (`SignupOut`; API key shown here is illustrative):
```json
{"user_id":"22222222-2222-4222-8222-222222222222","api_key":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","plan":"starter"}
```
Store the key securely: the plaintext key cannot be recovered. Claiming before
confirmation yields 400; claiming again yields 409.

Alternatively, subscribe directly (`SignupIn`):
```sh
curl -s -X POST "$BASE/signup" -H 'Content-Type: application/json' \
  -d '{"method":"subscribe","chain":null,"usd_amount":null,"plan":"growth","email":"ops@example.com"}'
```
Response (`SignupOut`):
```json
{"user_id":"22222222-2222-4222-8222-222222222222","api_key":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","plan":"growth"}
```

## 2. Register your own receive addresses

Request (`AddAddressesIn`), then list:
```sh
curl -s -X POST "$BASE/addresses" -H "X-API-Key: $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"chain":"ethereum","addresses":["0x1111111111111111111111111111111111111111"]}'
curl -s "$BASE/addresses" -H "X-API-Key: $KEY"
```
Both return arrays of `AddressOut`:
```json
[{"chain":"ethereum","address":"0x1111111111111111111111111111111111111111"}]
```
Registration is idempotent by user, chain, address. Replace the example with your
own cold-wallet address. Optional cleanup after finishing payments: remove an
address, optionally filtered by chain (skip this command while following the flow):
```sh
curl -s -X DELETE "$BASE/addresses/0x1111111111111111111111111111111111111111?chain=ethereum" -H "X-API-Key: $KEY"
```
Response:
```json
{"deleted":"0x1111111111111111111111111111111111111111"}
```

## 3. Create and track a payment

Request (`CreatePaymentIn`; all three fields required):
```sh
curl -s -X POST "$BASE/payments" -H "X-API-Key: $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"usd_amount":"25.00","chain":"ethereum","callback_url":"https://shop.example/callback"}'
```
Response (`CreatePaymentOut`):
```json
{"payment_id":"33333333-3333-4333-8333-333333333333","chain":"ethereum","address":"0x1111111111111111111111111111111111111111","amount_wei":"10483000000000000","amount_native":"0.010483","usd_amount":"25.00","expires_at":"2026-09-09T13:00:00+00:00"}
```
The service allocates an address from your pool. Have the payer send the exact
returned amount, without rounding. Despite its name, `amount_wei` is the smallest
native unit: satoshis on Bitcoin, wei on EVM chains. Amounts are strings to avoid
floating-point precision loss. Payment TTL defaults to 3600 seconds. Creating a
payment consumes one event; an active subscription and registered address are required.

```sh
PAYMENT_ID=33333333-3333-4333-8333-333333333333
curl -s "$BASE/payments/$PAYMENT_ID" -H "X-API-Key: $KEY"
curl -s "$BASE/payments" -H "X-API-Key: $KEY"
```
Single response (`PaymentStatus`); list returns an array of these, newest first:
```json
{"payment_id":"33333333-3333-4333-8333-333333333333","status":"PENDING","chain":"ethereum","address":"0x1111111111111111111111111111111111111111","amount_native":"0.010483","usd_amount":"25.00","tx_hash":null,"confirmations":0}
```
Payment statuses: `PENDING`, `PAID`, `EXPIRED`. `tx_hash` becomes a string once
detected. The payment confirmation threshold defaults to `CONFIRMATIONS_REQUIRED=2`.

## 4. Receive callbacks

Payment callback is a JSON POST to your `callback_url`:
```json
{"paymentId":"33333333-3333-4333-8333-333333333333","userId":"22222222-2222-4222-8222-222222222222","chain":"ethereum","address":"0x1111111111111111111111111111111111111111","txHash":"0xabc123","blockNumber":23000000,"confirmations":2,"expectedAmountWei":"10483000000000000"}
```
Respond with 2xx. Delivery retries on other statuses or exceptions with exponential
backoff starting at 1 second, doubling, capped at 30 seconds. Defaults:
`CALLBACK_MAX_RETRIES=6` total attempts and `CALLBACK_TIMEOUT_SECONDS=10` per attempt.
After exhaustion delivery is logged and dropped (no durable retry queue); poll
status to reconcile. Handle repeated callbacks idempotently by payment/challenge ID.

## Account and subscription

```sh
curl -s "$BASE/account" -H "X-API-Key: $KEY"
curl -s -X POST "$BASE/account/subscribe" -H "X-API-Key: $KEY" \
  -H 'Content-Type: application/json' -d '{"plan":"growth"}'
```
`SubscribeIn` contains only required `plan` (`starter`, `growth`, `scale`).
Both endpoints return `AccountOut` (example after subscribing):
```json
{"user_id":"22222222-2222-4222-8222-222222222222","email":"ops@example.com","plan":"growth","status":"active","events_used":1,"monthly_event_limit":10000,"period_end":"2026-10-09T12:00:00+00:00"}
```
`email` can be null. Without a subscription, `plan`, `status`, `events_used`,
`monthly_event_limit`, and `period_end` are all null. Billing periods use 30 days.

## Public donation challenges

No API key or account needed. Request (`ChallengeIn`):
```sh
curl -s -X POST "$BASE/challenges" -H 'Content-Type: application/json' \
  -d '{"chain":"ethereum","usd_amount":"1.00","callback_url":"https://shop.example/challenge-callback"}'
```
`chain` and positive `usd_amount` are required; `callback_url` defaults to null.
Default minimum is `CHALLENGE_MIN_USD=1`; TTL is `CHALLENGE_TTL=3600` seconds.
Response (`ChallengeOut`):
```json
{"challenge_id":"44444444-4444-4444-8444-444444444444","chain":"ethereum","address":"0x1ca9EB2a5C213d417269134b80111F57e1644105","amount_native":"0.0004","amount_smallest":"400000000000000","usd_amount":"1.00","expires_at":"2026-09-09T13:00:00+00:00"}
```
Pay exactly the returned amount to EFF, then poll or await the optional callback:
```sh
CHALLENGE_ID=44444444-4444-4444-8444-444444444444
curl -s "$BASE/challenges/$CHALLENGE_ID"
```
Response (`ChallengeStatusOut`):
```json
{"challenge_id":"44444444-4444-4444-8444-444444444444","status":"PENDING","chain":"ethereum","amount_native":"0.0004","tx_hash":null,"expires_at":"2026-09-09T13:00:00+00:00"}
```
Challenge statuses: `PENDING`, `CONFIRMED`, `EXPIRED`. Signup donations and challenges
are confirmed when the monitor matches a mined transfer; they do not use the
merchant payment confirmation threshold. Challenge callback payload:
```json
{"challengeId":"44444444-4444-4444-8444-444444444444","chain":"ethereum","address":"0x1ca9EB2a5C213d417269134b80111F57e1644105","txHash":"0xabc123","expectedAmount":"400000000000000","amountNative":"0.0004","usdAmount":"1.00"}
```

## Errors and references

HTTP exceptions and request-validation failures use this JSON envelope:
```json
{"error":{"code":"unauthorized","message":"Invalid API key","hint":null}}
```
`code` and `message` are strings; `hint` is a string or null. For validation errors,
`hint` contains the full validation error list serialized as a JSON string.
Default codes: 400 `bad_request`, 401 `unauthorized`, 403 `forbidden`,
404 `not_found`, 409 `conflict`, 422 `validation_error`, 429 `too_many_requests`,
502 `upstream_error`, 503 `service_unavailable`; other HTTP exceptions use `error`.
Explicit structured exception details may supply their own code/message/hint.

`GET /health` checks only the local database and monitor flag; no external API call.
A successful database query returns HTTP 200 with this shape:
```json
{"status":"ok","version":"1.1.0","database":"ok","monitor":"running","time":"2026-09-09T12:00:00+00:00"}
```
The monitor field is `running` or `stopped`; it is informational and does not
change the status code. A database failure returns HTTP 503 with `status` set to
`degraded` and `database` set to `error`, preserving the other fields.
See [exhaustive endpoint reference](/llms-full.txt), [OpenAPI JSON](/openapi.json),
and [interactive API documentation](/docs).
