Getting started

Cryptunnel is a non-custodial crypto payment gateway: your buyers pay in crypto straight to your own wallets. Cryptunnel never holds your funds - it detects the on-chain transfer, confirms the payment and notifies your system.

This page takes you from nothing to a confirmed sandbox payment. Budget thirty minutes. Every step that touches the API is shown as a plain HTTP call first - integrate from any language, tool or agent - with the Python, Node and PHP SDK forms beside it.

1. Get access

Sign up at contragent.cryptunnel.io and create a merchant (Merchants -> Create). A merchant is one shop or project; you can have several under one account.

2. Attach a wallet

Open your merchant's Wallets page, search for a currency and paste your receiving address. Every payment in that currency goes directly to this address.

For this walkthrough pick a Test network currency - TRX on Tron Nile is the quickest, its faucet asks for nothing. Sandbox and faucets has the details. A mainnet wallet works the same way when you go live.

3. Get an API key

On the merchant page, generate an API key. It is shown once - copy it. From here on every API call carries two headers:

x-merchant-id: <your merchant id>
x-api-key: <your API key>

The merchant id is on the same page. There is also an optional request-signing mode for high-security setups - see Authentication - but the key is all you need.

4. Check the credentials

One call that either works or tells you what is wrong:

curl https://api.cryptunnel.io/v1/merchants \
  -H 'x-merchant-id: <merchant id>' \
  -H 'x-api-key: <api key>'
from cryptunnel import CryptunnelSync

cryptunnel = CryptunnelSync("<merchant id>", "<api key>", sandbox=True)
print(cryptunnel.get_merchant())
import { Cryptunnel } from 'cryptunnel'

const cryptunnel = new Cryptunnel({ merchantId: '<merchant id>', apiKey: '<api key>', sandbox: true })
console.log(await cryptunnel.getMerchant())
use Cryptunnel\Cryptunnel;

$cryptunnel = new Cryptunnel('<merchant id>', '<api key>', sandbox: true);
print_r($cryptunnel->getMerchant());
{ "id": "540a0d3a-5506-4fc7-bffa-985013e835a9", "name": "My shop", "balance": "0", "created_at": "2026-09-01T10:00:00.000Z" }

A 401 means the merchant id or the key is wrong, the key was rotated, or the merchant is suspended - nothing else. Fix that before going further; every later call authenticates the same way.

5. Create a payment

Create a sandbox payment and get the checkout URL to send the buyer to. is_test: true (or the SDK sandbox switch) keeps it on test networks; external_id is your own order id, echoed back in every response and webhook.

curl -X POST https://api.cryptunnel.io/v1/payments/widget \
  -H 'content-type: application/json' \
  -H 'x-merchant-id: <merchant id>' \
  -H 'x-api-key: <api key>' \
  -d '{
    "amount": 10,
    "currency": "USD",
    "external_id": "order-1042",
    "callback_url": "https://shop.example.com/cryptunnel",
    "is_test": true
  }'
payment = cryptunnel.create_widget_payment(
    10, "USD", "order-1042",
    callback_url="https://shop.example.com/cryptunnel",
)
print(payment["url"])
const payment = await cryptunnel.createWidgetPayment({
  amount: 10,
  currency: 'USD',
  externalId: 'order-1042',
  callbackUrl: 'https://shop.example.com/cryptunnel',
})
console.log(payment.url)
$payment = $cryptunnel->createWidgetPayment(
    amount: 10,
    currency: 'USD',
    externalId: 'order-1042',
    callbackUrl: 'https://shop.example.com/cryptunnel',
);
echo $payment['url'];
{
  "id": "n9cdFaTccYbXecVekHKW8Q",
  "external_id": "order-1042",
  "status": "created",
  "created_at": "2026-09-29T12:00:00.000Z",
  "is_test": true,
  "url": "https://pay.cryptunnel.io/n9cdFaTccYbXecVekHKW8Q"
}

Keep the id. Creation is idempotent on external_id: if the call times out and you retry, you get this same payment back instead of a second one.

6. Pay it

Open the url. The page lists the testnet currencies your merchant can receive - pick the one you attached in step 2, send the displayed amount from your testnet wallet before the countdown ends, and watch the status turn confirmed. That is the same on-chain detection production uses.

If the list is empty, the wallet from step 2 is not attached or not a testnet one.

7. Receive the webhook

When the payment reaches a final status Cryptunnel POSTs to your callback_url. This is what arrives:

{
  "status": "confirmed",
  "external_id": "order-1042",
  "id": "n9cdFaTccYbXecVekHKW8Q",
  "original_amount": "10",
  "original_currency": "USD",
  "amount": "10.0031",
  "currency": "USDT",
  "terminal_id": "DEPRECATED",
  "merchant_id": "540a0d3a-5506-4fc7-bffa-985013e835a9"
}

It is signed: x-webhook-signature is an HMAC-SHA256 over "<x-webhook-timestamp>.<raw body>" keyed with your webhook secret from the merchant page. Verify it over the exact bytes you received - re-serialised JSON will not match - and only then act on status.

# the raw body is in body.json, the two headers in $TIMESTAMP and $SIGNATURE
{ printf '%s.' "$TIMESTAMP"; cat body.json; } | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //'
# compare the output with $SIGNATURE
from cryptunnel import verify_webhook

if not verify_webhook(WEBHOOK_SECRET, request.headers, request.get_data()):
    return "", 401
import { verifyWebhook } from 'cryptunnel'

// with express.raw({ type: 'application/json' }) in front of the route
if (!verifyWebhook(WEBHOOK_SECRET, req.headers, req.body)) return res.sendStatus(401)
use Cryptunnel\Webhook;

if (!Webhook::verify(WEBHOOK_SECRET, $_SERVER, file_get_contents('php://input'))) {
    http_response_code(401);
    exit;
}

Answer 2xx once you have handled it; anything else is retried once a minute for an hour. The whole contract - every field, retries, deduplication, raw body per framework - is on Webhooks.

No public URL yet? Skip the webhook for the walkthrough and read the status with GET /v1/payments/{id} instead - but wire the webhook before going live; it is the only path that survives a buyer closing the page.

8. Go live

Attach a mainnet wallet, drop is_test (or build the SDK client without sandbox), and the same calls take real payments. Nothing else changes - no second host, no second key.

No code at all?

Two ways that need no developer:

  • Payments page -> Create Payment: a one-time invoice with a checkout URL to send to your buyer.
  • Payment Links page -> Create Link: a reusable price-tag URL for a chat, a bot scenario or a QR code. See Payment links.

Next steps


Did this page help you?