How to check payment through the API without a webhook
Server-side invoice checks through the API: payment statuses, bounded polling, errors, one-time order fulfilment and recovery after a restart.
Updated:
You can check payment without a webhook from your application's server: periodically read the invoice through the Cryptanio API, match it to the order and fulfil the order when its current status is paid. This is called polling. It requires a checking queue, error handling and protection against duplicate fulfilment: a successful HTTP response alone does not confirm payment.
This approach works when the application can run background tasks but receiving incoming notifications is not convenient yet. Changes are discovered on the next read; polling does not provide an immediate notification.
What you need
Store the connection between the order and its invoice in your database: its id, the project, your reference, the agreed price and the operating mode. Keep identifiers as strings. Check the invoice for that specific order, rather than a transaction you found with a similar amount.
Prepare a server-side API key with permission to read invoices, access to the payments service and the required project. A real order requires the corresponding live mode. Do not put the key in the browser, the buyer's link or a public log. Read-only access is sufficient for a separate reconciliation process.
Step 1. Retrieve the current state
The example reads a single invoice. Set CRYPTANIO_API_KEY and CRYPTANIO_RESOURCE_ID on the server; the latter is the stored invoice id, not the order number or TXID.
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_RESOURCE_ID:?Set CRYPTANIO_RESOURCE_ID}"
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
urlencode() {
local LC_ALL=C text="$1" char index
for ((index=0; index<${#text}; index++)); do
char="${text:index:1}"
case "$char" in [a-zA-Z0-9.~_-]) printf '%s' "$char" ;; *) printf '%%%02X' "'$char" ;; esac
done
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
--silent --show-error --max-time 30 --request GET \
--header "Authorization: Bearer $CRYPTANIO_API_KEY" \
--header 'Accept: application/json' \
"https://api.cryptanio.com/v1/invoices/$(urlencode "$CRYPTANIO_RESOURCE_ID")" \
--output "$response_file" --write-out '%{http_code}')" || transfer_status=$?
if [[ "$http_status" == 2?? ]]; then
cat "$response_file"
exit "$transfer_status"
fi
printf 'HTTP %s\n' "$http_status" >&2
cat "$response_file" >&2
exit 1
After HTTP 200, parse the JSON and compare id, project_id, reference and sandbox with your records. For a live order, expect sandbox=false. If you stored a price in invoice_currency and invoice_amount, check those too. An unrelated invoice or a record from another mode must not change the order.
The checkout_url link from GET is for viewing. Do not use it to replace the interactive link saved from the invoice creation response: reading the status does not create a new payment or issue a new interactive checkout.
Step 2. Separate waiting from successful payment
Make your decision based on the invoice's status:
pending— awaiting payment;seen— a transfer has been detected, but payment is not complete yet;partial— part of the amount is confirmed and waiting continues under the payment's rules;underpaid,overpaid,late— investigation is required; account forneeds_decisionand the seller's decision;paid— the invoice is recorded as paid; after checking its connection to the order, you can prepare fulfilment;expired,canceled— stop the buyer's active wait, but keep the record for later reconciliation.
Expiry does not rule out a late receipt. A manual decision and a transaction rollback can also change subsequent handling. Do not declare a state permanently final or delete the connection between the order and invoice when the page closes.
A confirmed transaction and a paid invoice are different things. Confirmations do not prove that the required asset, amount and payment details belong to this particular order. The unpaid order troubleshooting guide explains these differences in detail.
Step 3. Account for the amount and payment attempt
An invoice may have different attempts when the payment method changes. For a paid invoice, account for settled_attempt_id and settled_attempt. When investigating the history, check attempt_history.has_more and the continuation cursor; use payment_history for transfers belonging to the selected attempt. The first page does not necessarily contain the entire history.
amount is the exact crypto amount to send. amount_raw and paid are strings containing integers in the asset's smallest units; decimals defines the scale. Do not compare them as floating-point numbers or sort them alphabetically as ordinary strings. Your own amount check must not override the invoice status and project rules: an overpayment may be accepted or sent for review. Adding underpayments together without checking the conditions is also inappropriate.
Step 4. Limit polling and retries
Place checks in a server-side queue with limited concurrency. Multiple buyer tabs must not create independent polling loops. Account for the account's combined traffic: different API keys and modes share the quota. The API reference lists current limits.
Choose delays that suit your workflow and leave capacity for other requests. For a network error or HTTP 500/503, use bounded retries with increasing delays and random jitter. For HTTP 429, reduce the combined traffic; if Retry-After is present, wait for the specified pause. Do not replace a missing header with an immediate retry. Each request needs a timeout, and each retry sequence needs a limit, after which the task remains available for later investigation.
HTTP 401/403 require checking the key and permissions. For 404, check the ID, project and mode: access restrictions can hide an invoice. These errors do not mean that the client has paid nothing.
Step 5. Persist fulfilment and recovery
When the status is paid, persist the order state and fulfilment task consistently. Use a unique order key and protection against concurrent processing. Reading paid again must not issue access or goods a second time; a failure after delivery must not create duplicate fulfilment.
Keep the last successful check and the next task in durable storage. After a restart, resume unfinished checks and tasks instead of starting the process only when the page opens. Bound active waiting by the timeframes of your workflow; separate background reconciliation must account for late payments, decisions and rollbacks, including previously paid orders. Before pending fulfilment is carried out, recheck whether it is still allowed; provide a review process for access already granted.
Use prepared responses to check repeated paid, a timeout, a restart and a transition after a rollback. The result is correct when the order is fulfilled once and an uncertain or disputed payment remains under review. Later, you can supplement polling with a webhook while keeping the same fulfilment protection.