CryptoLike SDKs
Official server-side client libraries for the merchant API — one per language, the same
behaviour in each: signed requests, an Idempotency-Key on every POST, retries with backoff,
typed resources and webhook verification. The API itself is described in API overview; this page
gets you from an empty project to the first confirmed payment in each language.
Pre-release: nothing is published to a registry yet. npm, PyPI, Packagist and the Go module proxy all answer
404for these names today — the commands in the Install column are the ones you will run after the public release. Until then each SDK installs from a checkout of this repository in one or two commands: Installing from the repository (pre-release). Nothing else on this page changes: the same code, the same environment variables, the same vectors.
| Language | Package | Install (after the release) | Requires | Source |
|---|---|---|---|---|
| TypeScript / Node.js | @cryptolike/sdk |
npm install @cryptolike/sdk |
Node ≥ 18 | sdk/typescript |
| Python | cryptolike |
pip install cryptolike |
Python ≥ 3.10, requests |
sdk/python |
| Go | github.com/Petya88/Cryptolike/sdk/go |
go get github.com/Petya88/Cryptolike/sdk/go |
Go ≥ 1.23, stdlib only | sdk/go |
| PHP | cryptolike/sdk |
composer require cryptolike/sdk |
PHP 7.4 – 8.x, no runtime dependencies; ext-curl, the stream wrapper or PSR-18; Composer optional (autoload.php) |
sdk/php |
Every SDK sends User-Agent: cryptolike-sdk-<language>/<version>, pins X-API-Version: v1,
and passes the same reference vectors (vectors.json, generated from the platform's
own signer) in its test suite.
The secret stays on your server — read this first
Your API secret never goes to a browser or a mobile app. It signs requests as you: whoever can read your page's JavaScript could create payments, read balances and change your webhooks.
- Your server holds the key id and secret (environment variable or secret manager — every
SDK reads
CRYPTOLIKE_API_KEY_ID/CRYPTOLIKE_API_SECRETby default) and creates payments. - Your page receives only the invoice id or
checkout_urland sends the customer there — or embeds the payment page (Checkout widget), which needs nothing but the id. - The TypeScript SDK throws
BrowserSecretErrorwhen loaded in a browser. - Webhook secrets are server-side too:
verifyWebhookruns where the delivery arrives. - No SDK logs the secret itself: none of them writes to stdout/stderr and none has a debug or
verbose mode. Each one also redacts the client object, but only on the dump paths below —
those are the ones a logger, an error reporter or a
console.logactually takes. Anything not listed as covered prints the fields as they are, so treat a client like any other credential holder and never hand it to a generic object dumper.
| SDK | Redacted (verified) | Still prints the secret |
|---|---|---|
| TypeScript | console.log(client), util.inspect (any options, incl. showHidden / customInspect: false), console.dir, %o/%j in a logger, JSON.stringify, spread / Object.entries — the secret is a #private field and the class has both toJSON() and a nodejs.util.inspect.custom hook |
— |
| Python | repr(client), str(client), f-strings, print(client) |
vars(client) / client.__dict__ — _secret is a plain attribute |
| Go | fmt.Print, %v, %+v, %s, %q, log.Print — through String() |
%#v (fmt.Sprintf("%#v", c)), which prints the struct literal |
| PHP | var_dump, print_r, json_encode — through __debugInfo(); serialize() refuses (closures) |
var_export($client, true), where PHP ignores __debugInfo() |
What every SDK does for you
| Concern | Behaviour (identical across languages) | Contract |
|---|---|---|
| Signing | X-API-Key, X-Timestamp (fresh per attempt), X-Signature = HMAC-SHA256 over timestamp\nMETHOD\npath?query\nsha256(body) |
Authentication §3 |
| Idempotency | Every POST carries Idempotency-Key: yours (e.g. your order id) or a UUID generated once per call and reused on each retry — a retry never creates a second invoice, address or withdrawal |
Authentication §7 |
| Retries | 429 (after Retry-After, or backoff), 5xx except 501, connection/timeout errors — up to 3 retries, min(8 s, 0.5 s · 2^attempt) with full jitter; a Retry-After above 30 s stops retrying |
Authentication §6 |
| Same-second replay | X-Timestamp is whole seconds, so no two attempts of one call are ever signed in the same second: before re-signing, the client waits out the second the previous attempt used. A retry after a genuine 5xx therefore comes back as the real error, never as 401 auth.replay. If the server still reports auth.replay (a truly concurrent identical request), the client waits for the next second, signs afresh and retries once — counted against the retry budget |
Authentication §5 |
| Errors | One error type carrying status, code (stable machine code), message, details, field, retry_after, request_id; 4xx is never retried |
API overview §5 |
| Amounts | Decimal strings ("10.50") — Python exposes Decimal; floats are refused everywhere |
API overview §6 |
| Pagination | list returns {items, next_cursor}; iterate / Each walks every page |
API overview §6 |
| Webhooks | verifyWebhook(headers, rawBody, secret, {tolerance: 300}): sha256= prefix, constant-time compare, timestamp within tolerance, then the parsed event. The options argument is idiomatic per language — TypeScript an object, Python a tolerance= keyword, Go *VerifyOptions{Tolerance: time.Duration} (nil = defaults), PHP the seconds themselves (Webhook::verify($headers, $body, $secret, ?int $tolerance = 300)); the default tolerance is 300 s in all four |
Webhooks §3 |
| Version | X-API-Version: v1 on every request; only additive changes within v1 |
API changelog |
Resources covered: invoices (create / get / list / cancel / export), static addresses (with the
archive: archiveStaticAddress / restoreStaticAddress / listArchivedStaticAddresses, below),
transactions, balances, currencies, rates, withdrawals (+ estimate), webhook endpoints and
deliveries, and the checkout-widget allowlist (/v1/embed). Anything newer than your SDK version
is reachable through the generic request method with the same signing and retries.
Static-address archive (all languages)
An archived address is no longer offered and leaves real-time scanning; it is never deleted and
never issued again. The platform checks archived addresses for incoming transfers regularly (every
24 h by default): a transfer found is credited as usual and restores the address automatically.
Addresses with no incoming transfers for 180 days (platform setting) are archived automatically,
after a warning 7 days earlier. Restoring is refused with 409 conflict
(details.active_address_id) while the customer has another active address for the currency —
archive that one first.
| TypeScript | Python | Go | PHP | |
|---|---|---|---|---|
| Archive | api.archiveStaticAddress(id) |
api.archive_static_address(id) |
api.ArchiveStaticAddress(ctx, id, key) |
$api->archiveStaticAddress($id) |
| Restore | api.restoreStaticAddress(id) |
api.restore_static_address(id) |
api.RestoreStaticAddress(ctx, id, key) |
$api->restoreStaticAddress($id) |
Archive list (status=archived forced; search, coin_id, customer_id) |
listArchivedStaticAddresses(q), iterateArchivedStaticAddresses(q) |
list_archived_static_addresses(**f), iterate_archived_static_addresses(**f) |
ListArchivedStaticAddresses(ctx, q), EachArchivedStaticAddress(ctx, q, fn) |
listArchivedStaticAddresses($f), iterateArchivedStaticAddresses($f) |
Archive webhooks (static_address.archived / .restored / .archive_warning) |
isWebhookEvent(event, StaticAddressEvent.Restored) → typed event.data |
event.typed_data() → StaticAddress*Data |
event.DecodeData(&cryptolike.StaticAddressRestoredData{}) |
Webhook::STATIC_ADDRESS_* + documented array shape |
| Example | examples/static-address-archive.ts |
examples/static_address_archive.py |
examples/static-address-archive |
examples/static-address-archive.php |
They are the same signed calls as addresses.archive / addresses.restore / addresses.list
(POST /v1/addresses/{id}/archive|restore, GET /v1/addresses?status=archived); the webhook
data types follow StaticAddress*Data in OpenAPI spec (Webhooks §5).
Before you start (all languages)
- Create a key in the cabinet: API keys → Create key with
create_invoiceandread. Copy the secret — it is shown once. Start with ack_test_…key: test keys work with test networks and are created in the cabinet of the test environment (CryptoLike support gives you access and its API URL). - Create a webhook endpoint (cabinet → Webhooks, or
webhooks.endpoints.createbelow) forinvoice.paid(or*); store its secret next to the API secret. - Export the three values on your server:
export CRYPTOLIKE_API_KEY_ID=ck_test_…
export CRYPTOLIKE_API_SECRET=…
export CRYPTOLIKE_WEBHOOK_SECRET=whsec_…
# Where the API lives. Defaults to https://api.cryptolike.net — set it whenever your key is not
# a production key: a `ck_test_…` key only works with test networks, never with the production API.
export CRYPTOLIKE_API_URL=… # with a ck_test_ key: the test API URLA payment is priced in fiat (amount + currency) and the payer chooses the coin and network
on the checkout page (each option with an informative ≈ amount; the exact amount and rate are
fixed when the payer picks); pass coin_id (from currencies.list) only when you want to fix that
choice up front, or allowed_coin_ids to narrow it for one payment — ids of coins with
invoice_enabled (signed currencies.list, cabinet → Coins → «For invoices»); omitted = every coin
you offer for invoices; none of them usable → 422 no_allowed_coins; an empty list is refused by
the SDK before the request, so a filter that found nothing never turns into "every coin". Each
SDK README has a "Payer chooses the coin" section. The rest of the flow is the same everywhere: create the payment → send the customer to checkout_url →
receive invoice.paid on your webhook (verify, deduplicate by event_id, fulfil) — or poll
invoices.get until the status is final (paid, expired, cancelled).
Installing from the repository (pre-release)
Until the packages are published, every SDK is installed from a checkout. Clone the repository once
(access is granted per integrator while it is private) — $CL below is the path to that clone:
CL=~/src/cryptolike # anywhere you like
git clone git@github.com:Petya88/Cryptolike.git "$CL"Then, in your project:
# TypeScript / Node.js — build a tarball once, install it like any other package
(cd "$CL/sdk/typescript" && npm ci && npm run pack) # → pack/cryptolike-sdk-0.1.0.tgz
npm install "$CL/sdk/typescript/pack/cryptolike-sdk-0.1.0.tgz"
# Python (into your virtualenv)
pip install "$CL/sdk/python"
# Go — the module is not on the proxy, so resolve it from disk
go mod edit -replace github.com/Petya88/Cryptolike/sdk/go="$CL/sdk/go"
go mod tidy
# PHP — a path repository next to Packagist (run where your composer.json is)
composer config repositories.cryptolike path "$CL/sdk/php"
composer require cryptolike/sdk:@devEvery import, class name and snippet below is the same as after the release; switching over later
means replacing the tarball with @cryptolike/sdk, dropping the replace line, or removing the
path repository. All four recipes are verified against an empty project; the installed SDK is the
same build the test suites and the reference vectors run against.
TypeScript / Node.js
# pre-release (see above); after the release: npm install @cryptolike/sdk
npm install "$CL/sdk/typescript/pack/cryptolike-sdk-0.1.0.tgz"import { CryptoLike, ApiError } from "@cryptolike/sdk";
const api = new CryptoLike(); // credentials from the environment
// 1. Create the payment on your server
const invoice = await api.invoices.create(
{ amount: "10.50", currency: "USD", order_id: "A-1001", return_url: "https://shop.example/orders/A-1001" },
"order-A-1001", // Idempotency-Key
);
// 2. Send the customer there
res.redirect(invoice.checkout_url);// 3. Webhook (Express) — RAW body, verify, dedupe, 2xx fast
import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@cryptolike/sdk";
app.post("/hooks/cryptolike", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = verifyWebhook(req.headers, req.body, process.env.CRYPTOLIKE_WEBHOOK_SECRET!);
if (event.type === "invoice.paid" && !seen(event.event_id)) fulfil(event.data.invoice);
res.sendStatus(200);
} catch (e) {
if (e instanceof WebhookVerificationError) return res.sendStatus(401);
throw e;
}
});// or poll
const detail = await api.invoices.get(invoice.id);
if (detail.status === "paid") fulfil(detail);Errors: catch (e) { if (e instanceof ApiError && e.code === "validation_error") … }.
Full README and examples: sdk/typescript.
Python
# pre-release (see above); after the release: pip install cryptolike
pip install "$CL/sdk/python"from cryptolike import Client, ApiError
api = Client() # credentials from the environment
# 1. Create the payment on your server
invoice = api.invoices.create(
amount="10.50", currency="USD", order_id="A-1001",
return_url="https://shop.example/orders/A-1001",
idempotency_key="order-A-1001",
)
# 2. Send the customer there
return redirect(invoice.checkout_url)# 3. Webhook (Flask) — RAW body, verify, dedupe, 2xx fast
from flask import request, abort
from cryptolike import verify_webhook, WebhookVerificationError
@app.post("/hooks/cryptolike")
def hook():
try:
event = verify_webhook(request.headers, request.get_data(), os.environ["CRYPTOLIKE_WEBHOOK_SECRET"])
except WebhookVerificationError:
abort(401)
if event.type == "invoice.paid" and not seen(event.event_id):
fulfil(event.data["invoice"])
return "", 200# or poll
detail = api.invoices.get(invoice.id)
if detail.status == "paid":
fulfil(detail)Amounts come back as decimal.Decimal; errors are ApiError (.status, .code, .field).
Full README and examples: sdk/python.
Go
# pre-release (see above); after the release: go get github.com/Petya88/Cryptolike/sdk/go
go mod edit -replace github.com/Petya88/Cryptolike/sdk/go="$CL/sdk/go" && go mod tidyimport cryptolike "github.com/Petya88/Cryptolike/sdk/go"
api, err := cryptolike.New(cryptolike.Options{}) // credentials from the environment
// 1. Create the payment on your server
inv, err := api.Invoices.Create(ctx, cryptolike.CreateInvoiceRequest{
Amount: "10.50", Currency: "USD", OrderID: "A-1001", ReturnURL: "https://shop.example/orders/A-1001",
}, "order-A-1001") // Idempotency-Key
// 2. Send the customer there
http.Redirect(w, r, inv.CheckoutURL, http.StatusFound)// 3. Webhook — RAW body, verify, dedupe, 2xx fast
http.HandleFunc("POST /hooks/cryptolike", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
event, err := cryptolike.VerifyWebhook(r.Header, body, os.Getenv("CRYPTOLIKE_WEBHOOK_SECRET"), nil)
if err != nil { w.WriteHeader(http.StatusUnauthorized); return }
if event.Type == "invoice.paid" && !seen(event.EventID) {
var data struct{ Invoice cryptolike.Invoice `json:"invoice"` }
_ = json.Unmarshal(event.Data, &data)
fulfil(data.Invoice)
}
w.WriteHeader(http.StatusOK)
})// or poll
detail, err := api.Invoices.Get(ctx, inv.ID)
if err == nil && detail.Status == "paid" { fulfil(*detail) }Errors: var apiErr *cryptolike.APIError; if errors.As(err, &apiErr) { apiErr.Code … }.
Full README and examples: sdk/go.
PHP
# pre-release (see above); after the release: composer require cryptolike/sdk
composer config repositories.cryptolike path "$CL/sdk/php" && composer require cryptolike/sdk:@devuse CryptoLike\Client;
$api = new Client(); // credentials from the environment
// 1. Create the payment on your server
$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
);
// 2. Send the customer there
header('Location: ' . $invoice['checkout_url'], true, 302);// 3. Webhook — RAW body, verify, dedupe, 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(401); exit;
}
if ($event['type'] === 'invoice.paid' && !seen($event['event_id'])) { fulfil($event['data']['invoice']); }
http_response_code(200);// or poll
$detail = $api->getInvoice($invoice['id']);
if ($detail['status'] === 'paid') { fulfil($detail); }Laravel / Symfony: Webhook::verify($request->headers->all(), $request->getContent(), $secret).
Errors: catch (CryptoLike\ApiException $e) { $e->status, $e->apiCode, $e->field(), $e->reason() }.
Without Composer (a CMS plugin): copy autoload.php + src/ and require 'autoload.php'. Shop
plugins create the payment with $api->createInvoiceForOrder($orderId, Amount::format($total, 2), $currency)
— one open invoice per order (integration guides). Runs on PHP 7.4 – 8.x; tests on 7.4 and 8.3.
Full README and examples: sdk/php.
Checklist for the first payment
- [ ] Key created with
create_invoice+read; secret in your server's environment only. - [ ] Webhook endpoint created (
https://, public host) with its secret next to the API secret. - [ ] Payment created with your order id as
Idempotency-Key; customer sent tocheckout_url. - [ ] Webhook verified over the raw body,
event_idstored,invoice.paidfulfils the order. - [ ]
expired/cancelledhandled (show "Payment expired. Create a new payment."). - [ ] Clock synced (NTP): signatures are valid for ±5 minutes.
Reference vectors and tests
vectors.json holds request-signing vectors (query strings, unicode bodies, PUT /
DELETE) and webhook vectors — five valid, four that must be rejected (tampered body, wrong
secret, changed timestamp, missing sha256=) plus an upper-case-hex case. Every SDK's test suite
pins them; the file itself is regenerated from the platform's signer by go test ./docs -run TestSDKVectors -update, so a change in the signing contract turns every SDK red at once. Use the
same file to test an SDK of your own.
Run the suites: sdk/typescript — npm ci && npm test; sdk/python — pip install -e '.[test]' && pytest;
sdk/go — go test -race ./...; sdk/php — php tests/run.php. CI runs all four
(.github/workflows/ci-sdk.yml) and keeps the built npm package as an artifact; nothing is
published automatically — and nothing is published at all yet, so integrators install from the
repository (see "Installing from the repository (pre-release)").
Versioning
SDK versions follow SemVer independently of the API contract; each SDK's User-Agent names its
version. Within API v1 only additive changes ship (API changelog); a field added to the
API appears in the raw response of every SDK immediately (TypeScript: regenerate the types;
Python: .raw; Go: add the field; PHP: arrays) and in the typed surface with the next SDK release.