Cripay Docs

TypeScript SDK

@cryptopay/sdk: a typed client generated from the OpenAPI document, with idempotency, retries, pagination and webhook verification. Every method, with an example.

Install

The SDK lives in the repository under packages/sdk-js until it is published to npm. In a monorepo add it as a workspace dependency; elsewhere copy the folder or build it (pnpm --filter @cryptopay/sdk build) and reference the dist/ output.

pnpm add @cryptopay/sdk            # once published; today: "@cryptopay/sdk": "workspace:*"

Any other language works against the same REST API: the OpenAPI document generates a client for Python, PHP, Go or Java, and the webhook verifiers for Python and PHP are in the webhooks guide.

Create the client

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

const cryptopay = new CryptoPay({
  apiKey: process.env.CRYPTOPAY_API_KEY!,   // cpr_test_… or cpr_live_…
  baseUrl: process.env.CRYPTOPAY_API_URL!,   // your API origin
  maxRetries: 2,                             // 429 and 5xx are retried with backoff (default 2)
  timeoutMs: 30_000,                         // per request (default 30 s)
});

The key decides test or live mode; the SDK adds nothing else. Keep it on the server: never ship it to a browser or a mobile app.

Every request option

Every method takes an optional last argument:

{ idempotencyKey?: string; signal?: AbortSignal; headers?: Record<string, string> }

Creates (payments.create, paymentLinks.create, payouts.create) send an Idempotency-Key automatically (a random one) unless you pass your own. Pass your order id so a retried request returns the same payment instead of a second one.

Payments

// Create: price in USD, get a checkout_url
const payment = await cryptopay.payments.create(
  { amount: "25.00", currency: "USD", merchant_order_id: "ORDER-1042", description: "Black T-shirt M", success_url: "https://shop.example/thanks", cancel_url: "https://shop.example/cart" },
  { idempotencyKey: "ORDER-1042" },
);
payment.id;            // "pay_…"
payment.checkout_url;  // send the customer here
payment.status;        // "pending"

// Restrict what can be paid, or how
await cryptopay.payments.create({ amount: "100.00", currency: "USD", accepted_assets: ["USDT", "USDC"], accepted_chains: ["tron", "base"], allowed_payment_methods: ["transfer"], expires_in_minutes: 60 });

// Read
const same = await cryptopay.payments.retrieve(payment.id);
same.is_final;         // true once nothing can change

// List (cursor pagination) and iterate every page
const page = await cryptopay.payments.list({ status: "confirmed", limit: 50 });
page.has_more; page.next_cursor;
for await (const p of cryptopay.payments.listAutoPaging({ status: "confirmed" })) console.log(p.id, p.amount);

// Cancel an unpaid payment
await cryptopay.payments.cancel(payment.id);

Payment fields worth knowing: status and is_final (see statuses), amount/currency (what you priced), asset/chain/tx_hash (how it was paid, once paid), paid_amount_units and net_amount_units (what arrived and what you receive, in minor units), fee_payer, expires_at, metadata.

Test mode

// Only with a cpr_test_ key: confirm a payment without crypto and deliver the real webhooks
await cryptopay.test.simulatePayment(payment.id, { outcome: "success" }); // underpaid | overpaid | expired | failed
const link = await cryptopay.paymentLinks.create({ title: "Consulting hour", amount: "80.00", currency: "USD", max_uses: 10 });
link.url;                                               // share it; each open creates a payment
await cryptopay.paymentLinks.create({ title: "Tip jar", min_amount: "1.00", max_amount: "500.00", collect_email: true });
await cryptopay.paymentLinks.update(link.id, { status: "inactive" });
const links = await cryptopay.paymentLinks.list({ status: "active" });
for await (const l of cryptopay.paymentLinks.listAutoPaging()) console.log(l.title);

Currencies and estimates

const currencies = await cryptopay.currencies.list();        // asset × network your key can use, minimums, network fee, confirmations
const estimate = await cryptopay.estimates.create({ amount: "25.00", currency: "USD", chain: "tron", asset: "USDT", payment_method: "transfer" });
estimate.expected_amount_units; estimate.fee_amount_units; estimate.net_amount_units; estimate.fee_payer;

Webhook endpoints and deliveries

