# Host-to-host (H2H): show the crypto payment in your own interface

> Show a CryptoLike payment on your own screen: address, exact amount, network, QR and timer from the invoice, coin choice via the API, orders confirmed by webhook.

Source: https://cryptolike.net/docs/h2h
Updated: 2026-10-18

Host-to-host (H2H) means your server creates the payment through the CryptoLike API and your own interface shows it: the payer never opens a CryptoLike page. Every invoice response carries what the screen needs — the address, the exact amount, the network, the QR payload and the deadline — and a signed webhook tells your server when the payment is confirmed. This page lists when to choose H2H, the flow, what the screen must show and how to handle the cases where the payer sends too little, too much or too late.

## When to choose H2H, the hosted page or the widget

All three show the same invoice with the same statuses and webhooks; they differ in who draws the payment screen.

| Way | Who draws the screen | Choose it when | What you build |
|---|---|---|---|
| Hosted page | CryptoLike, at `checkout_url` | You want the shortest path: a redirect and a webhook | A redirect from your backend |
| Checkout widget | CryptoLike, in a frame on your page | The customer should stay on your checkout, and the CryptoLike page may appear inside it | Your origin on the allowlist, a script and a CSP rule |
| H2H | You | A mobile app, a kiosk, a messenger bot or a checkout that must look entirely like your product | The payment screen with every element listed below |

