Start free
English

Developers

A crypto payment API built for direct settlement

Create an invoice with a single authenticated request and Unheld returns the address to pay, the exact amount, and the confirmation depth it will wait for. Payments settle straight to a wallet you control — the API never takes custody of anything it reports on.

Create an invoice

One POST. The amount is written the way a person writes it and scaled server-side by the asset’s own decimals, so you never hand-convert to base units.

curl -X POST https://api.unheld.io/api/v1/invoices \
  -H "Authorization: Bearer $UNHELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chainId": "11155111",
    "assetType": "native",
    "amountDecimal": "0.05",
    "expiresInSeconds": 900,
    "metadata": { "orderId": "A-1043" }
  }'

Response

{
  "id": "e2b0…",
  "status": "CREATED",
  "receiveAddress": "0x…",
  "expectedAmount": "50000000000000000",
  "minConfirmations": 3,
  "expiresAt": "2026-08-10T14:15:00.000Z",
  "instructions": {
    "chainId": "11155111",
    "to": "0x…",
    "amount": "50000000000000000",
    "assetType": "native"
  }
}

receiveAddress is derived from your wallet and locked to this invoice. expectedAmount is in base units. minConfirmations is the chain’s own threshold, not a setting we invented.

Authentication and scopes

Keys are bearer tokens, created in the dashboard and shown once. Every key carries an explicit scope list and a request is rejected if the key lacks the scope for that route.

Available scopes

  • invoices:write
  • invoices:read
  • webhooks:write
  • webhooks:read
  • balances:read
  • customers:write
  • customers:read
  • subscriptions:write
  • subscriptions:read

Grant the narrowest set that works. A key that only creates invoices cannot read your customers, and a leaked read key cannot move anything — because nothing in this API can.

Webhooks you can verify

Every delivery is signed. The signature is an HMAC-SHA256 of the timestamp and the raw body joined by a dot, using your webhook secret, sent as a hex digest.

Verify a delivery

const signature = crypto
  .createHmac("sha256", webhookSecret)
  .update(`${req.headers["x-timestamp"]}.${rawBody}`)
  .digest("hex");

crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(req.headers["x-signature"])
);

Compare digests in constant time, reject a timestamp outside your tolerance window, and treat X-Request-Id as the idempotency key — retries reuse it, so the same event arriving twice is normal and must not be processed twice.

Invoice events

A payment moves through states rather than flipping from unpaid to paid, and every transition is a webhook. Underpayment and overpayment are their own events, not errors.

EventWhat happened
invoice.createdThe invoice exists and the receive address is watched.
invoice.receivingA paying transaction was seen on-chain but is not yet confirmed.
invoice.confirmingThe payment is confirming and has not reached the chain’s threshold.
invoice.paidFully confirmed at the chain’s required depth. Safe to fulfil.
invoice.underpaidLess arrived than expected. Partial payments accumulate, so a second transfer can complete it.
invoice.overpaidMore arrived than expected, recorded exactly rather than rounded away.
invoice.expiredThe window closed without full payment.

Test the whole flow free

Test invoice creation, detection, confirmations and webhooks without using production funds. Testnet tokens have no monetary value. Testnet activity is not a production payment.

Scope

  • It never holds funds. There is no balance endpoint to withdraw from, because payments land on your address and stay there.
  • It does not convert or settle in fiat. Payments arrive in the asset and on the chain they were sent on.
  • There are no official SDKs yet. Everything here is plain HTTP, and examples are published rather than packages we would have to maintain badly.

Developer questions

Is there a sandbox?

Test invoice creation, detection, confirmations and webhooks without using production funds. Testnet tokens have no monetary value. Testnet activity is not a production payment.

How do I avoid processing the same webhook twice?

Use X-Request-Id as an idempotency key. Retries reuse the same value, so record it and ignore a repeat. Retries are expected — a delivery that fails is retried on a schedule rather than dropped.

Do I have to convert amounts to base units myself?

No. Send amountDecimal as a person would write it and the API scales it by the asset’s own decimals. The response returns expectedAmount in base units so both representations are explicit and cannot drift.

Examples verified against the live API on .

Build against testnet first

Create a key, point it at a testnet chain, and run the whole flow before anything real moves.

Start free

Pricing · Supported networks and assets. · Your money does not pass through us.