К содержимому

PHP

PHP 7.4 – 8.x

Настроить в кабинетеКлючи, URL вебхуков и загрузки для этой платформы — в кабинете, раздел «Интеграции».

Эта инструкция на английском.

cryptolike/sdk (PHP)

Minimal server-side SDK for the CryptoLike merchant API. PHP 7.4 – 8.x, no runtime dependencies, works with or without Composer. HTTP through ext-curl; without it, through PHP's https:// stream wrapper (allow_url_fopen + ext-openssl); or any PSR-18 client.

Pre-release: Packagist has nothing under this name yet. Install from a checkout of the repository ($CL = path to the clone, see SDK.md); the command below is the one for after the public release.

bash
composer config repositories.cryptolike path "$CL/sdk/php" && composer require cryptolike/sdk:@dev

# after the release:
composer require cryptolike/sdk
  • Signed requests (X-API-Key / X-Timestamp / X-Signature, HMAC-SHA256 — API-AUTH)
  • Idempotency-Key generated for every POST (pass your own to make retries exact)
  • Retries with exponential backoff and jitter on 429 (honours Retry-After), 5xx and transport errors
  • Every endpoint of the merchant API as a method; responses are arrays shaped as in docs/openapi.yaml" target="_blank" rel="noopener noreferrer">docs/openapi.yaml; amounts are decimal strings, floats are refused
  • Webhook::verify() / Client::verifyWebhook() — signature and timestamp check of webhook deliveries (WEBHOOKS)
  • For shop plugins: createInvoiceForOrder() (one open invoice per order, platform metadata), Amount::format() (shop total → decimal string) — INTEGRATIONS

Without Composer (CMS plugins)

Copy autoload.php and src/ into your plugin (for example lib/cryptolike-sdk/) and require the one file — a PSR-4 autoloader for CryptoLike\, nothing else is loaded:

php
require __DIR__ . '/lib/cryptolike-sdk/autoload.php';
$api = new \CryptoLike\Client(['key_id' => $keyId, 'secret' => $secret, 'base_url' => $apiUrl]);

The tests load the SDK exactly this way. If several plugins on one site vendor the SDK, the first autoloader registered serves all of them — ship the same SDK version.

The secret stays on your server

The API secret signs requests as you: whoever has it can create payments, read balances and change your webhooks. Use this package from your backend only; never put the secret into JavaScript, a mobile app or anything served to a browser. Your page needs only the invoice id / checkout_url your server obtained — see WIDGET §1.

Client reads CRYPTOLIKE_API_KEY_ID and CRYPTOLIKE_API_SECRET from the environment when the options leave them empty; the secret is never logged and __debugInfo() hides it from var_dump, print_r and json_encode. PHP ignores __debugInfo() in var_export() — do not dump a client that way.

Quickstart — to the first confirmed payment

php
use CryptoLike\Client;
use CryptoLike\ApiException;

$api = new Client(); // credentials from the environment

// 1. Create a payment on your server; send the customer to checkout_url.
$invoice = $api->createInvoice(
    ['amount' => '10.50', 'currency' => 'USD', 'order_id' => 'A-1001', 'return_url' => 'https://shop.example/orders/A-1001'],
    'order-A-1001', // Idempotency-Key — a retry never creates a second payment
);
header('Location: ' . $invoice['checkout_url'], true, class="tok-num">302);

// 2. Learn the outcome from the webhook (below) — or poll:
$detail = $api->getInvoice($invoice['id']);
if ($detail['status'] === 'paid') { fulfil($detail['order_id']); }
php
// 3. Webhook receiver — verify over the RAW body, dedupe by event_id, answer 2xx fast.
use CryptoLike\Webhook;
use CryptoLike\WebhookVerificationException;

try {
    $event = Webhook::verify(getallheaders(), file_get_contents('php://input'), getenv('CRYPTOLIKE_WEBHOOK_SECRET'));
} catch (WebhookVerificationException $e) {
    http_response_code(class="tok-num">401); exit;
}
if (!alreadySeen($event['event_id'])) { handle($event); } // $event['type'] === 'invoice.paid' → fulfil
http_response_code(class="tok-num">200);

Laravel / Symfony: Webhook::verify($request->headers->all(), $request->getContent(), $secret). Full examples: examples/create-payment.php, examples/webhook.php.

Methods

Area Methods
Invoices createInvoice($params, $idempotencyKey), getInvoice, listInvoices($filters), iterateInvoices($filters), cancelInvoice
Addresses createAddress, getAddress($id, $limit, $cursor), listAddresses, iterateAddresses, archiveAddress
Transactions listTransactions, iterateTransactions, getTransaction, exportTransactionsCsv
Catalogue listBalances(), listCurrencies(), listRates($fiat)
Withdrawals estimateWithdrawal, createWithdrawal, getWithdrawal, listWithdrawals, iterateWithdrawals, cancelWithdrawal
Webhooks listWebhookEndpoints, createWebhookEndpoint, getWebhookEndpoint, updateWebhookEndpoint, deleteWebhookEndpoint, rotateWebhookSecret, listWebhookDeliveries, getWebhookDelivery, retryWebhookDelivery
Widget getEmbedOrigins(), setEmbedOrigins($origins) — checkout widget allowlist
Any endpoint request($method, $path, $query, $body, $idempotencyKey)

