Developers
From API call to payment status.
Accept crypto payments in your product: your server creates the payment, the customer pays on a hosted page or inside your own checkout, a signed webhook tells you it is confirmed. This page is the whole path — about ten minutes from a test key to your first payment.
Quickstart: ten minutes to a confirmed payment
Five steps, one test key, no node to run. Every snippet here runs on your server — the browser never sees a secret.
- 1
Create a test key 2 min
Developers → API keys → Create key with create_invoice and read. The secret is shown once: put it in your server's environment or secret manager. Test keys (ck_test_) are created the same way in the cabinet of the test environment — CryptoLike support gives you access and the test API URL (see below).
- 2
Create the payment on your server 3 min
One call with the amount, your order id and where to return the customer. Send your order id as the Idempotency-Key: a retry returns the same payment, never a second one.
- 3
Let the customer pay 2 min
Redirect to checkout_url, or embed the same page in your checkout with the widget. The page shows the address, the QR, the exact amount, the network warning and the live status.
- 4
Pay it on a test network 2 min
Ask CryptoLike support for test coins — they have no value — and send the exact amount to the address on the page. The status turns pending when the transfer is detected, paid when the confirmations are in.
- 5
Receive the result 1 min
Verify the webhook signature over the raw body, skip an event_id you have already stored, answer 2xx within 10 seconds, then fulfil the order. Polling GET /v1/invoices/{id} every few seconds works too.
Code language — The samples on this page follow your choice.
// Your server. The secret never reaches a browser.
import { CryptoLike } from "@cryptolike/sdk";
const api = new CryptoLike(); // CRYPTOLIKE_API_KEY_ID + CRYPTOLIKE_API_SECRET
const invoice = await api.invoices.create(
{
amount: "49.90",
currency: "USD", // or a ticker: "BTC", "USDT"
order_id: "A-1001",
return_url: "https://shop.example/orders/A-1001",
},
"order-A-1001", // Idempotency-Key: a retry returns this same payment
);
res.redirect(invoice.checkout_url); // invoice.status === "new"The SDKs read CRYPTOLIKE_API_KEY_ID and CRYPTOLIKE_API_SECRET from the environment. For test networks set CRYPTOLIKE_API_URL to the test API URL that CryptoLike support gives you.
That is the whole integration. Everything below is the detail behind those five steps.
Create a payment
One signed POST /v1/invoices. You get back a payment with an id, a status and the page to send the customer to.
| Field | What it does |
|---|---|
amount | What to charge, as a decimal string: "49.90". Floats are refused everywhere — amounts are strings end to end. |
currency | A fiat code (USD, EUR, UAH, GBP or CAD) or a coin ticker (BTC, USDT). With fiat the customer picks the coin on the page and the rate is locked at that moment. |
coin_id | Optional, from GET /v1/currencies. Fixes the coin now: the address and the exact amount come back in the response. |
order_id | Your own reference. It comes back in every webhook and in every export. |
return_url | Where the page offers to send the customer once the payment is done. |
Idempotency-Key | Header. The same key returns the same payment for 24 hours; the same key with a different body is 422 idempotency.mismatch. |
The response carries id, status: new, checkout_url and ttl_expires_at — the moment the payment stops waiting for a first transfer. Optional blocks are present as null, so one parser handles every response.
Repeat customers do not need an invoice each time: POST /v1/addresses issues a permanent deposit address for one customer and one asset. Every transfer to it is credited and announced by deposit.confirmed.
Every field and every route: API overview →Customer payment: hosted page or embedded widget
The same payment page, two ways to show it. Both read public payment data only, and neither needs a secret in the browser.
Hosted page
Redirect the customer to checkout_url. Nothing to build and nothing to style: the page handles the coin choice, the QR, the countdown and the live status, in English or Russian, light or dark.
- No front-end code on your side
- Returns the customer to your return_url
- A redirect from your backend is all it takes
Embedded widget
Keep the customer on your page: a loader of about 5 KB puts the same page in an iframe and reports status changes to your code. Add your origin to the allowlist in the cabinet first — Settings, the Integration card, Allowed origins.
- Your checkout keeps the customer
- onStatus / onPaid events in your page
- Brand themes and accent, no free colours
<div id="pay"></div>
<script src="https://cryptolike.net/widget/v1/cryptolike.js"></script>
<script>
CryptoLike.mount({
container: "#pay",
invoiceId: "4e2bc88c-cb79-4afc-b864-c1ba8e9d50c9", // from your server — the page gets nothing else
theme: "dark",
onStatus: function (m) { if (m.isFinal) showResult(m.status); },
});
</script>Status model
One status, shown the same way in the API, on the payment page and in webhooks. Terminal means the status does not change by itself.
| Status | Meaning | Terminal |
|---|---|---|
new | Created. Nothing received yet. | no |
pending | A transfer was detected. Waiting for confirmation. | no |
paid | Confirmed and credited to your balance. An overpayment is still paid, with overpaid_amount above zero. | yes |
underpaid | Less than the amount arrived. The same address stays open for a top-up until ttl_expires_at; remaining_amount says how much is missing. | after the TTL |
expired | The time limit passed with nothing confirmed. | yes |
cancelled | You cancelled it — possible only from new. | yes |
Terminal states: paid, expired, cancelled — and underpaid once the TTL has passed. Everything else can still move.
- Ship on paid.
- If a chain reorganisation rolls a confirmation back, the amount is recomputed and you receive invoice.reverted. Stop fulfilment until a new invoice.paid arrives.
- Late funds are still credited.
- A transfer confirmed after the payment closed lands on your balance, the payment gets late_payment: true and you receive invoice.late_payment. The status does not change — decide with the customer.
- Two amounts, one payment.
- payer_amount is what the customer must send, amount_crypto is what you receive. They differ only when the platform fee is paid by the payer.
Webhooks: signature, retries, exactly once
We POST a signed JSON event to your endpoint on every change. Three rules make the handler correct.
- 1
Verify over the raw bytes
X-Signature is "sha256=" plus the HMAC-SHA256 of X-Timestamp + "." + the raw body. Compare in constant time and reject a timestamp more than a few minutes off your clock, either way. Do not re-serialise the JSON first: key order is not guaranteed.
- 2
Act once per event_id
Delivery is at least once. The same event can arrive again with the same event_id, the same body and a fresh signature. Store the id; acknowledge a repeat without touching the order twice.
- 3
Answer 2xx within 10 seconds
Store the event, answer, process afterwards. Anything else — another status, a redirect, a timeout — is retried after 1 min, 5 min, 15 min, 1 h, 6 h and 24 h, then marked exhausted. You can resend any delivery from the cabinet.
import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@cryptolike/sdk";
// express.raw: the signature covers the bytes as received, not re-serialised JSON.
app.post("/hooks/cryptolike", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = verifyWebhook(req.headers, req.body, process.env.CRYPTOLIKE_WEBHOOK_SECRET);
} catch (e) {
if (e instanceof WebhookVerificationError) return res.sendStatus(401);
throw e;
}
res.sendStatus(200); // answer first — 10 seconds is the budget
if (seen(event.event_id)) return; // delivery is at least once: a repeat is normal
remember(event.event_id);
if (event.type === "invoice.paid") fulfil(event.data.invoice);
});Every SDK ships this as one call — verifyWebhook(headers, rawBody, secret) — with the constant-time compare and the timestamp tolerance built in. The same test vectors every SDK is pinned to are in the webhook contract.
Deliveries are independent, so a retried invoice.pending can arrive after invoice.paid. Trust created_at and the status inside data, not the arrival order.
Event types, fields and the test vector: webhooks contract →Errors: machine code, human message, recovery
One envelope for every error. Branch on code, show message to a person, read details for specifics — details.field names the bad field.
| Code | What happened | What to do |
|---|---|---|
400 validation_error | A field is missing or malformed. | Fix details.field and send again. |
401 auth.invalid_signature | The signature does not match the request we received. | Sign the timestamp, the method, the path with its query and the body hash, in that order; check the clock (±5 minutes). |
403 auth.permission_denied | The key lacks the permission in details.required. | Add the permission to the key in the cabinet, or use another key. |
404 not_found | No such id — or it belongs to another account. | Check the id. Both cases answer the same way, on purpose. |
409 conflict | The current state does not allow it: cancelling a paid payment, a second active address. | Read the current state, then decide. |
422 idempotency.mismatch | The same Idempotency-Key arrived with a different request. | Use a new key for a new request. |
429 rate_limited | Too many requests for this key — 600 per minute by default. | Wait Retry-After seconds, then retry with backoff. |
503 unavailable | A rate or a coin is temporarily unavailable. | Retry with backoff, or create the payment priced in crypto. |
504 timeout | We did not finish in time. | Retry with the same Idempotency-Key — it cannot create a second payment. |
A 4xx is never worth retrying unchanged. The SDKs retry 429 and 5xx (except 501) up to three times with jittered backoff, and stop when Retry-After is longer than 30 seconds.
Every code and its details: errors in the API overview →Sandbox: test keys and test networks
A ck_test_ key signs exactly like a live one and works only in the test environment, on test networks — there a live key, too, moves test coins only. Test coins have no value, and the flow is identical to production. Production runs on mainnets: it does not issue test keys and refuses them with 401 auth.test_key, so nothing is created.
- 1
Point the SDK at test networks
Create the key with the same permissions and set CRYPTOLIKE_API_URL to the test API URL; ask CryptoLike support for it. Keep test and live keys in separate configurations.
- 2
Read what the test networks offer
GET /v1/currencies lists the assets, their networks, their decimals and the confirmations each one needs — the same numbers the payment page shows the customer.
- 3
Get test coins
Ask CryptoLike support for test coins; they have no value. Send the exact payer_amount to the address on the page, on the network named there.
- 4
Walk the whole path
Underpay on purpose to see underpaid and a top-up; let one payment run out to see expired; cancel one from new. Developers → Webhooks → endpoint → Send test event exercises your handler with a signed ping, without a payment.
Go-live checklist
Ten checks before the first real payment.
- Live key created with the smallest permissions that work (create_invoice + read); the secret lives in your server's secret store, never in the repository.
- Test keys and the test API URL are gone from the production configuration.
- Every POST carries an Idempotency-Key — your order id, or a UUID generated once and reused on retries.
- The webhook endpoint is https:// on a public host and answers 2xx within 10 seconds.
- The signature is verified over the raw body; the timestamp is checked against a synced clock (NTP, ±5 minutes).
- event_id is stored; a repeated delivery is acknowledged without touching the order twice.
- The order is fulfilled on invoice.paid, not on a browser event; invoice.reverted stops fulfilment.
- expired, cancelled and final underpaid each have a screen and a next action for the customer.
- Amounts are decimal strings end to end — no floats, no rounding of your own.
- If you embed the widget: the production origin is on the allowlist and your checkout is served over https.
After go-live: the cabinet shows every delivery with its attempts and response, and you can resend any of them.
Questions developers ask
How long does it take to integrate CryptoLike?
A first payment on a test network takes about ten minutes: create an API key, call POST /v1/invoices from your server, send the customer to checkout_url and handle one signed webhook. Production is the same three pieces of code — the rest of the work is your own order states.
Do I need to run a node or hold private keys?
No. CryptoLike derives the addresses, watches the networks, counts confirmations, consolidates funds and signs payouts. Your integration is one REST API: create a payment, read its status, receive signed webhooks. Confirmed funds land on your platform balance.
Can I keep the payment page inside my own checkout?
Yes. The checkout widget embeds the same payment page in an iframe with a loader of about 5 KB and reports status changes to your page through postMessage. Add your origin to the allowlist in the cabinet; the browser receives the invoice id and nothing else.
How do I process a webhook exactly once?
Verify the HMAC-SHA256 signature over the raw body, then look event_id up in your store: if you have seen it, answer 2xx and stop. Delivery is at least once and retried six times over about 31 hours, so duplicates are expected, not an error.
Can I price a payment in fiat?
Yes — currency takes a fiat code (USD, EUR, UAH, GBP or CAD) as well as a coin ticker. With fiat the customer picks the coin on the payment page and the exchange rate is locked at that moment; it holds until the payment expires, so the amount the customer sees does not move.
What happens if the customer pays too little, too much or too late?
Too little: the payment becomes underpaid and the address stays open for a top-up until the TTL. Too much: the status is paid with overpaid_amount above zero. Too late: the funds are credited anyway, the payment carries late_payment and you receive invoice.late_payment.
Read next
- API overview — resources, conventions, versioning →
- Authentication — keys, permissions, request signing →
- Webhooks — event types, fields, test vector →
- SDKs — TypeScript, Python, Go, PHP →
- Checkout widget — embed the payment page →
API reference
Every route, field and error in the OpenAPI document, browsable as Swagger UI.
Official SDKs
TypeScript, Python, Go and PHP: signed requests, an Idempotency-Key on every POST, retries and webhook verification. The secret stays on your server.