Invoices
Hosted-invoice endpoints. All three require the X-API-Key header - see
README.md.
Base URL: https://api.geat.io (pending final deploy confirmation).
| Method | Path |
|---|---|
| POST | /v1/invoices |
| GET | /v1/invoices/{invoice_uuid} |
| POST | /v1/invoices/{invoice_uuid}/reconcile |
A path parameter that is not a well-formed UUID answers 404
NOT_FOUND before the handler runs.
POST /v1/invoices
Create an invoice and get back its hosted payment-page URL.
Request body
| Field | Type | Required | Rules |
|---|---|---|---|
profile_uuid | string (UUID) | yes | A non-deleted payment profile owned by the key's account. |
invoice_id | string | yes | Your own reference. Max 255 chars, pattern ^[A-Za-z0-9._-]+$. Not unique - the same value may be reused. |
price | number or numeric string | yes | Greater than 0, at most 1,000,000, finite. |
success_url | string (URL) | yes | http/https with a host. Where the payer is sent after a successful payment. |
callback_url | string (URL) | yes | Webhook target. Must resolve to a public address - private, loopback and link-local destinations are rejected. |
failure_url | string (URL) | yes | http/https with a host. |
redirect | boolean | no (default false) | When true the endpoint answers 302 to the payment page instead of a JSON body. |
customer_email | string | no | Validated email; used for the payer receipt. |
currency | string | no | Fiat symbol, upper-cased. Must be an active currency. Defaults to the profile's currency. |
Example request
curl -X POST https://api.geat.io/v1/invoices \
-H "X-API-Key: $GEATIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile_uuid": "2c9a1f44-5d3b-4f0e-9a21-7c6b8e2d4f10",
"invoice_id": "ORDER-1042",
"price": 49.00,
"currency": "USD",
"customer_email": "payer@example.com",
"success_url": "https://shop.example.com/thanks",
"failure_url": "https://shop.example.com/oops",
"callback_url": "https://shop.example.com/hooks/geatio",
"redirect": false
}'
Response - 201 Created
{
"success": true,
"data": {
"uuid": "0f0f6a6c-7e2e-4a4e-9f4b-9a6d0f3a1b22",
"invoice_id": "ORDER-1042",
"price": "49.00",
"currency": "USD",
"currency_name": "United States Dollar",
"customer_email": "payer@example.com",
"status": "created",
"redirect": false,
"created_at": "2026-10-14T09:12:04.118233+00:00",
"invoice_url": "https://pay.geat.io/0f0f6a6c-7e2e-4a4e-9f4b-9a6d0f3a1b22"
}
}
invoice_url is built from the server's configured invoice base URL plus
the invoice UUID. Send the payer there; everything after that is handled
by the payer endpoints in Payer endpoints.
With redirect: true the response is instead a 302 whose Location
is that same URL.
Errors
| Status | Code | When |
|---|---|---|
| 400 | MISSING_FIELDS | profile_uuid, invoice_id, or any of the three URLs is absent/blank. |
| 400 | INVALID_INVOICE_ID | Longer than 255 chars, or contains characters outside A-Za-z0-9._-. |
| 400 | INVALID_PRICE | Not a number, not positive, not finite, or above 1,000,000. |
| 400 | INVALID_URL | success_url/failure_url is not a valid http(s) URL, or callback_url is not a valid public URL. |
| 400 | INVALID_EMAIL | customer_email is not a valid address. |
| 400 | INVALID_CURRENCY | The requested currency is not an active fiat currency. |
| 400 | INVALID_PROFILE_TYPE | The profile exists but is not a payment profile. |
| 400 | NO_COINS | The profile has no accepted coins configured. |
| 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 returned for a malformed profile_uuid). |
| 503 | RATE_UNAVAILABLE | The price could not be converted to USD - exchange rates are temporarily unavailable. |
GET /v1/invoices/{invoice_uuid}
Read one invoice. Scoped to the key's account: another merchant's invoice answers 404.
Response - 200 OK
Identical body to the create response (the model dict plus
invoice_url), with one deliberate difference: the internal status
expired_partial is reported as expired. The external contract
exposes only the status set the webhooks promise; the partial-receipt
distinction stays internal for the payer page.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown UUID, malformed UUID, soft-deleted invoice, or an invoice belonging to another account. |
Invoice status lifecycle
created ──> waiting ──> partial ──> pending ──> completed │ │ │ │ │ └──> expired_partial ──(API: reported as expired) │ └──> expired └──> failed
| Status | Meaning |
|---|---|
created | Invoice exists; the payer has not picked a coin yet. |
waiting | A coin is locked and an address is issued; waiting for funds. |
partial | Some funds arrived but less than the expected amount. |
pending | Enough has arrived; waiting for the coin's confirmation threshold. |
completed | Confirmed and credited. Terminal. Emits invoice.completed. |
expired | The payment window closed with nothing credited. Emits invoice.expired. |
expired_partial | Internal only: expired after a partial receipt. The v1 API reports this as expired. |
failed | Cancelled or otherwise dead. Terminal. |
The payer-facing invoice-session status (returned by the pay endpoints) is a separate, narrower set - see Payer endpoints.
POST /v1/invoices/{invoice_uuid}/reconcile
Force a re-check of a paid-but-stuck invoice. The handler pulls the confirmed transaction universe for the invoice's watched address directly from the watch service and completes the invoice if the pulled total now covers the expected amount.
Non-custodial and idempotent: it never moves funds, only mirrors on-chain truth into the invoice ledger, and a concurrent scheduler completion makes one of the two callers a no-op rather than double-crediting.
Request
No body. Path parameter only.
Response - 200 OK
{
"success": true,
"data": {
"status": "completed",
"settled": true,
"reconciled": true,
"confirmed_amount": "0.00412000",
"expected_amount": "0.00412000"
}
}
| Field | Type | Notes |
|---|---|---|
status | string | The invoice status after the call. expired_partial is reported as expired, as everywhere else on v1. |
settled | boolean | true once the invoice is completed. |
reconciled | boolean | true only when this call performed the completion. A call that found the invoice already completed, or that lost the race to a concurrent completion, returns false. |
confirmed_amount | string | Fixed-point total pulled from the watch service. Absent on the already-completed short-circuit, which does no pull. |
expected_amount | string | Fixed-point amount the invoice is waiting for. Absent on the same short-circuit. |
When the invoice was already complete the response also carries
"message": "Invoice was already completed.".
When the confirmed total is still short, the call is not an error: it
answers 200 with settled: false, reconciled: false and both amounts,
so you can show the shortfall.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown/malformed UUID, soft-deleted, or another account's invoice. |
| 409 | NOT_RECONCILABLE | No payment session, no coin selected, the invoice was cancelled, or there is no payable watched address for the selected coin. |
| 502 | WATCH_UNAVAILABLE | The watch service could not be reached, or the invoice has no active watch to verify against. |