Address requests
Bare receive addresses for merchants who want a deposit endpoint rather
than the full hosted invoice flow. All three endpoints require the
X-API-Key header - see README.md.
Base URL: https://api.geat.io (pending final deploy confirmation).
| Method | Path |
|---|---|
| POST | /v1/addresses |
| POST | /v1/addresses/rates |
| GET | /v1/addresses/{address_uuid} |
Note that /v1/addresses/rates is a POST, not a GET - it takes a
JSON body.
POST /v1/addresses
Derive a fresh receive address from the merchant's own xpub, register it with the watch service, and charge one credit.
Headers
| Header | Required | Notes |
|---|---|---|
X-API-Key | yes | See README.md. |
Idempotency-Key | no | At most 255 characters. See Idempotency below. |
Request body
| Field | Type | Required | Rules |
|---|---|---|---|
profile_uuid | string (UUID) | yes | A non-deleted payment profile owned by the key's account. |
coin_symbol | string | yes | Upper-cased before matching. Must be an active coin enabled on the profile. |
network | string | no | Lower-cased before matching. Required when the symbol exists on more than one network for this profile. |
callback_url | string (URL) | no | Webhook target for this address. Must resolve to a public address. |
Example request
curl -X POST https://api.geat.io/v1/addresses \
-H "X-API-Key: $GEATIO_API_KEY" \
-H "Idempotency-Key: deposit-2026-10-14-ORDER-1042" \
-H "Content-Type: application/json" \
-d '{
"profile_uuid": "2c9a1f44-5d3b-4f0e-9a21-7c6b8e2d4f10",
"coin_symbol": "BTC",
"network": "bitcoin",
"callback_url": "https://shop.example.com/hooks/geatio"
}'
Repeating that exact call returns the same address with 200 instead of 201, and charges nothing - see Idempotency.
Response - 201 Created
{
"success": true,
"data": {
"uuid": "6b1f1b6c-2f3a-4c57-8d1a-1d1e9e7a55c1",
"address": "bc1q...",
"coin": { "symbol": "BTC", "name": "Bitcoin", "network": "bitcoin" },
"status": "waiting",
"qr_code": "data:image/png;base64,iVBORw0KG...",
"callback_url": "https://shop.example.com/hooks/geatio",
"expires_at": "2026-10-15T09:12:04.118233+00:00",
"created_at": "2026-10-14T09:12:04.118233+00:00",
"tx_count": 0,
"total_received": "0",
"transactions": []
}
}
| Field | Type | Notes |
|---|---|---|
uuid | string | The address request's public identifier. |
address | string | The derived receive address, verbatim (EVM addresses are checksummed). |
coin | object | symbol, name, network. Each can be null if the coin row is gone. |
status | string | waiting, paid, or expired - see lifecycle. |
qr_code | string | A data:image/png;base64,... QR encoding of the address, or null when there is no address. |
callback_url | string | Echo of what you sent, or null. |
expires_at | string (ISO 8601) | Defaults to 24 hours after creation. |
created_at | string (ISO 8601) | |
tx_count | integer | Number of credited transactions on this address so far. |
total_received | string | Sum of the credited net_amounts, 18 decimal places. Exactly "0" when nothing has been credited yet. |
transactions | array | Credited transactions, oldest first. Each entry: txid, net_amount (18 dp string), credited_at (ISO 8601 or null). |
An address request is a deposit endpoint, not a one-shot: it can collect
more than one credited payment over its lifetime, which is what
tx_count, total_received and transactions report.
Idempotency
Send an Idempotency-Key header to make a retry safe. The key namespace
is per merchant account, so two merchants may use the same key value
without colliding.
- ·Without the header: unchanged behaviour. Every call charges a credit and issues a fresh address.
- ·Replay (same account, same key, same profile and coin): 200 OK
with the stored request's current representation - so
status,tx_count,total_receivedandtransactionsreflect the present state, not a frozen copy of the original 201. No credit is charged, no address is derived, no watch is registered. - ·An expired address request still replays under its key. A new address needs a new key.
- ·Cross-coin or cross-profile reuse: 409
IDEMPOTENCY_KEY_REUSED. Silently handing back some other coin's address would be a client trap. - ·Key longer than 255 characters: 400
INVALID_IDEMPOTENCY_KEY. - ·A whitespace-only header value is treated as no key at all.
- ·Two concurrent requests with the same key: the loser's transaction is rolled back entirely (credit refunded, no watch registered) and it then returns the winner's representation under the same replay rules.
Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_IDEMPOTENCY_KEY | Idempotency-Key longer than 255 characters. |
| 400 | MISSING_FIELDS | profile_uuid or coin_symbol absent/blank. |
| 400 | INVALID_PROFILE_TYPE | The profile is not a payment profile. |
| 400 | AMBIGUOUS_COIN | The symbol exists on several networks for this profile and no network was given. The message lists the available networks. |
| 400 | COIN_NOT_ACCEPTED | The coin is not enabled on this profile, or is not active. |
| 400 | COIN_NOT_PAYABLE | The coin has no watch-service mapping or no confirmation threshold, or the merchant has no active xpub for its chain. Checked before any credit is claimed. |
| 400 | INVALID_URL | callback_url is not a valid public URL. |
| 402 | INSUFFICIENT_CREDITS | The merchant has no credits left. Top up to create more addresses. |
| 403 | PROFILE_SUSPENDED | The profile is suspended. |
| 403 | PROFILE_INACTIVE | The profile is not active. |
| 404 | NOT_FOUND | No such profile under this account (also for a malformed profile_uuid). |
| 409 | CONFLICT | A uniqueness conflict fired but no replayable row is visible. Nothing was charged - retry. |
| 409 | IDEMPOTENCY_KEY_REUSED | The key was already used for a different profile/coin combination. |
| 502 | WATCH_REGISTRATION_FAILED | The watch service could not be reached. The transaction is rolled back and the credit refunded. |
POST /v1/addresses/rates
Current crypto rates for a profile's enabled coins, in that profile's fiat currency.
Request body
| Field | Type | Required |
|---|---|---|
profile_uuid | string (UUID) | yes |
Any profile owned by the account is accepted here - unlike the create endpoints, this one does not require a payment profile.
Example request
curl -X POST https://api.geat.io/v1/addresses/rates \
-H "X-API-Key: $GEATIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"profile_uuid": "2c9a1f44-5d3b-4f0e-9a21-7c6b8e2d4f10"}'
Response - 200 OK
{
"success": true,
"data": {
"rates": [
{
"symbol": "BTC",
"name": "Bitcoin",
"network": "bitcoin",
"rate": "61234.55000000",
"currency": "USD",
"updated_at": "2026-10-14T09:10:00+00:00"
}
],
"currency": "USD"
}
}
Only active coins enabled on the profile are listed. rate and
updated_at are null when no rate row exists yet for that coin and
currency pair.
Errors
| Status | Code | When |
|---|---|---|
| 400 | MISSING_FIELDS | profile_uuid absent/blank. |
| 404 | NOT_FOUND | No such profile under this account (also for a malformed UUID). |
GET /v1/addresses/{address_uuid}
Read one address request, including everything credited to it.
Response - 200 OK
The same object as the create response, with extra top-level fields when the request has been paid:
| Field | Type | Present when |
|---|---|---|
gross_amount | string (18 dp) | Status is paid and a credited transaction exists. Taken from the first credited transaction. |
fee_amount | string (18 dp) | Same. Always 0 - see README.md. |
net_amount | string (18 dp) | Same. |
gross_fiat_amount | string (2 dp) | Additionally requires a rate row for the coin and the profile currency. |
currency | string | Accompanies gross_fiat_amount. |
These five are legacy single-payment fields kept for integrators who only
ever expected one payment per address. The full history is in
transactions.
Reading a waiting request whose expires_at has passed flips its
status to expired as a side effect. That is not terminal: the scheduler
flips an expired request back to paid the moment a confirmed
transaction lands on it, because addresses get reused. Read expired as
"nothing credited yet as of this read".
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown UUID, malformed UUID, or an address request belonging to another account. |
Address request lifecycle
waiting ──(confirmed payment credited)──> paid │ ▲ └──(expires_at passed, read or sweep)──> expired ──(late payment)──┘
| Status | Meaning |
|---|---|
waiting | Issued, nothing credited yet. |
paid | At least one confirmed payment has been credited. |
expired | Nothing credited by expires_at (default: 24 hours after creation). Reversible - a later confirmed payment flips it back to paid. |
Payment notifications for these addresses are the address.paid and
address.payment webhooks - see Webhooks.