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 docs/API.md; 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 ≥ 8.1, ext-curl or PSR-18 |
sdk/php |
Every SDK sends User-Agent: cryptolike-sdk-<language>/<version>, pins X-API-Version: v1,
and passes the same reference vectors (docs/sdk/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 (docs/WIDGET.md), 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) |
docs/API-AUTH.md §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 |
docs/API-AUTH.md §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 |
docs/API-AUTH.md §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 |
docs/API-AUTH.md §5 |
| Errors | One error type carrying status, code (stable machine code), message, details, field, retry_after, request_id; 4xx is never retried |
docs/API.md §5 |
| Amounts | Decimal strings ("10.50") — Python exposes Decimal; floats are refused everywhere |
docs/API.md §6 |
| Pagination | list returns {items, next_cursor}; iterate / Each walks every page |
docs/API.md §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 |
docs/WEBHOOKS.md §3 |
| Version | X-API-Version: v1 on every request; only additive changes within v1 |
docs/API-CHANGELOG.md |
Resources covered: invoices (create / get / list / cancel / export), static addresses,
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.
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. Use ack_test_…key against a dev/stage instance first. - 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 from a dev or stage instance only works against that host.
export CRYPTOLIKE_API_URL=https://api.stage.exampleA payment is priced in fiat (amount + currency) and the payer chooses the coin and network
on the checkout page; pass coin_id (from currencies.list) only when you want to fix that choice
up front. 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(class="tok-num">401)
if event.type == "invoice.paid" and not seen(event.event_id):
fulfil(event.data["invoice"])
return "", class="tok-num">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, class="tok-num">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(class="tok-num">401); exit;
}
if ($event['type'] === 'invoice.paid' && !seen($event['event_id'])) { fulfil($event['data']['invoice']); }
http_response_code(class="tok-num">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() }.
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
docs/sdk/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 (docs/API-CHANGELOG.md); 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.