The @your-org/checkout-sdk package is a fully typed TypeScript client for the Crypto Checkout API. It covers all public resources — payment links, checkout sessions, webhook endpoints, and events — and includes built-in webhook signature verification so you never have to hand-roll HMAC logic.
Installation
npm install @your-org/checkout-sdk
Initialization
Import CheckoutClient and pass your credentials. The client is safe to instantiate once and reuse across requests.
import { CheckoutClient } from "@your-org/checkout-sdk";
const client = new CheckoutClient({
apiKey: process.env.CHECKOUT_API_KEY!, // ck_live_... or ck_test_...
baseUrl: "https://your-checkout-domain.com", // your self-hosted instance URL
});
ClientOptions
| Option | Required | Description |
|---|
apiKey | ✅ | Your API key (ck_live_… for production, ck_test_… for test mode). |
baseUrl | No | The root URL of your Crypto Checkout instance. Defaults to https://checkout.example.com. Always set this to your own deployment. |
fetch | No | A custom fetch implementation. Useful when polyfilling in older Node.js runtimes or in test environments. |
The apiKey is the only required option. If you omit baseUrl the client falls back to the default placeholder — make sure you override it in production.
Resources
Payment Links
Payment links are reusable URLs that can accept unlimited payments. Create one per product, plan, or donation tier and share the checkoutUrl with customers.
// Create a payment link
const link = await client.paymentLinks.create({
title: "Premium Plan",
fiatAmount: "99.00",
fiatCurrency: "USD",
successUrl: "https://your-site.com/thanks",
});
console.log(link.checkoutUrl); // share this with customers
// List payment links (cursor-paginated)
const { data } = await client.paymentLinks.list({ limit: 10 });
// Retrieve a single link
const link = await client.paymentLinks.retrieve("pl_...");
// Update — e.g. deactivate
const updated = await client.paymentLinks.update("pl_...", { active: false });
// Delete permanently
await client.paymentLinks.del("pl_...");
Checkout Sessions
Checkout sessions are single-use payment intents. Create a session either from an existing payment link (linkId) or ad-hoc with an inline amount. Always redirect the customer to session.checkoutUrl immediately after creation.
// Link mode — amount is inherited from the payment link
const session = await client.checkoutSessions.create(
{
linkId: "pl_...",
customerEmail: "[email protected]",
metadata: { orderId: "ORD-42" },
},
{ idempotencyKey: "order-42-v1" },
);
// Ad-hoc mode — supply amount directly
const session = await client.checkoutSessions.create(
{
amount: { value: "73.42", currency: "USD" },
successUrl: "https://store.com/ok",
},
{ idempotencyKey: "payment-abc-v1" },
);
console.log(session.checkoutUrl); // redirect the customer here
console.log(session.status); // "pending"
// Retrieve by ID
const s = await client.checkoutSessions.retrieve("cs_...");
// List with filters
const paid = await client.checkoutSessions.list({ status: "paid", limit: 25 });
Always pass an idempotencyKey when creating sessions so that network retries don’t create duplicate payments. See Idempotency for the full key format rules.
Session status values
| Status | Meaning |
|---|
pending | Waiting for a blockchain transfer |
detected | Transfer seen on-chain, awaiting confirmations |
paid | Fully confirmed at the expected amount |
underpaid | Payment received but below the required amount |
overpaid | Payment received but above the required amount |
paid_late | Confirmed after the session had expired |
expired | Timer elapsed with no detected payment |
failed | Terminal failure |
Webhook Endpoints
Register HTTPS URLs to receive real-time event notifications. The secret returned on creation is used to verify incoming payloads — save it immediately, it is only shown once.
// Create a webhook endpoint
const endpoint = await client.webhookEndpoints.create({
url: "https://your-site.com/webhooks/crypto",
events: ["session.paid", "session.expired"],
});
console.log(endpoint.secret); // save this — only shown once
// Retrieve (secret is NOT returned after creation)
const ep = await client.webhookEndpoints.retrieve(endpoint.id);
// List all endpoints
const { data: endpoints } = await client.webhookEndpoints.list();
// Send a test event to verify your handler
await client.webhookEndpoints.test(endpoint.id);
// Delete an endpoint
await client.webhookEndpoints.del(endpoint.id);
The webhook secret (prefixed whsec_…) is returned only in the create response. Store it in a secret manager or environment variable before the response leaves your server.
Events
Events are an immutable log of everything that happened in your account. You can fetch them individually, page through them, or use autoPaginate to stream all matching events without managing cursors.
// List the 20 most recent events
const { data: events } = await client.events.list({ limit: 20 });
// Retrieve a single event by ID
const event = await client.events.retrieve("evt_...");
// Auto-paginate — iterate over all paid events without managing cursors
for await (const event of client.events.list({
type: "session.paid",
autoPaginate: true,
})) {
console.log(event.id, event.createdAt);
}
Error Handling
Every API error is thrown as a typed subclass of CheckoutError. Catch the most specific class you care about; all errors expose status, code, type, and an optional param field that names the invalid request parameter.
import {
CheckoutError,
AuthenticationError,
PermissionError,
InvalidRequestError,
NotFoundError,
ConflictError,
RateLimitError,
ServerError,
} from "@your-org/checkout-sdk";
try {
const session = await client.checkoutSessions.create({ /* ... */ });
} catch (err) {
if (err instanceof AuthenticationError) {
// Invalid or missing API key — check your CHECKOUT_API_KEY env var
console.error("Auth failed:", err.message);
} else if (err instanceof InvalidRequestError) {
// The request body had a validation problem
console.error(`Invalid param "${err.param}":`, err.message);
} else if (err instanceof NotFoundError) {
// The resource ID doesn't exist or isn't accessible
console.error("Not found:", err.message);
} else if (err instanceof RateLimitError) {
// Back off and retry after a short delay
await sleep(1000);
} else if (err instanceof CheckoutError) {
// Catch-all for any other API error
console.error(`[${err.status}] ${err.code}:`, err.message);
} else {
throw err; // re-throw non-SDK errors
}
}
Error class reference
| Class | HTTP status | When it’s thrown |
|---|
AuthenticationError | 401 | Missing, malformed, or revoked API key |
PermissionError | 403 | Key lacks the required scope |
InvalidRequestError | 400 | Request body or parameters failed validation |
NotFoundError | 404 | Resource doesn’t exist or isn’t accessible |
ConflictError | 409 | Duplicate idempotent request with different parameters |
RateLimitError | 429 | Too many requests — back off and retry |
ServerError | 5xx | Unexpected server-side error |
Idempotency
Pass an idempotencyKey string in the second argument (RequestOptions) on any mutating call. The server replays the original response for up to 24 hours when it receives the same key, so retrying a failed network request never creates a duplicate.
const session = await client.checkoutSessions.create(
{ amount: { value: "25.00", currency: "USD" } },
{ idempotencyKey: `order-${orderId}-v1` },
);
Keys must be unique per logical operation. A good convention is <resource>-<externalId>-<attemptVersion>. See Idempotency for the full specification.
Webhook Verification
Use the verifyWebhook helper (shipped as a separate entry point) to validate the HMAC-SHA256 signature on every incoming webhook before trusting its payload.
import { verifyWebhook, WebhookVerificationError } from "@your-org/checkout-sdk/webhooks";
// In your HTTP handler (Express, Next.js Route Handler, etc.)
const rawBody = await req.text(); // must be the raw, unparsed body string
try {
const event = verifyWebhook({
payload: rawBody,
signature: req.headers.get("x-webhook-signature") ?? "",
secret: process.env.CHECKOUT_WEBHOOK_SECRET!,
toleranceSeconds: 300, // default; reject events older than 5 minutes
});
switch (event.type) {
case "session.paid":
await fulfillOrder(event.data);
break;
// handle other event types...
}
} catch (err) {
if (err instanceof WebhookVerificationError) {
return new Response("Unauthorized", { status: 401 });
}
throw err;
}
For the full signature algorithm and header format, see Webhook Signature Verification.