Skip to content

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. 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. 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. 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. 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. 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.

Create a payment
// 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.

FieldWhat it does
amountWhat to charge, as a decimal string: "49.90". Floats are refused everywhere — amounts are strings end to end.
currencyA 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_idOptional, from GET /v1/currencies. Fixes the coin now: the address and the exact amount come back in the response.
order_idYour own reference. It comes back in every webhook and in every export.
return_urlWhere the page offers to send the customer once the payment is done.
Idempotency-KeyHeader. 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
Your checkout page
<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>
Origins, events and CSP framing: widget guide →

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.

StatusMeaningTerminal
newCreated. Nothing received yet.no
pendingA transfer was detected. Waiting for confirmation.no
paidConfirmed and credited to your balance. An overpayment is still paid, with overpaid_amount above zero.yes
underpaidLess 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
expiredThe time limit passed with nothing confirmed.yes
cancelledYou 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. 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. 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. 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.

Verify and handle an event
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.

CodeWhat happenedWhat to do
400 validation_errorA field is missing or malformed.Fix details.field and send again.
401 auth.invalid_signatureThe 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_deniedThe key lacks the permission in details.required.Add the permission to the key in the cabinet, or use another key.
404 not_foundNo such id — or it belongs to another account.Check the id. Both cases answer the same way, on purpose.
409 conflictThe current state does not allow it: cancelling a paid payment, a second active address.Read the current state, then decide.
422 idempotency.mismatchThe same Idempotency-Key arrived with a different request.Use a new key for a new request.
429 rate_limitedToo many requests for this key — 600 per minute by default.Wait Retry-After seconds, then retry with backoff.
503 unavailableA rate or a coin is temporarily unavailable.Retry with backoff, or create the payment priced in crypto.
504 timeoutWe 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. 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. 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. 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. 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 reference

Every route, field and error in the OpenAPI document, browsable as Swagger UI.

Open 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.

SDK quickstart →

Plain-text version of this page for AI assistants ↗