BlockonomicsAPI Reference
Guides

Payment Intents

Collect a fiat amount in BTC or USDT. Your server creates the intent, and the payer, a person or an AI agent, pays it with only the intent ID.


Overview

A payment intent is a fiat amount you want collected, such as 10.00 USD. Your server creates it with your API key. The payer pays it through the public Payer API using only the intent ID: they pick a crypto, get an address and an exact amount, and send the funds. Blockonomics tracks the payments and calls your callback URL as they move the intent to processing, underpaid and paid.

Your server never converts prices, generates addresses or watches the blockchain. The payer can split the amount across several payments, even in different cryptos.

WhoAuthenticates withDoes
Your serverAPI keyCreates intents, receives callbacks, checks the status before fulfilling
The payer: your checkout page, the checkout widget or an AI agentThe intent IDChecks what is due, gets a quote, pays, and submits USDT transaction hashes

How It Works

Payment intent flowYour server creates a payment intent and passes its ID to the payer. The payer checks the status, gets a quote, and sends funds directly to Blockonomics without an API key. Blockonomics calls your server back when the intent is paid, and your server verifies it before fulfilling the order.PayerIntent ID, no API keyYour serverAPI keyBlockonomicsAPI + blockchainCreate payment intent1Intent ID pi_…Intent ID2Get payment status3due_amount, accepted_cryptosCreate payment quote4address, crypto_amount, 5-min price lockSend funds (USDT: submit txhash)5Callback: paid6Get payment intentstatus paid → fulfil order
Numbers match the steps below.
  1. Your server creates a payment intent for the order total.
  2. You give the intent id to the payer.
  3. The payer calls Get payment status to see due_amount and accepted_cryptos.
  4. The payer calls Create a payment quote with a crypto, and gets an address, a crypto_amount and a 5-minute price lock.
  5. The payer sends the funds. For USDT, they also submit the transaction hash.
  6. Blockonomics calls your callback URL as the status moves through processing to paid. Your server fetches the intent and fulfils the order on paid.

Setup

You need a store with a callback URL and a wallet for each crypto you accept. Follow the setup steps in Receiving Payments.

The payer can only pay in the cryptos the store has a wallet for. These are listed in accepted_cryptos.


Create a Payment Intent

Call Create a payment intent from your server when the customer checks out:

bash
curl -X POST https://www.blockonomics.co/api/v2/payment_intents \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": "10.00", "currency": "USD", "extra_data": "order-1234"}'
json
{
  "data": {
    "id": "pi_5GfD5m1b8khzFSLza91VR6",
    "store_id": 1243,
    "amount": "10.00",
    "currency": "USD",
    "status": "unpaid",
    "due_amount": "10.00",
    "extra_data": "order-1234",
    "created_at": 1793615164,
    "payments": []
  }
}
  • extra_data is your own reference, up to 2048 characters. Use it to link the intent to your order. The payer never sees it.
  • match_callback picks the store when your account has more than one. Pass part of that store's callback URL, such as "example.com".
  • Amounts are decimal strings with 2 decimals, in requests and responses.
  • Intents can't be changed and don't expire. Only quotes expire. If the total changes, create a new intent.

Store the intent id with your order.


Let the Customer Pay

The intent id is all the payer needs. Anyone who has it can see the amount and pay it, so share it only with the payer.

On Your Website: the Checkout Widget

The checkout widget runs the whole payer flow: crypto selection, quote, QR code, USDT wallet connection, and a receipt with a Pay remaining button after an underpayment.

html
<script src="https://www.blockonomics.co/js/checkout.js"></script>

<div id="payment_area"></div>

<script>
  Blockonomics.checkout({
    msg_area: 'payment_area',
    intent_id: 'pi_5GfD5m1b8khzFSLza91VR6',
  })
</script>
OptionRequiredDescription
msg_areaYesid of the element to render into
intent_idYesThe payment intent ID
onPaymentDetectedNoCalled once a payment covering the full amount is seen on the network, before it confirms. Receives { txid, value, expected, crypto, intentId }. Don't fulfil the order here: wait for the paid callback.

Style it with CSS custom properties on the blockonomics-checkout element, such as --bk-bg, --bk-fg, --bk-accent, --bk-font and --bk-radius.

With an AI Agent or Your Own UI: the Payer API

The Payer API needs no API key and allows cross-origin requests, so it works from a browser, a mobile app or an AI agent.

1. Check what is due.

bash
curl https://www.blockonomics.co/api/v2/pay_stat/pi_5GfD5m1b8khzFSLza91VR6
json
{
  "data": {
    "id": "pi_5GfD5m1b8khzFSLza91VR6",
    "amount": "10.00",
    "currency": "USD",
    "status": "unpaid",
    "due_amount": "10.00",
    "created_at": 1793615164,
    "accepted_cryptos": ["BTC", "USDT"],
    "testmode": false
  }
}

