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)
- 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.
- Tap Charge, type
25, and choose QR. You see a QR code, the amount, and "Waiting for payment". - 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.
- 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.com2. 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
- Add a payout wallet for each network family you accept (Profile → Payout wallets). This is where real money goes.
- Create a live key (Developers → API keys → New, switch the mode to Live first) and a live webhook endpoint.
- 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.idmore 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-storein the repository.