How to automatically confirm order payment through a webhook
Link an invoice to an order, verify webhook signatures and guard fulfilment against retries, underpayment and rollbacks.
Updated:
To let your website mark an order as paid automatically, create an invoice for it in Cryptanio and configure a webhook: a notification the service sends to your server when a payment changes state. The server verifies the message, finds the order, and checks the invoice state. It can then grant the purchased access or send the order for fulfilment.
We will use a store that creates an invoice priced in dollars as a unit of account, while the buyer chooses an available cryptocurrency and network on the payment page. Funds arrive at the store's addresses. Orders and product delivery stay in the store's application; Cryptanio connects the invoice to the transfer and reports its state. This integration requires a developer or the ability to modify your website's server-side code.
Prepare the project and receiving addresses
In your Cryptanio account, select a project and add receiving addresses that belong to you. Connect them to Payments: ordinary address monitoring is not sufficient for issuing invoices. Check that the addresses are active and belong to the chosen network. Real payments require live-mode addresses.
Prepare several addresses in advance if customers can pay for orders at the same time. An address may be occupied by an open invoice or temporarily unavailable after it closes. Your integration needs to display a clear error when no address is free and let the customer try again later.
Create an API key and store it on the store's server. You will use it to call https://api.cryptanio.com/v1. Do not put the key in the page's JavaScript, a mobile application, or a public repository.
Configure the notification receiver
Open “Notifications” for the selected project. If “Use account notification settings” is enabled, configure Webhook through “Account settings”, or turn inheritance off and set the project’s own endpoint.
In the Webhook card, enter your handler’s HTTPS address in “Endpoint URL” and click “Save”. The handler must accept POST requests directly: redirects to another URL are not used. Obtain the “Signing secret” in the same card and keep it on the server separately from the API key.
Make sure Webhook is not paused. Handle payment.confirmed and payment.rolled_back in your receiver, and use payment.pending for an intermediate status if needed. Before fulfilling the order, verify live mode and the other conditions below. There is no need to enable a separate Webhook channel for an invoice address.
Create an invoice for a specific order
First save the order in the store's database. The server then creates an invoice through POST /v1/invoices. Supply your order identifier in reference. In this example, the order price is 12.50 USD: invoice_currency and invoice_amount define the price, while the buyer chooses an available network and asset on the payment page. Therefore, do not send chain, asset, amount, address, or tolerance in this request. Specifying USD does not mean accepting fiat money or exchanging the received cryptocurrency for dollars.
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${CRYPTANIO_OPERATION_ID:?Set CRYPTANIO_OPERATION_ID}"
: "${CRYPTANIO_PROJECT_ID:?Set CRYPTANIO_PROJECT_ID}"
json_string() {
local text="$1" char code index output='"'
for ((index=0; index<${#text}; index++)); do
char="${text:index:1}"
case "$char" in
'"'|'\') output+="\\$char" ;;
*) printf -v code '%d' "'$char"
if ((code < 32)); then printf -v char '\\u%04x' "$code"; fi
output+="$char" ;;
esac
done
printf '%s"' "$output"
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
--silent --show-error --max-time 30 --request POST \
--header "Authorization: Bearer $CRYPTANIO_API_KEY" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: $CRYPTANIO_OPERATION_ID" \
--data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"invoice_currency":"USD","invoice_amount":"12.50","reference":"order-42"}' \
"https://api.cryptanio.com/v1/invoices" \
--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
Before running the example, set CRYPTANIO_API_KEY, CRYPTANIO_PROJECT_ID, and CRYPTANIO_OPERATION_ID on your server. Use a live API key with permission to create invoices; CRYPTANIO_PROJECT_ID must identify the prepared Payments project. CRYPTANIO_OPERATION_ID is the saved creation operation identifier for this order: 16 to 80 ASCII letters, digits, dashes, or underscores. The example sends it in the Idempotency-Key header, not in a JSON field. Generate a different value for a new order; keep the existing value when retrying the same request.
On successful creation, the API returns HTTP 201. Save the invoice id and the returned checkout_url alongside the order, and send the buyer to that URL. Until a payment method is selected, the network, address, and crypto amount have not been assigned. On the payment page, the buyer chooses an available method and receives the current payment details and deadline. If your own interface displays payment details, use the data for the specific payment attempt, the exact amount, and its expires_at deadline; do not round the amount or replace it with asked. Links returned by subsequent GET requests and the invoice list are read-only; store the interactive creation link separately and share it only with the intended buyer.
If the connection breaks after the request is sent, retry the same body with the same Idempotency-Key. A successful retry returns HTTP 201 and the same id. Changing the parameters with an already used key returns HTTP 409. reference connects an invoice to an order, but does not replace the idempotency key. Do not change the key merely because the request timed out.
Verify the incoming webhook
Cryptanio sends X-Timestamp, X-Signature, X-Event-Id, X-Event-Type, and X-Attempt. The signature is calculated as follows:
hex(HMAC-SHA256(secret, timestamp + "." + raw_body))
raw_body means the original request bytes. Obtain them before parsing JSON: serializing it again may change whitespace or field order and break verification. Limit the accepted body size and reject requests without a valid signature.
The example below uses a five-minute freshness window. This is a setting in your handler; it requires the server's clock to be synchronized.
import hashlib
import hmac
import re
import time
def verify_signature(secret, timestamp, raw_body, signature):
if not secret or not timestamp or not re.fullmatch(r"[0-9]{1,12}", timestamp):
return False
if not signature or not re.fullmatch(r"[0-9a-f]{64}", signature):
return False
if abs(time.time() - int(timestamp)) > 300:
return False
signed = timestamp.encode("ascii") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
The function verifies the signature and request time. After verification succeeds, parse the JSON and check schema_version="v1", the expected project_id, mode="live", and sandbox=false.
Use event_id and event_type from the signed body for decisions. You can compare the corresponding headers against these fields; those event headers are not themselves protected by the signature. Run payment logic only for the expected payment object with direction in.
Distinguish transfer confirmation from order payment
The main store events are:
payment.pending— the transfer has been detected and is awaiting confirmation;payment.confirmed— the transfer has reached the required confirmations;payment.rolled_back— a previously detected transfer was revoked after a change to the blockchain.
A payment.confirmed event does not yet mean that a particular order is fully paid. Check the invoice object and match invoice.id and invoice.reference to the saved order. Do not automatically assign a message without an associated invoice to a purchase merely because the amount looks similar.
Before fulfilment, request the current state through GET /v1/invoices/{id}. Check id, project_id, reference, the order price in invoice_currency and invoice_amount, and sandbox=false. Check the network, asset, address, and selected payment attempt; if there are multiple attempts, take settled_attempt_id and settled_attempt into account. Successful payment corresponds to invoice status paid; pending, seen, and partial do not authorize fulfilment. The buyer returning to your site or opening a payment link does not confirm payment.
An underpayment can have status underpaid, a partial payment awaiting a top-up has partial, and a late arrival has late. For an overpayment, the project policy can leave the invoice in paid or set overpaid with needs_decision=true. States underpaid, overpaid, and late require a separate decision before fulfilling the order. The API sends amount_raw and paid as strings. To compare them, parse them as exact integers in the asset's smallest units; decimals defines the scale. Do not use binary floating-point calculations. Define how to handle any difference in advance; this does not imply an automatic refund.
Handle retries and rollbacks
Delivery can include repeated messages. Save the verified event in durable storage and use event_id as a unique key. A repeat of an already accepted event must receive a successful response without delivering the product again.
A transaction identifier alone is not enough to discard all subsequent messages: a transfer passes through several states. For the combination of chain, tx_id, index, address, and asset identity, save the last applied payment.revision. Apply a transition only when its revision is greater than the one already processed.
Commit the order change and the fulfilment job consistently, with protection against concurrent processing. The job must have a unique order key. Immediately before execution, check that the order is still authorized for fulfilment. If a rollback arrives, update the state and stop any unexecuted fulfilment; for access already granted, provide a process for review or suspension.
Check the complete flow
Before taking a real payment, test body parsing, signature verification, and duplicate protection with prepared data. A service event of type webhook.test does not confirm a purchase or replace a full workflow test.
Then use your own control order to check the full process: creating the invoice, sending the transfer, confirming it, and updating the order. Separately test duplicate events, underpayment, and rollback with prepared data. Return HTTP 2xx after reliably saving the accepted message; if it cannot temporarily be saved, return a server error so delivery can be retried.
In your account, match the event and delivery log against the record in your application. The check is complete when a confirmed order is fulfilled exactly once, while an intermediate, unrelated, or repeated event cannot cause incorrect fulfilment.
Reference: API v1 invoices, webhook signature verification.