2. Get a quote in one of accepted_cryptos.

bash
curl -X POST https://www.blockonomics.co/api/v2/pay/pi_5GfD5m1b8khzFSLza91VR6 \
  -H "Content-Type: application/json" \
  -d '{"crypto": "BTC"}'
json
{
  "data": {
    "id": "pq_8GgAyuReWzLbJtEQrTNaT5",
    "amount": "10.00",
    "crypto_amount": "12966",
    "paid_crypto_amount": "0",
    "crypto": "BTC",
    "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
    "status": "pending",
    "txid": null,
    "expires_timestamp": 1793615475,
    "paid_timestamp": null
  }
}

crypto_amount is in base units, as a string:

CryptoBase unitExample
BTCsatoshi, 8 decimals"12966" = 0.00012966 BTC
USDT6 decimals"10013511" = 10.013511 USDT

The price is locked until expires_timestamp, 5 minutes after the quote is created. If it passes before the payer sends, call the same endpoint again. For the same crypto and the same due_amount, you get the same quote and address back with a new price and a new 5-minute lock. This also makes it safe to call on every page load.

3. Send the payment.

  • BTC: send exactly crypto_amount satoshis to address, for example from the payment URI bitcoin:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh?amount=0.00012966. Each BTC quote has its own address, so the payment is detected automatically.
  • USDT: send an ERC-20 transfer of crypto_amount units of USDT (contract 0xdAC17F958D2ee523a2206206994597C13D831ec7 on Ethereum) to address. The payer also needs ETH for gas. Then submit the transaction hash, as below.

4. USDT only: submit the transaction hash.

All USDT quotes on a store share the store's wallet address, so the transaction hash is the only way to tell which intent a transfer pays. A USDT payment whose hash is never submitted is not detected.

bash
curl -X POST https://www.blockonomics.co/api/v2/pay/pi_5GfD5m1b8khzFSLza91VR6/submit \
  -H "Content-Type: application/json" \
  -d '{"crypto": "USDT", "txhash": "0x9b1c7f3e2a4d5b6c8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"}'

The response is 200 with an empty body. Submit the hash as soon as you have it. If your wallet can sign without broadcasting, submit the hash first and broadcast after. Submitting the same hash again returns 200, so retrying is safe.

5. Poll until the payment is detected. Call Get payment status, at most every 10 seconds:

  • due_amount above 0.00: something is still owed. Get a quote for it (step 2).
  • due_amount is 0.00 and status is processing: the payment is detected and waiting for confirmations. The payer is done.
  • status is paid: the payment is confirmed.

Telling an Agent How to Pay

An agent that hits your checkout or a paid endpoint needs the intent ID and where to find these docs. For example, your API can answer with 402 Payment Required:

json
{
  "error": "payment_required",
  "payment_intent_id": "pi_5GfD5m1b8khzFSLza91VR6",
  "amount": "10.00",
  "currency": "USD",
  "pay_status_url": "https://www.blockonomics.co/api/v2/pay_stat/pi_5GfD5m1b8khzFSLza91VR6",
  "docs": "https://developers.blockonomics.co/llms.txt"
}

The response format is yours to choose. Blockonomics only needs the payer to call the Payer API with the intent ID.


Statuses

Payment Intent

StatusMeaningPayerYour server
unpaidNo payment detectedPay due_amountWait
processingA payment is detected but not confirmedPay due_amount if it is above 0.00, otherwise waitWait
underpaidConfirmed payments cover part of amount, nothing else is in progressPay due_amountWait for a top-up, or contact the customer
paidConfirmed payments cover amountNothingFulfil the order

due_amount counts a payment as soon as it is detected. status only counts confirmed payments. So due_amount can be 0.00 while status is still processing. Fulfil on status, never on due_amount.

Payment Quote

StatusMeaning
pendingWaiting for payment
unconfirmedPayment detected, no confirmations yet
partially_confirmedPayment has confirmations but is not final
confirmedPayment is final and credited to the intent
failedThe USDT transaction reverted on-chain and is not credited. See Failed payments.

A quote is valid only until expires_timestamp, even though its status still reads pending. Create a quote again to renew it.


Partial Payments and Top-ups

A payer can pay in several parts, in the same crypto or different ones. Each payment is credited at the price of the quote it was sent to.

  1. The payer sends 5 USDT on a 10.00 USD intent. Once the payment is detected, status is processing and due_amount is 5.01.
  2. The payer calls Create a payment quote again, in any crypto. They get a new quote for 5.01. They don't need to wait for the first payment to confirm.
  3. When both payments confirm, status is paid.

If the first payment confirms before a top-up arrives, status becomes underpaid and your server gets a callback. The intent stays open, and the payer can top it up at any time.

