Cripay Docs

Quickstart

A confirmed test payment in ten minutes. First without code from the app, then from your own code with a test key, the checkout URL and a signed webhook.

What you will have at the end

A payment created by you, paid (simulated, no real money), visible as confirmed in your activity, and received by your server through a webhook. Everything happens in test mode: nothing moves real funds and you do not need to own any crypto.

You need: a Cripay account (one minute to create), and for the code part a terminal with curl and any backend that can receive an HTTP POST. Replace https://api.your-domain.com and https://pay.your-domain.com below with the API and app URLs of your deployment (the app shows both under Developers).

Part 1: your first payment without code (2 minutes)

  1. Open the app, sign in with your email (you receive a 6-digit code) or with a wallet, and give your business a name. Leave the mode switch on Test.
  2. Tap Charge, type 25, and choose QR. You see a QR code, the amount, and "Waiting for payment".
  3. Open the checkout your customer would see: tap Share and open the link in another tab. Pick USDT and a network. Choose Scan or transfer: that is the address and exact amount a customer would send to.
  4. Nobody is going to pay a test QR, so simulate it: go to Activity, open the payment, and tap Simulate → Paid. The payment turns Confirmed and the checkout tab shows the receipt.

That is the whole product. Links work the same way (Charge → Link), and every payment you create from code shows up in the same list.

Part 2: the same from your code (8 minutes)

1. Create a test API key

Developers → API keys → New. Name it, copy the cpr_test_… key: it is shown once. Keep it on your server, never in a browser.

export CRYPTOPAY_API_KEY=cpr_test_...
export CRYPTOPAY_API_URL=https://api.your-domain.com

2. Create a payment

curl -X POST "$CRYPTOPAY_API_URL/v1/payments" \
  -H "Authorization: Bearer $CRYPTOPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-1042" \
  -d '{"amount":"25.00","currency":"USD","merchant_order_id":"ORDER-1042","description":"Black tee, size M","success_url":"https://shop.example/thanks"}'

What comes back (shortened):

{
  "id": "pay_8Qf3mZk2...",
  "status": "pending",
  "amount": "25.00",
  "currency": "USD",
  "checkout_url": "https://pay.your-domain.com/pay/pay_8Qf3mZk2...",
  "expires_at": "2026-10-27T12:30:00Z",
  "merchant_order_id": "ORDER-1042"
}

The three fields you care about: id (save it next to your order), checkout_url (where the customer goes) and status (pending for now).

The Idempotency-Key header makes retries safe: if your code sends the same request twice (a timeout, a retry), you get the same payment back instead of two. Your order id is a good key.

3. Send the customer to the checkout

Redirect to checkout_url, or open it on your own page:

<script src="https://pay.your-domain.com/embed.js" defer></script>
<button data-cryptopay-payment="pay_8Qf3mZk2..." data-cryptopay-success-url="/thanks">Pay with crypto</button>

Open the URL yourself now: you will see the checkout with your business name, the amount, the coins and networks, and the two ways to pay. The page updates by itself when the payment changes; never mark an order as paid from the browser, that is what the webhook is for.

4. Tell Cripay where your server is

Create a webhook endpoint under Developers → Webhooks → Add endpoint (or with POST /v1/webhook-endpoints). Copy its whsec_… secret once.

For a first try on your laptop, any public URL works: a tunnel such as ngrok http 3000, or a temporary inbox like webhook.site to just look at the event.

5. Simulate the payment

Test mode has a simulator, so you do not need a wallet:

curl -X POST "$CRYPTOPAY_API_URL/v1/test/payments/pay_8Qf3mZk2.../simulate" \
  -H "Authorization: Bearer $CRYPTOPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"outcome":"success"}'

Within a second: the payment is confirmed, the checkout shows the receipt, and your endpoint receives a POST like this:

{
  "id": "evt_5n2...",
  "type": "payment.confirmed",
  "mode": "test",
  "created_at": "2026-10-27T12:05:11Z",
  "data": {
    "payment_intent": {
      "id": "pay_8Qf3mZk2...",
      "status": "confirmed",
      "merchant_order_id": "ORDER-1042",
      "amount": "25.00",
      "currency": "USD",
      "asset": "USDT",
      "chain": "tron-nile",
      "tx_hash": "0xsim…"
    }
  }
}

Try the other outcomes too: underpaid, overpaid, expired, failed. Each produces its own event.

6. Handle the webhook in your code

Verify the signature, then act only on payment.confirmed. With the TypeScript SDK and Express:

import express from "express";
import { constructEvent, WebhookSignatureError } from "@cryptopay/sdk";

const app = express();

app.post("/webhooks/cryptopay", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = constructEvent({
      rawBody: req.body,                                   // the exact bytes, not a parsed object
      signature: req.header("x-cryptopay-signature"),
      timestamp: req.header("x-cryptopay-timestamp"),
      secret: process.env.CRYPTOPAY_WEBHOOK_SECRET!,       // the whsec_… from step 4
    });
  } catch (e) {
    return res.sendStatus(e instanceof WebhookSignatureError ? 400 : 500);
  }

  res.sendStatus(200);                                     // answer first, work after

  if (event.type === "payment.confirmed") {
    const payment = event.data.payment_intent;
    markOrderPaid(payment.merchant_order_id, event.id);    // ignore an event.id you already handled
  }
});

Without the SDK: compute HMAC-SHA256 over "<timestamp>.<raw body>" with the secret and compare it, constant-time, with the v1= value in X-CryptoPay-Signature. Python and PHP versions are in the webhooks guide.

7. Go live

  1. Add a payout wallet for each network family you accept (Profile → Payout wallets). This is where real money goes.
  2. Create a live key (Developers → API keys → New, switch the mode to Live first) and a live webhook endpoint.
  3. Replace the key and the secret in your configuration. Nothing else changes: same endpoints, same fields, same events.

Same thing with the SDK

import { Cripay } from "@cryptopay/sdk";

const cryptopay = new CryptoPay({ apiKey: process.env.CRYPTOPAY_API_KEY!, baseUrl: process.env.CRYPTOPAY_API_URL });

const payment = await cryptopay.payments.create(
  { amount: "25.00", currency: "USD", merchant_order_id: "ORDER-1042", success_url: "https://shop.example/thanks" },
  { idempotencyKey: "ORDER-1042" },
);
redirect(payment.checkout_url);

// later, in test mode:
await cryptopay.test.simulatePayment(payment.id, { outcome: "success" });

Mistakes we see most

  • Marking the order paid when the checkout says "confirmed". The browser can be faked; only the webhook (or GET /v1/payments/{id} from your server) is trustworthy.
  • Parsing the body before verifying the signature. The signature covers the raw bytes; verify first, parse after.
  • Forgetting that events repeat. Retries and replays deliver the same event.id more than once. Store it.
  • Using a test key in production. Test keys only see test networks; a real customer could never pay. The checkout shows a yellow "Test mode" strip when this happens.
  • Not sending an Idempotency-Key. A retried request without it creates two payments.

Next

  • How it works: statuses, fees, settlement and partial payments explained.
  • Payment links: sell without code.
  • Embedded checkout: modal or inline on your site.
  • Webhooks: every event, retries, replay, other languages.
  • The complete runnable example: examples/demo-store in the repository.

On this page