The hosted page and the widget already show the network warning, the timer, the live status and the texts for an underpayment or an expired payment in English, Russian and Spanish. With H2H that becomes your work, so start with the [hosted page or the widget](https://cryptolike.net/docs#customer-payment) unless your interface needs to be your own. The widget is described in the [checkout widget guide](https://cryptolike.net/docs/widget).

## The flow

1. **Your server creates the invoice** — `POST /v1/invoices`, signed with your API key, with an `Idempotency-Key` header. Pass `coin_id` to fix the coin now; omit it to let the payer choose in your interface.
2. **For an invoice without a coin, the payer chooses.** Your server reads `GET /v1/invoices/{id}/coin-options`, your interface lists the coins, and your server fixes the choice with `POST /v1/invoices/{id}/coin`.
3. **Your interface shows the payment** from the invoice response: network, address, `payer_amount`, QR, timer to `ttl_expires_at`, status.
4. **Your server tracks the status** through the `invoice.*` webhooks and, for the confirmation counter, `GET /v1/invoices/{id}` — no more often than every few seconds. Your page asks your server, never the CryptoLike API.
5. **Your server confirms the order** only on a verified `invoice.paid` webhook, then your interface shows the confirmation.

The API secret stays on your server. The browser or the app receives only what your server decides to show; it never signs requests. Signing is described in [authentication](https://cryptolike.net/docs/auth).

## Create the invoice

The request below fixes the coin at once (USDT on TRON). Without `coin_id` the response has the coin fields, `address` and the payment block set to `null` until the choice.

```http
POST /v1/invoices HTTP/1.1
Host: api.cryptolike.net
Content-Type: application/json
Idempotency-Key: order-10042
X-API-Key: <key_id>
X-Timestamp: <unix seconds>
X-Signature: <HMAC-SHA256 of the request>

{"amount": "49.90", "currency": "USD", "coin_id": "6f1c2a3e-8d4b-4c5a-9e7f-0a1b2c3d4e5f", "order_id": "10042", "description": "Order #10042 — annual plan", "ttl_seconds": 1800, "fee_paid_by": "merchant"}
```

The response is the invoice (example values from the API reference):

```json
{
  "id": "4e2bc88c-cb79-4afc-b864-c1ba8e9d50c9",
  "status": "new",
  "currency": "USD",
  "amount": "49.90",
  "coin_id": "6f1c2a3e-8d4b-4c5a-9e7f-0a1b2c3d4e5f",
  "ticker": "USDT",
  "network": "tron",
  "address": "TXYZopYRdj2D9XRtbG411XZZ3kM5VW9t7g",
  "amount_crypto": "49.90",
  "payer_amount": "49.90",
  "paid_amount": "0",
  "remaining_amount": "49.90",
  "overpaid_amount": "0",
  "amount_fiat": "49.90",
  "fiat_currency": "USD",
  "rate_locked": "1.0004",
  "rate_locked_at": "2026-09-16T12:00:00Z",
  "late_payment": false,
  "fee_paid_by": "merchant",
  "order_id": "10042",
  "description": "Order #10042 — annual plan",
  "return_url": null,
  "webhook_url": null,
  "metadata": null,
  "allowed_coin_ids": null,
  "checkout_url": "https://cryptolike.net/pay/4e2bc88c-cb79-4afc-b864-c1ba8e9d50c9",
  "ttl_expires_at": "2026-09-16T12:30:00Z",
  "expired_at": null,
  "created_at": "2026-09-16T12:00:00Z",
  "updated_at": "2026-09-16T12:00:00Z",
  "payment_uri": "tron:TXYZopYRdj2D9XRtbG411XZZ3kM5VW9t7g?amount=49.9&token=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
  "qr_payload": "TXYZopYRdj2D9XRtbG411XZZ3kM5VW9t7g",
  "confirmations": null,
  "required_confirmations": 20
}
```

The fields your screen uses:

- `payer_amount` and `ticker` — exactly what the payer must send. With `fee_paid_by: payer` the platform fee is already included, so `payer_amount` is larger than `amount_crypto`; show `payer_amount`, never `amount` or `amount_crypto`. Fees are on the [pricing page](https://cryptolike.net/pricing).
- `network` — `btc`, `ltc`, `eth` or `tron`. The same ticker can live on two networks (USDT on TRON and on Ethereum), so the network decides where the payer sends.
- `address` — the deposit address of this invoice.
- `payment_uri` — a link that opens a wallet with the address and the amount still owed: BIP-21 on Bitcoin and Litecoin, EIP-681 on Ethereum, a `tron:` link on TRON.
- `qr_payload` — what to encode in the QR code: the bare address on every network, with no amount in it. Many payers send from an exchange, and an exchange withdrawal form accepts an address only. If you want a "QR with amount" for wallets, offer it as a switch on Bitcoin and Litecoin and encode `payment_uri` there.
- `ttl_expires_at` — the deadline. `ttl_seconds` sets it, from 60 seconds to 30 days; without it the account setting applies (30 minutes by default). For a price in USD or another fiat currency the rate in `rate_locked` stays fixed until then.
- `confirmations` and `required_confirmations` — the confirmation counter; `confirmations` is `null` while nothing has arrived.

Keep every amount as the string the API returns: do not round it and do not pass it through a floating-point number.

## Let the payer choose the coin in your interface

For an invoice created without `coin_id`, the coin list and the choice go through two calls — the same list and the same operation as on the hosted page.

```http
GET /v1/invoices/4e2bc88c-cb79-4afc-b864-c1ba8e9d50c9/coin-options
```

```json
{
  "coin_options": [
    { "coin_id": "6f1c2a3e-8d4b-4c5a-9e7f-0a1b2c3d4e5f", "ticker": "USDT", "name": "Tether USD", "network": "tron", "network_name": "TRON",
      "decimals": 6, "confirmations": 20, "estimated_amount": "49.90", "rate_as_of": "2026-09-16T12:00:03Z",
      "estimate_reason": null, "estimate_message": null },
    { "coin_id": "a01d6e2b-3c4f-4a5b-8c7d-9e0f1a2b3c4d", "ticker": "BTC", "name": "Bitcoin", "network": "btc", "network_name": "Bitcoin",
      "decimals": 8, "confirmations": 2, "estimated_amount": null, "rate_as_of": null,
      "estimate_reason": "rate_stale",
      "estimate_message": { "key": "checkout.estimate.rate_stale", "default": "The exchange rate is updating. Choose this currency to see the exact amount." } }
  ]
}
```

- Show `network_name` next to every ticker, and `estimated_amount` with a `≈` sign: it is an estimate. When it is `null`, show the text of `estimate_message` instead; the coin can still be chosen.
- The list contains only the coins you offer for invoices that match the invoice currency, narrowed by `allowed_coin_ids` when the invoice has them; coins under maintenance are left out.
- When the coin is already chosen, the call answers `409` with `details.coin_id`, and the invoice itself already has the address.

Fix the choice from your server, with an `Idempotency-Key`:

```http
POST /v1/invoices/4e2bc88c-cb79-4afc-b864-c1ba8e9d50c9/coin HTTP/1.1
Content-Type: application/json
Idempotency-Key: order-10042-coin

{"coin_id": "6f1c2a3e-8d4b-4c5a-9e7f-0a1b2c3d4e5f"}
```

The answer is the invoice with `address`, `payer_amount`, `payment_uri`, `qr_payload` and `ttl_expires_at` — the same shape as above. The choice prices the payment again and locks the rate, so show `payer_amount` from this answer, not the estimate. Refusals: `400` with `details.field: coin_id` for a coin outside the options; `409` when a coin is already chosen (also the same one) or the invoice is no longer `new`; `503` when the rate or a new address is not available right now — wait a few seconds and send the same request again. A repeat with the same `Idempotency-Key` returns the first answer. The coin can be chosen once; if the payer has the hosted page open too, it switches to the payment at once.

## What your payment screen must show

These elements are on the hosted page; your screen needs each of them.

| Element | Field | Why |
|---|---|---|
| The network in words, with the standard for a token (TRON, TRC20) | `network`, `ticker` | The same address format and ticker exist on several networks |
| Wrong-network warning next to the address | `ticker`, `network` | "Send only USDT on the TRON network to this address. Funds sent on another network cannot be recovered." |
| The address, with a copy button | `address` | The payer copies it into a wallet; funds sent to a mistyped address cannot be recovered |
| The exact amount, with a copy button | `payer_amount`, `ticker` | A transfer below the amount becomes an underpayment |
| The QR code, with the amount printed next to it | `qr_payload` | The QR carries only the address on every network |
| An "open in wallet" link | `payment_uri` | One tap on a phone with a wallet installed |
| The time left | `ttl_expires_at` | After it the invoice expires, and a later transfer is a late payment |
| The status, with confirmations while it waits | `status`, `confirmations`, `required_confirmations` | "Payment detected, 3 of 20 confirmations" tells the payer not to send twice |
| The outcome screen | `status`, `paid_amount`, `remaining_amount` | Paid, underpaid with the remaining amount, expired or cancelled, each with the next step |

Count the timer down against your server's clock rather than the payer's device. When it reaches zero, hide the address and the QR and offer to create a new payment. The statuses are `new`, `pending`, `paid`, `underpaid`, `expired` and `cancelled`; [confirmations](https://cryptolike.net/glossary/confirmations) explains why a transfer waits before it counts.

## Underpayment, overpayment and late payment

- **Underpayment.** A confirmed transfer below `payer_amount` sets the status to `underpaid` and sends `invoice.underpaid`; a shortfall within the underpayment tolerance of your account still counts as `paid`. The invoice stays open for a top-up to the same address until `ttl_expires_at`: show `remaining_amount` and the updated `payment_uri`, which now asks for the remainder. After the deadline the underpayment is final — tell the payer to contact you. More: [underpaid payment](https://cryptolike.net/glossary/underpaid-payment).
- **Overpayment.** The status is `paid` with `overpaid_amount` above zero; `invoice.paid` carries it and is sent again when another transfer raises it. The whole sum is credited to your balance; settle the difference with the customer.
- **Late payment.** A transfer confirmed after the invoice closed (`expired`, `cancelled` or a final `underpaid`) is still credited to your balance: the invoice gets `late_payment: true`, you receive `invoice.late_payment`, and the status does not change. This also happens when a transfer was seen but not yet confirmed at the deadline: the invoice expires, and the transfer arrives as a late payment. Decide whether to fulfil the order or refund. More: [expired payment](https://cryptolike.net/glossary/expired-payment).
- **Reversal.** If a network reorganisation removes a confirmed transfer, you receive `invoice.reverted` with the recomputed amounts. Stop fulfilment until a new `invoice.paid` arrives.

## Confirm the order only on the webhook

Fulfil an order when your server receives `invoice.paid` with a valid [webhook signature](https://cryptolike.net/glossary/webhook-signature) — not when the payer presses "I have paid", and not on a status your page has seen. Delivery is at least once: store `event_id`, answer `2xx` within 10 seconds and skip repeats. Webhooks are the same for H2H, the hosted page and the widget, and they do not carry `payment_uri`, `qr_payload` or the confirmation counter — read `GET /v1/invoices/{id}` when you need them. Event types, retries and a test vector are in the [webhooks reference](https://cryptolike.net/docs/webhooks); step by step: [verify crypto payment webhooks](https://cryptolike.net/guides/verify-crypto-payment-webhooks).

Before you go live, run one payment through every case on a test network: paid, underpaid then topped up, expired, and a late transfer. The test environment is described in the [developer guide](https://cryptolike.net/docs#sandbox).

## FAQ

### Do I need the widget for H2H?

No. With H2H your interface draws the screen from the invoice fields, and nothing from CryptoLike loads in the payer's browser. The widget is the other option: the CryptoLike payment page inside a frame on your checkout.

### Can I still send the payer to the hosted page?

Yes. Every invoice has `checkout_url`, and the hosted page shows the same invoice. A coin chosen through the API switches an open page to the payment, so you can offer the page as a fallback, for example to a payer without a wallet on the same device.

### Does H2H change the webhooks?

No. The events, the signature and the retries are the same as with the hosted page. The payment block of the invoice — `payment_uri`, `qr_payload`, `confirmations` and `required_confirmations` — is not in the webhook body; read the invoice with `GET /v1/invoices/{id}`.

### Which amount does the payer send when the payer pays the fee?

`payer_amount`. With `fee_paid_by: payer` it already includes the platform fee, and `amount_crypto` is what you receive. Show `payer_amount` and keep it as the string the API returns.