If a top-up is requested before Blockonomics knows how much the first payment was, the request fails with 409 and code 1079. Retry after a few seconds.

Overpayments are credited in full. due_amount stops at 0.00, status is paid, and nothing is refunded automatically. Compare paid_crypto_amount with crypto_amount in payments to find the excess.

To work out the fiat value of a payment, use amount × paid_crypto_amount / crypto_amount from its entry in payments.

Failed Payments

A USDT transaction can be detected and then revert when it is mined, for example when the sender doesn't hold enough USDT. Its quote becomes failed, its credit comes off due_amount, and the intent goes back to unpaid or underpaid. No callback is sent for a failed payment. The change shows in Get a payment intent and Get payment status, and the payer can pay again with a new quote.


Callbacks

Blockonomics sends an HTTP GET to your store's callback URL each time a payment moves an intent to processing, underpaid or paid. A payment event that leaves the status unchanged, such as a first confirmation, sends no callback, and neither does a failed payment.

GET https://example.com/blockonomics/callback?secret=YOUR_SECRET&id=pi_5GfD5m1b8khzFSLza91VR6&status=paid&amount=10.00&currency=USD&due_amount=0.00&extra_data=b3JkZXItMTIzNA%3D%3D
ParameterDescription
idPayment intent ID
statusNew status: processing, underpaid or paid
amountTotal amount, in currency
currencyFiat currency code
due_amountAmount still to pay, counting payments not confirmed yet
extra_dataYour extra_data, base64-encoded. Empty if the intent has none.

Payments to a payment intent send only this callback, never the per-address callback (addr, value, txid) described in Callbacks. If your store also uses the per-address flow, the id parameter tells the two apart.

Handling the Callback

  1. Check the secret in your callback URL.
  2. Fetch the intent with Get a payment intent. Callbacks are not signed, so read the status from the API, not from the query string.
  3. If status is paid, fulfil the order. Make this idempotent: the same status can arrive more than once.
  4. Return 200.
javascript
// Express.js example
app.get('/blockonomics/callback', async (req, res) => {
  if (req.query.secret !== process.env.BLOCKONOMICS_CALLBACK_SECRET) {
    return res.status(403).send('Forbidden')
  }

  const response = await fetch(
    `https://www.blockonomics.co/api/v2/payment_intents/${encodeURIComponent(req.query.id)}`,
    { headers: { Authorization: `Bearer ${process.env.BLOCKONOMICS_API_KEY}` } }
  )
  const { data: intent } = await response.json()

  if (intent.status === 'paid') {
    await fulfilOrder(intent.extra_data, intent) // no-op if already fulfilled
  }

  res.send('OK')
})

A callback that doesn't get a 200 is retried up to 7 times with exponential backoff. After the last failure, Blockonomics emails you. Every attempt is listed at Log/Test Bench → Callback Logs.


Testing

Turn on Testmode for the store, as described in Testing. Quotes then get test addresses (1TestBTCAddress…, 0xTestUSDTAddress…), testmode is true in Get payment status, and callbacks fire as they would for real payments.

Do not send real BTC or USDT to test addresses — funds sent will be lost.

BTC: create a BTC quote, then go to Log/Test Bench. Under Test Bitcoin Wallet, send to the quote's address. Send less than crypto_amount to test an underpayment. The payment is detected at once and confirmed after about 10 minutes.

USDT: create a USDT quote, then submit the hash TestUSDTTxid_<base units>_<unique suffix>. It simulates a payment of that many base units, detected at once and confirmed after about 1 minute. Use a new suffix every time: like a real hash, a test hash can only be used once.

bash
# Pays 5 USDT, an underpayment on a 10.00 USD intent
curl -X POST https://www.blockonomics.co/api/v2/pay/pi_5GfD5m1b8khzFSLza91VR6/submit \
  -H "Content-Type: application/json" \
  -d '{"crypto": "USDT", "txhash": "TestUSDTTxid_5000000_order1234a"}'

The checkout widget's test wallet does this for you.


Errors

Errors use the standard format:

json
{ "error": { "code": 1010, "message": "Invalid API parameter. crypto is required" } }
HTTPCodeWhen
4001010A parameter is missing or invalid. The message says which.
4001055The request body is missing or is not valid JSON
4001049No store matches match_callback
4001032Your account has several stores and match_callback was left out
4001050The USDT transaction hash is not valid
4001038The transaction hash was already used for another payment
401Missing or invalid API key. The body is empty.
404The intent doesn't exist or isn't yours, or there is no open quote to submit a hash to. The body may not be JSON, so check the status code.
4091078The intent is already paid
4091079A payment already covers the intent, or is still being processed. Retry after a few seconds if you are topping up.
4091041The store has no wallet for this crypto
4221029Unsupported crypto
4221069Unsupported fiat currency