# Node.js SDK

Source: https://caravelpay.app/docs/node-sdk · Caravel Pay docs · Last updated 2026-11-09

`caravel-pay` is ESM with TypeScript types and has **zero dependencies**: it uses Node's built-in `fetch` and `node:crypto`. It signs every request, verifies webhooks in constant time and checks responses before you use them. Node 18.17 or newer.

## Install

Install the package from its tarball. npm records the address in `package.json`, so your deploys fetch the same file.

*npm*

```sh
npm install https://caravelpay.app/download/caravel-pay-1.0.0.tgz
```

Or download [`caravel-pay-1.0.0.tgz`](https://caravelpay.app/download/caravel-pay-1.0.0.tgz) and run `npm install ./caravel-pay-1.0.0.tgz`.

## Get API keys

Run once and keep the result in your server's environment. Every call creates a new merchant. See [Get your API keys](https://caravelpay.app/docs/connect).

*Connect once*

```js
import { connect } from "caravel-pay";

const keys = await connect({
  site_url: "https://shop.example",
  payout_address: "0x52908400098527886E0F7030069857D2E4169EE7",
});
// { merchant_id: "mch_…", secret: "…", status: "active", fee_rate: "2%" }
```

## Create a checkout

*Create a checkout and redirect*

```js
import { Caravel, normalizeAmount } from "caravel-pay";

const caravel = new Caravel({
  merchantId: process.env.CARAVEL_MERCHANT_ID,
  secret: process.env.CARAVEL_SECRET,
  maxRetries: 2,
});

const checkout = await caravel.createCheckout({
  order_id: "1042",
  amount: normalizeAmount(order.total), // "136.41"
  return_url: "https://shop.example/thanks",
  cancel_url: "https://shop.example/cart",
  callback_url: "https://shop.example/webhooks/caravel",
});
res.redirect(303, checkout.checkout_url);
```

## Handle the webhook

*Express*

```js
import express from "express";
import { CaravelSignatureError, isPaid, txHash, verifyWebhook } from "caravel-pay";

// express.raw keeps the exact bytes that were signed
app.post("/webhooks/caravel", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = verifyWebhook({
      body: req.body,
      headers: req.headers,
      secret: process.env.CARAVEL_SECRET, // or { [merchantId]: secret } while old keys are in use
      callbackUrl: "https://shop.example/webhooks/caravel",
    });
  } catch (error) {
    if (error instanceof CaravelSignatureError) return res.sendStatus(error.status);
    throw error;
  }
  if (await alreadyHandled(event.id)) return res.sendStatus(200);
  // Re-read the checkout before changing the order.
  const checkout = await caravel.getCheckout(event.data.id);
  if (isPaid(checkout)) await markPaid(checkout.order_id, txHash(checkout));
  await remember(event.id);
  res.sendStatus(200);
});
```

`headers` can be a plain object or a `Headers` instance. `body` can be a string, `Buffer` or `Uint8Array`, but never parsed JSON.

## Reference

| Call | Endpoint |
|---|---|
| `connect(params)` | `POST /v1/connect`, unsigned, never retried |
| `caravel.createCheckout(params, { idempotencyKey })` | `POST /v1/checkouts` |
| `caravel.getCheckout(id)` | `GET /v1/checkouts/{id}` |
| `caravel.listCheckouts({ limit, cursor })`, `for await (… of caravel.allCheckouts())` | `GET /v1/checkouts` |
| `caravel.getMerchant()` | `GET /v1/merchant` |
| `caravel.createPaymentLink(params)`, `Caravel.createPaymentLinkWithoutKeys(params)` | `POST /v1/payment-links` |
| `caravel.getPaymentLink(id)`, `deactivatePaymentLink(id)` | `GET /v1/payment-links/{id}`, `POST …/deactivate` |
| `verifyWebhook({ body, headers, secret, callbackUrl })` | Verifies a webhook and returns the event |

Errors:

- `CaravelApiError` has `code`, `status`, `param`, `retryAfter` and `retryable`.
- `CaravelNetworkError` means no response arrived.
- `CaravelUnexpectedResponseError` means a malformed 2xx answer.
- `CaravelSignatureError` has `reason` and `status`.

From CommonJS, use `await import("caravel-pay")`. On Node 20.19+ and 22.12+, `require("caravel-pay")` also works.
