Skip to main content
@sandpay/node is the official TypeScript SDK for Node.js 20+. It wraps the HTTP API, normalizes errors, and exposes a helper for verifying webhook signatures.
Prefer a ready-to-run starter? See Stack Builder — it generates a Next.js/Express/Hono project with @sandpay/node already wired up and your keys pre-filled.

Installation

npm install @sandpay/node
# or
pnpm add @sandpay/node

Import and constructor

import { SandPay } from "@sandpay/node";

const sp = new SandPay({
  apiKey: process.env.SANDPAY_API_KEY!,
  // baseUrl: "https://api.sandbox.sandpay.dev", // optional — default: https://api.sandpay.dev
});
OptionTypeDefaultDescription
apiKeystringRequired. Your sp_sk_test_... key.
baseUrlstringhttps://api.sandpay.devAPI root URL. Override to point at a custom environment.

payments.create(input)

Creates a simulated payment.
const tx = await sp.payments.create({
  amount: 25000,
  currency: "FCFA",
  operator: "orange",
  country: "CI",
  msisdn: "+22507123456",
  reference: "ORDER-2026-A1",
  scenario: "success", // optional — default: "success"
});
FieldTypeRequiredNotes
amountnumberyesPositive integer, smallest unit (FCFA = whole unit).
currencystringyesFCFA, XOF, XAF, RWF
operatormtn/orange/moov/airtelyesSee operator guides.
countrystring (2 chars)yesISO-3166. CI, BJ, TG, RW.
msisdnstringyesE.164 format (+225…).
referencestringyesYour internal reference (idempotency key).
descriptionstringnoFree-form description (max 200 characters).
scenarioPaymentScenarionoSee Scenarios.

payments.get(id)

const tx = await sp.payments.get("TX_8K3M9F");
Returns the same Payment object as create. Throws SandPayApiError with status: 404 if the ID is unknown.

payments.list(params?)

const page = await sp.payments.list({
  limit: 50,
  country: "CI",
  operator: "orange",
  status: "SUCCESS",
  cursor: previousPage.nextCursor ?? undefined,
});

console.log(page.data.length, page.hasMore, page.nextCursor);
ParamTypeDefaultNotes
limitnumber (1–100)50
countrystring2-letter filter.
operatorPaymentOperator
statusPaymentStatusSee scenarios for all 11 values.
cursorstringOpaque cursor returned by the previous page.

webhooks.verify(header, payload, secret)

Verifies the HMAC-SHA256 signature of a webhook. payload must be the raw body, exactly as received.
const ok = sp.webhooks.verify(
  req.headers["x-sandpay-signature"] as string,
  rawBody,
  process.env.SANDPAY_WEBHOOK_SECRET!,
);

Getting the rawBody

// app/api/webhooks/sandpay/route.ts
export async function POST(req: Request) {
  const rawBody = await req.text();
  const ok = sp.webhooks.verify(
    req.headers.get("x-sandpay-signature") ?? "",
    rawBody,
    process.env.SANDPAY_WEBHOOK_SECRET!,
  );
  if (!ok) return new Response("invalid", { status: 401 });
  const event = JSON.parse(rawBody);
  // ...
  return Response.json({ received: true });
}

webhooks.parseEvent(rawBody)

Parses an already-verified webhook body into a typed WebhookPayload. Call this after verify(...).
const rawBody = await req.text();
const signature = req.headers.get("x-sandpay-signature") ?? "";

if (!sp.webhooks.verify(signature, rawBody, process.env.SANDPAY_WEBHOOK_SECRET!)) {
  return new Response("invalid signature", { status: 401 });
}

const event = sp.webhooks.parseEvent(rawBody);
// event.event === "payment.completed"
// event.tx_id, event.status, event.amount, etc. — all typed
Throws an Error if:
  • the body is not valid JSON (Invalid JSON in webhook body),
  • the body is not a JSON object (Webhook body must be a JSON object),
  • the event name is unknown (Unknown webhook event: ...),
  • a required field is missing or has the wrong type (Missing or invalid field: ...).
Validation is intentionally minimal — it checks the shape just enough to provide safe downstream typing. For exhaustive validation, compose with zod.

Exported types

  • WebhookEventName — literal union of event names ("payment.completed" today).
  • WebhookPayload — interface of the payload returned by parseEvent.

SandPayApiError

Thrown on any non-2xx HTTP status.
import { SandPay, SandPayApiError } from "@sandpay/node";

try {
  await sp.payments.create({ /* ... */ });
} catch (err) {
  if (err instanceof SandPayApiError) {
    console.error(err.status, err.code, err.message, err.requestId);
    console.error(err.detail); // optional structured payload (per-field Zod errors)
  }
}
PropertyTypeDescription
statusnumberHTTP status (400, 401, 402, 404, 429, 5xx).
codestringMachine-readable code (see Errors).
messagestringHuman-readable message.
detailunknownOptional structured payload.
requestIdstring?X-Request-Id response header (useful for support).

Source code

The SDK is open source: github.com/sandpay/sandpay-node. PRs welcome.