List filters are query parameters named as in the API (['status' => 'paid', 'from' => '…', 'limit' => 50]); lists return ['items' => …, 'next_cursor' => …]; iterate*() walks every page.

Shop plugins — one invoice per order

php
use CryptoLike\Amount;
use CryptoLike\ApiException;
use CryptoLike\Client;

$api = new Client([
    'key_id' => $settings['key_id'], 'secret' => $settings['secret'], 'base_url' => $settings['api_url'],
    'integration' => ['platform' => 'woocommerce', 'platform_version' => WC_VERSION, 'plugin_version' => '1.0.0', 'store_url' => home_url()],
]);
try {
    $invoice = $api->createInvoiceForOrder(
        (string) $order->get_id(),
        Amount::format($order->get_total(), wc_get_price_decimals()), // float/string total → "49.90", once
        $order->get_currency(),
        ['return_url' => $order->get_checkout_order_received_url(), 'description' => 'Order #' . $order->get_order_number()],
    );
    // redirect the customer to $invoice['checkout_url']
} catch (ApiException $e) {
    if ($e->reason() === 'order_has_payment') { /* the order already received funds — never charge twice */ }
    if ($e->reason() === 'order_invoice_mismatch') { /* the total changed: cancel $e->details['invoice_id'] (if new) and retry */ }
}

createInvoiceForOrder($orderId, $amount, $currency, $options = [], $metadata = []) sends idempotency_by_order_id: true: a second call for the same order — double click, reload, retry — returns the open invoice instead of creating another. The integration option goes into the invoice metadata (never shown to the payer) and the User-Agent (cryptolike-sdk-php/0.2.0 woocommerce/9.3.1 cryptolike-woocommerce/1.0.0), which is how the platform counts payments per CMS. Amount::format($value, $decimals) converts the shop's total exactly once (strings and ints exactly; a float rounded to the currency's decimals; more non-zero decimals than allowed are refused); Amount::compare('10.50', $invoice['paid_amount']) compares without floats. The full plugin contract — status mapping, webhook handling, retries, test mode — is docs/INTEGRATIONS.md.

Errors

A non-2xx response throws ApiException ($status, $apiCode, getMessage(), $details, field(), reason(), $retryAfter, $requestId); a transport failure that survived the retries throws NetworkException. Codes: API-AUTH §5, API §5.

Retries and idempotency

429 (after Retry-After, or backoff), 5xx except 501, and connection errors are retried up to max_retries (default 3) with min(retry_max, retry_base · 2^attempt) full-jitter backoff. Every POST carries an Idempotency-Key (yours, or a UUID generated once per call and reused on each retry), so a retry never creates a second invoice, address or withdrawal. Each attempt is signed with a fresh timestamp. Redirects are never followed. A 401 auth.replay — two identical requests signed within the same second share a signature — waits for the next second and signs afresh.

Options

php
new Client([
    'key_id' => …, 'secret' => …,            // default: CRYPTOLIKE_API_KEY_ID / CRYPTOLIKE_API_SECRET
    'base_url' => …,                          // default: CRYPTOLIKE_API_URL or https://api.cryptolike.net
    'transport' => new Psr18Transport($client, $requestFactory, $streamFactory), // default: CurlTransport, else StreamTransport
    'timeout' => class="tok-num">30.0, 'max_retries' => class="tok-num">3, 'retry_base' => class="tok-num">0.5, 'retry_max' => class="tok-num">8.0, 'retry_after_max' => class="tok-num">30.0,
    'api_version' => 'v1',                    // X-API-Version pin
    'integration' => ['platform' => 'opencart', 'platform_version' => '4.0.2.3', 'plugin_version' => '1.0.0', 'store_url' => 'https://shop.example'],
    'app_info' => 'my-shop/2.3',              // appended to User-Agent: cryptolike-sdk-php/<version> …
]);

Transports: CurlTransport (default when ext-curl is loaded), StreamTransport (the fallback: allow_url_fopen = On, ext-openssl for https; certificates verified, redirects not followed; a clear NetworkException when neither is available), Psr18Transport (Guzzle, Symfony HttpClient — needs psr/http-client + psr/http-factory).

Webhook verification

Webhook::verify(array $headers, string $rawBody, string $secret, ?int $tolerance = 300, ?int $now = null) (the same as Client::verifyWebhook($headers, $rawBody, $secret, $tolerance); Webhook::verifyRequest($secret) reads php://input and the headers of the current request) returns the decoded event (event_id, type, created_at, data) or throws WebhookVerificationException with $reason (missing_header | invalid_timestamp | timestamp_out_of_tolerance | invalid_signature | invalid_body). $rawBody must be the bytes as received. Answer 401/400 on failure, 2xx on success, and deduplicate by event_id — deliveries are at least once and may arrive out of order.

Development

bash
composer lint     # php -l on every file
composer test     # tests/run.php (vectors of docs/sdk/vectors.json + client behaviour) and
                  # tests/http.php (cURL + stream transports against `php -S`, signature checked server-side)
# PHP 7.4 and 8.3 in docker, no PHP on the host (what CI runs):
tests/docker.sh   # or `make sdk-php-test` from the repository root; PHP_VERSIONS="8.1 8.4" to widen