Addresses API · Geat.io

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).

MethodPath
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

HeaderRequiredNotes
X-API-KeyyesSee README.md.
Idempotency-KeynoAt most 255 characters. See Idempotency below.

Request body

FieldTypeRequiredRules
profile_uuidstring (UUID)yesA non-deleted payment profile owned by the key's account.
coin_symbolstringyesUpper-cased before matching. Must be an active coin enabled on the profile.
networkstringnoLower-cased before matching. Required when the symbol exists on more than one network for this profile.
callback_urlstring (URL)noWebhook 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": []
  }
}
FieldTypeNotes
uuidstringThe address request's public identifier.
addressstringThe derived receive address, verbatim (EVM addresses are checksummed).
coinobjectsymbol, name, network. Each can be null if the coin row is gone.
statusstringwaiting, paid, or expired - see lifecycle.
qr_codestringA data:image/png;base64,... QR encoding of the address, or null when there is no address.
callback_urlstringEcho of what you sent, or null.
expires_atstring (ISO 8601)Defaults to 24 hours after creation.
created_atstring (ISO 8601)
tx_countintegerNumber of credited transactions on this address so far.
total_receivedstringSum of the credited net_amounts, 18 decimal places. Exactly "0" when nothing has been credited yet.
transactionsarrayCredited 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_received and transactions reflect 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

StatusCodeWhen
400INVALID_IDEMPOTENCY_KEYIdempotency-Key longer than 255 characters.
400MISSING_FIELDSprofile_uuid or coin_symbol absent/blank.
400INVALID_PROFILE_TYPEThe profile is not a payment profile.
400AMBIGUOUS_COINThe symbol exists on several networks for this profile and no network was given. The message lists the available networks.
400COIN_NOT_ACCEPTEDThe coin is not enabled on this profile, or is not active.
400COIN_NOT_PAYABLEThe 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.
400INVALID_URLcallback_url is not a valid public URL.
402INSUFFICIENT_CREDITSThe merchant has no credits left. Top up to create more addresses.
403PROFILE_SUSPENDEDThe profile is suspended.
403PROFILE_INACTIVEThe profile is not active.
404NOT_FOUNDNo such profile under this account (also for a malformed profile_uuid).
409CONFLICTA uniqueness conflict fired but no replayable row is visible. Nothing was charged - retry.
409IDEMPOTENCY_KEY_REUSEDThe key was already used for a different profile/coin combination.
502WATCH_REGISTRATION_FAILEDThe 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

FieldTypeRequired
profile_uuidstring (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

StatusCodeWhen
400MISSING_FIELDSprofile_uuid absent/blank.
404NOT_FOUNDNo 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:

FieldTypePresent when
gross_amountstring (18 dp)Status is paid and a credited transaction exists. Taken from the first credited transaction.
fee_amountstring (18 dp)Same. Always 0 - see README.md.
net_amountstring (18 dp)Same.
gross_fiat_amountstring (2 dp)Additionally requires a rate row for the coin and the profile currency.
currencystringAccompanies 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

StatusCodeWhen
404NOT_FOUNDUnknown 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)──┘
StatusMeaning
waitingIssued, nothing credited yet.
paidAt least one confirmed payment has been credited.
expiredNothing 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.