// Register once; the secret comes back only here
const endpoint = await cryptopay.webhookEndpoints.create({ url: "https://shop.example/webhooks/cryptopay", enabled_events: [] }); // [] = every event
endpoint.secret;                                             // whsec_… → CRYPTOPAY_WEBHOOK_SECRET

await cryptopay.webhookEndpoints.list();
await cryptopay.webhookEndpoints.update(endpoint.id, { enabled_events: ["payment.confirmed", "payment.overpaid", "payment.expired"] });
await cryptopay.webhookEndpoints.sendTest(endpoint.id);      // a signed test.ping
const rotated = await cryptopay.webhookEndpoints.rotateSecret(endpoint.id); // the old secret keeps signing for 24 h (two v1= values)
await cryptopay.webhookEndpoints.revokePreviousSecret(endpoint.id);          // if the old one leaked
await cryptopay.webhookEndpoints.delete(endpoint.id);

// What was delivered, with your server's response code and body
const deliveries = await cryptopay.webhookDeliveries.list({ payment: payment.id });
await cryptopay.webhookDeliveries.retry(deliveries.data[0].id);

Verify a webhook

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

// Express: keep the raw body
app.post("/webhooks/cryptopay", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = constructEvent({
      rawBody: req.body,                                  // Buffer or string, exactly as received
      signature: req.header("x-cryptopay-signature"),
      timestamp: req.header("x-cryptopay-timestamp"),
      secret: process.env.CRYPTOPAY_WEBHOOK_SECRET!,
      toleranceSeconds: 300,                              // default
    });
  } catch (e) {
    return res.sendStatus(e instanceof WebhookSignatureError ? 400 : 500);
  }
  res.sendStatus(200);                                    // answer first, work after
  if (event.mode !== "live") return;                      // ignore test events in production
  if (event.type === "payment.confirmed" || event.type === "payment.overpaid") {
    fulfil(event.data.payment_intent.merchant_order_id, event.id);  // de-duplicate by event.id
  }
});

// Next.js route handler (App Router)
export async function POST(req: Request) {
  const rawBody = await req.text();
  const event = constructEvent({ rawBody, signature: req.headers.get("x-cryptopay-signature"), timestamp: req.headers.get("x-cryptopay-timestamp"), secret: process.env.CRYPTOPAY_WEBHOOK_SECRET! });
  // … queue the work, then:
  return new Response(null, { status: 200 });
}

verifySignature({ rawBody, signature, timestamp, secret }) returns a boolean when you prefer to decide yourself; signPayload(secret, timestamp, rawBody) produces the same signature (useful in tests).

Merchant, balances, payouts, request logs

const merchant = await cryptopay.merchant.retrieve();
await cryptopay.merchant.update({ fee_payer: "payer", settlement_mode: "direct", default_locale: "es" });

const balances = await cryptopay.balances.list();                     // custodial balance per asset/network (settlement_mode = balance)
const payout = await cryptopay.payouts.create({ asset: "USDT", chain: "tron", amount_units: "25000000", destination: "T…" }, { idempotencyKey: "payout-2026-10-27" });
await cryptopay.payouts.retrieve(payout.id);

const logs = await cryptopay.requestLogs.list({ status: "4xx", limit: 20 }); // your recent calls, redacted bodies, request_id

Errors

import { CryptoPayError } from "@cryptopay/sdk";
try {
  await cryptopay.payments.retrieve("pay_missing");
} catch (e) {
  if (e instanceof CryptoPayError) {
    e.status;      // 404
    e.code;        // "payment_not_found"  (stable, documented per endpoint)
    e.type;        // "not_found"
    e.requestId;   // "req_…" → Developers → Request logs
    e.message;
  }
}

Validation errors are 400 invalid_request with details; a wrong key is 401; a key without the scope is 403 forbidden_scope; a key of the other mode never sees the resource (404). The SDK retries 429 (honouring Retry-After) and 5xx up to maxRetries, always with the same idempotency key.

Types

Every request and response type is exported and generated from the OpenAPI document, so they cannot drift from the API:

import type { Payment, PaymentList, CreatePaymentParams, PaymentLink, WebhookEndpoint, WebhookDelivery, WebhookEvent, Currency, Estimate, Merchant, Payout, Balance, RequestLog } from "@cryptopay/sdk";

Regenerate after an API change with pnpm --filter @cryptopay/sdk generate.

On this page