Source: https://cryptanio.com/wiki/confirm-orders-with-crypto-payment-webhooks/
Language: en

# 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: 2026-11-14

To let your website mark an order as paid automatically\, create an [invoice](<https://cryptanio.com/wiki/what-is-an-invoice/>) for it in Cryptanio and configure a [webhook](<https://cryptanio.com/wiki/what-is-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 {#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 {#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 {#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\.

```bash
#!/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
```

[Download example](</wiki/examples/ace23bd7c3cba0e47dafdcd6075a506ddb2ec8d6581c6129af815c1558f5a8fc.sh>)

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 {#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\:

```text
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\.

```python
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 {#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 {#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 {#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](<https://cryptanio.com/docs.html#invoices>)\, [webhook signature verification](<https://cryptanio.com/docs.html#verify>)\.

Tags: [Payments](https://cryptanio.com/wiki/tag/payments/), [Webhooks](https://cryptanio.com/wiki/tag/webhooks/)

## Related articles

- [What is an invoice](https://cryptanio.com/wiki/what-is-an-invoice/)
- [What is a webhook](https://cryptanio.com/wiki/what-is-a-webhook/)
- [How to accept crypto payments in a Telegram bot](https://cryptanio.com/wiki/accept-crypto-payments-telegram-bot/)
