Skip to content

For developersGuide

How to accept crypto payments in PHP

Accept Bitcoin and USDT on a PHP site: sign an API request with hash_hmac, create a payment, send the customer to the payment page and verify the webhook.

A PHP site accepts crypto with CryptoLike in three pieces of code: a function that signs API requests with hash_hmac, a call that creates the payment for an order, and a webhook handler that marks the order paid. No extension beyond cURL is needed, and the same code runs on PHP 7.4 and 8.x, in Laravel, Symfony or plain PHP.

This guide is for a custom site or application. A shop on WooCommerce, OpenCart, PrestaShop or Magento 2 needs no code: install the WooCommerce plugin or the module for your platform instead.

What you need

A CryptoLike account, a PHP server that can make outgoing HTTPS requests, and a public https:// URL for the webhook. Keep the API secret and the webhook secret in environment variables — the examples read CRYPTOLIKE_API_KEY_ID, CRYPTOLIKE_API_SECRET and CRYPTOLIKE_WEBHOOK_SECRET, plus CRYPTOLIKE_API_URL for the test API URL (unset means production). Test everything first with test keys and test networks, so no real money moves.

1. Create a test API key

In the cabinet open Developers → API keys → Create key and give the key create_invoice and read. Keys that start with ck_test_ work with test networks. Test keys come from the test environment: CryptoLike support gives you access to its cabinet, where you create the same key, and its API URL (CRYPTOLIKE_API_URL in the examples). The production cabinet issues live keys only, and on mainnet a test key is refused with 401 auth.test_key; switch to a live key only when the flow works end to end. The secret is shown once: put it straight into your server's environment, never into a repository or a page.

2. Sign API requests with hash_hmac

Every request carries three headers: the key ID, the Unix time in seconds and an HMAC-SHA256 signature of four lines — the timestamp, the method, the path with the query, and the SHA-256 of the body. This function does it with cURL:

php
function cryptolike(string $method, string $path, ?array $payload = null, ?string $idempotencyKey = null): array
{
    $body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES);
    $ts = (string) time();
    $canonical = implode("\n", [$ts, strtoupper($method), $path, hash('sha256', $body)]);
    $headers = [
        'X-API-Key: ' . getenv('CRYPTOLIKE_API_KEY_ID'),
        'X-Timestamp: ' . $ts,
        'X-Signature: ' . hash_hmac('sha256', $canonical, getenv('CRYPTOLIKE_API_SECRET')),
        'Content-Type: application/json',
    ];
    if ($idempotencyKey !== null) {
        $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    }
    $ch = curl_init((getenv('CRYPTOLIKE_API_URL') ?: 'https://api.cryptolike.net') . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => $body === '' ? null : $body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
    ]);
    $response = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $data = json_decode((string) $response, true) ?? [];
    if ($status >= 400) {
        throw new RuntimeException(($data['error']['code'] ?? 'http_' . $status) . ': ' . ($data['error']['message'] ?? ''));
    }
    return $data;
}

The body you hash must be the exact string you send, which is why the function encodes it once. Check your signing against the reference vector in API authentication before the first real call: a 401 auth.invalid_signature almost always means a different body string or the time in milliseconds.

3. Create the payment for an order

Create one payment per order from the server. Price it in your currency and let the customer choose the coin, or pass a coin_id from GET /v1/currencies to fix it. Without coin_id the payment page lists the coins you offer for invoices, each with an approximate amount marked ≈; to offer only some of them for this order, add allowed_coin_ids with ids that have invoice_enabled: true in the signed GET /v1/currencies. The order ID as the idempotency key makes a double click or a retry return the same payment:

php
$invoice = cryptolike('POST', '/v1/invoices', [
    'amount' => '49.90',
    'currency' => 'USD',
    'order_id' => $order->id,
    'idempotency_by_order_id' => true,
    'return_url' => 'https://example.com/orders/' . $order->id,
], 'order-' . $order->id);

$order->saveInvoiceId($invoice['id']);

Amounts are decimal strings, never floats: '49.90', not 49.9. The response carries id, status (new) and checkout_url.

4. Send the customer to the payment page

Redirect to checkout_url: the page shows the amount, the network, the address with a QR code, the time left and the live status, and returns the customer to return_url after the payment. To keep the customer on your site, embed the same page with the checkout widget. Your page only ever receives the invoice ID; the API secret stays on the server.

php
header('Location: ' . $invoice['checkout_url'], true, 303);
exit;

5. Verify the webhook and mark the order paid

Add an endpoint in the cabinet under Developers → Webhooks with the invoice.* events and store its secret. The handler verifies the signature over the raw body, rejects old timestamps, skips events it has already processed and answers 200 quickly:

php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$secret = (string) getenv('CRYPTOLIKE_WEBHOOK_SECRET');
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);

if ($secret === '' || !ctype_digit($ts) || abs(time() - (int) $ts) > 300 || !hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
if (!alreadyProcessed($event['event_id'])) {
    if ($event['type'] === 'invoice.paid') {
        markOrderPaid($event['data']['invoice']['order_id']);
    }
    rememberEvent($event['event_id']);
}
http_response_code(200);

Read the body with php://input, not $_POST, and do not decode and re-encode it before the check. Deliveries are at least once and can arrive out of order, so trust the status inside data, not the arrival order. More detail and the test vector: verify crypto payment webhooks.

6. Test on a test network, then go live

Create a payment with the test key, pay it with test coins on the network shown on the page, and watch invoice.pending and then invoice.paid arrive. Then handle the other outcomes: invoice.underpaid (the customer can top up to the same address until the deadline), invoice.expired (offer a new payment) and invoice.reverted (stop fulfilment). Replace the test key with a live one and run the go-live checklist in the developer guide.

Common errors and fixes

Error What happened What to do
401 auth.invalid_signature The signed string differs from the request Hash the exact body you send; sign the path with its query; use seconds, not milliseconds
401 auth.expired_timestamp The server clock is more than 5 minutes off Sync the clock with NTP
403 auth.permission_denied The key lacks a permission Add create_invoice or read to the key in the cabinet
409 conflict The order already has a payment for another amount, or has received money Read details.invoice_id; do not create a second payment
422 idempotency.mismatch The same Idempotency-Key came with a different body Use one key per order and per request
422 no_allowed_coins None of allowed_coin_ids is offered for invoices in this currency Send other ids, or leave the field out to offer every coin

Prefer an SDK?

The official PHP SDK wraps the same calls for PHP 7.4 – 8.x with no runtime dependencies: signed requests, retries on 429 and 5xx, one open invoice per order and Webhook::verify(). The code in this guide is what it does under the hood, so you can start with either.

FAQ

Do I need Composer or a library to accept crypto in PHP?

No. Signing a CryptoLike request takes hash() and hash_hmac() from PHP's standard library, and the requests go through cURL. The function in step 2 is enough for a whole integration. The official PHP SDK adds retries, pagination and helpers for shops, and it also works without Composer by copying its files into your project.

Can I show the payment page on my own site?

Yes. Create the payment on your server as in step 3, then embed the same payment page with the checkout widget instead of redirecting. Add your site's address to the allowed origins first: the Allowed origins card on the cabinet's Settings page. The widget reads only public payment data, so the API secret never reaches the browser.

How do I know which order a webhook belongs to?

The invoice object in the webhook carries the order_id you sent when you created the payment, and metadata if you sent it. Look the order up by that value, check the status inside data, and store the event_id so a repeated delivery of the same event is acknowledged without changing the order twice.

Terms used here

Read next

Start with a test payment

Create an account, ask support for the test environment and see a payment go from pending to paid on a test network.