snap-payments-sdk
v1.3.0
Published
Official TypeScript SDK for the SNAP payments API - Somalia's developer-first payment infrastructure.
Maintainers
Readme
snap-payments-sdk
Official server-side TypeScript SDK for the SNAP payments API. Runs in Node 18+, Bun, Deno, and edge runtimes.
Server vs browser. This package (
snap-payments-sdk) is for backend code, it authenticates with your secret key and can create payments, refunds, and other side-effecting operations. For browser code that mounts Card / QR / Wallet UI and confirms payments with a publishable key, use@snap-pay/js
@snap-pay/elementsinstead. Never ship your secret key to the browser.
Package name note. The
@snapnpm scope is owned by Snap Inc., so the server SDK publishes as the unscopedsnap-payments-sdk. The browser SDK uses the SNAP-owned@snap-payscope (@snap-pay/js,@snap-pay/elements,@snap-pay/react).
Install
npm install snap-payments-sdk
# or
pnpm add snap-payments-sdkQuick start
import { Snap } from "snap-payments-sdk";
const client = new Snap(process.env.SNAP_SECRET_KEY!);
const payment = await client.payments.create({
amount: 1500, // $15.00 in cents
provider: "evcPlus",
customer: { phone: "+252615123456" },
});
console.log(payment.id, payment.status);Surface
Core
client.payments.create(params, options?)
client.payments.retrieve(id, options?)
client.refunds.create(params, options?)
client.refunds.retrieve(id, options?)
client.checkout.sessions.create(params, options?)
client.webhooks.verify({ payload, header, secret, tolerance?, now? })Billing (v0.2.0)
client.customers.create(params, options?)
client.customers.retrieve(id, options?)
client.customers.update(id, params, options?)
client.customers.list({ limit?, starting_after?, email?, includeArchived? }, options?)
client.customers.delete(id, options?) // archives; customers are never hard-deleted
client.invoices.create(params, options?)
client.invoices.retrieve(id, options?)
client.invoices.list(params?, options?)
client.invoices.send(id, options?)
client.invoices.void(id, options?)
client.prices.create(params, options?)
client.prices.list({ productId?, limit? }, options?)
client.prices.archive(id, options?)
client.subscriptions.create(params, options?)
client.subscriptions.cancel(id, { atPeriodEnd? }, options?)
client.subscriptions.resume(id, options?)
client.payouts.create(params, options?)
client.payouts.retry(id, options?)
client.payouts.cancel(id, options?)
client.portalSessions.create(params, options?)Platform (v0.3.0)
client.connectedAccounts.create(params, options?)
client.connectedAccounts.list({ status?, limit? }, options?)
client.transfers.create(params, options?)
client.transfers.list({ destination?, status?, limit? }, options?)
client.revenue.summary({ rangeStart, rangeEnd }, options?)
client.revenue.listSchedules(params?, options?)
client.revenue.retrieveSchedule(id, options?)
// name: revenue_daily | refund_rate | provider_mix | top_customers
// | mrr_trend | subscription_activity
// format: "csv" resolves to the CSV text instead of a Report
client.reports.get({ name, rangeStart, rangeEnd, format? }, options?)
// conditions: [{ signal, comparator, numberValue | stringValue | ... }]
client.radar.createRule(params, options?)
client.radar.listRules({ active?, limit? }, options?)
client.radar.listEvaluations({ status?, limit? }, options?)
client.radar.retrieveEvaluation(id, options?)
client.radar.resolveEvaluation(id, { outcome, reason? }, options?)
client.payoutMethods.create(params, options?)
client.payoutMethods.delete(id, options?)All return values are fully typed against the wire format the API returns. Field names are snake_case to match the JSON payload - no silent renaming.
End-to-end: subscribe a new customer
A minimal flow that covers most SaaS onboarding paths - create a recurring price, start a subscription, mint a customer-portal link for the confirmation email, and hand back a session URL the browser can redirect to.
import { Snap } from "snap-payments-sdk";
const client = new Snap(process.env.SNAP_SECRET_KEY!);
// 1. One-time setup: a product + a monthly price. Skip on repeat.
const price = await client.prices.create({
productId: "prod_starter",
unitAmount: 2900, // $29.00
interval: "month",
nickname: "Starter monthly",
});
// 2. Start the subscription. Server generates the first invoice
// and holds it in `incomplete` until the checkout succeeds.
const subscription = await client.subscriptions.create({
customerId: "cus_123",
priceId: price.id,
});
// 3. Hosted checkout for the first invoice. `successUrl` gets a
// session_id query param so you can verify the return.
const invoiceUrl = subscription.latest_invoice_id
? (await client.invoices.retrieve(subscription.latest_invoice_id))
.hosted_invoice_url
: null;
// 4. Customer-portal link for the receipt email. Expires in 24h.
const portal = await client.portalSessions.create({
customerId: "cus_123",
returnUrl: "https://app.example.com/billing",
expiresInSeconds: 60 * 60 * 24,
});
return { checkoutUrl: invoiceUrl, portalUrl: portal.url };Any of these calls throws a typed SnapApiError on 4xx and auto-
retries transient 5xx / 429 responses with exponential backoff.
Authentication & idempotency
const client = new Snap({
apiKey: process.env.SNAP_SECRET_KEY!,
baseUrl: process.env.SNAP_API_URL!, // or omit and export SNAP_API_URL
});The API host is not baked in - you must either pass baseUrl
explicitly or export SNAP_API_URL in the process environment. If
neither is set the constructor throws, so a typo can't silently
route live-mode calls at the wrong environment.
Mutating calls (POST/PUT) auto-generate an Idempotency-Key (UUID
v4) when you don't pass one. To pin your own - recommended for any
call that might be retried by your caller - pass it via the
per-request options:
await client.payments.create(
{ amount: 100, provider: "mock" },
{ idempotencyKey: `order_${orderId}` },
);The API replays the same response for the same key + same body for
the merchant's idempotency TTL window. A different body under the
same key throws IdempotencyConflictError.
Errors
import {
Snap,
SnapApiError,
AuthenticationError,
NotFoundError,
IdempotencyConflictError,
} from "snap-payments-sdk";
try {
await client.payments.retrieve("pay_missing");
} catch (err) {
if (err instanceof NotFoundError) {
// 404 - handle missing payment
} else if (err instanceof AuthenticationError) {
// 401 - rotate or check the API key
} else if (err instanceof SnapApiError) {
// any other API-side failure - `err.code`, `err.requestId`, etc.
} else {
// network failure, timeout, or non-JSON 5xx - SnapConnectionError
throw err;
}
}| Class | Wire codes |
| --------------------------- | --------------------------------------------------------------------- |
| AuthenticationError | authentication_failed |
| PermissionDeniedError | permission_denied, merchant_not_approved, merchant_suspended |
| InvalidRequestError | validation_error, idempotency_key_missing, unsupported_provider |
| IdempotencyConflictError | idempotency_conflict |
| NotFoundError | not_found |
| ProviderError | provider_offline, provider_error |
| ServerError | internal_error |
| SnapConnectionError | network failures, timeouts, malformed responses |
Every SnapApiError carries code, type, statusCode, requestId,
and the raw response body for log forwarding.
Webhooks
import { Snap, WebhookVerificationError } from "snap-payments-sdk";
const client = new Snap(process.env.SNAP_SECRET_KEY!);
app.post("/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await client.webhooks.verify({
payload: req.body.toString("utf8"),
header: req.header("X-SNAP-Signature")!,
secret: process.env.SNAP_WEBHOOK_SECRET!,
});
// event.type → "payment.succeeded" | "payment.failed" | ...
// event.data → the payment / refund / payout / ... itself
// event.created → millisecond timestamp
res.sendStatus(204);
} catch (err) {
if (err instanceof WebhookVerificationError) res.sendStatus(400);
else throw err;
}
});The verifier checks both the HMAC-SHA256 MAC over ${t}.${payload}
and that the timestamp is within tolerance seconds of the current
time (default 300). Pass tolerance: 0 to disable the timestamp
check entirely (useful for offline replay tooling).
Per-request options
Every resource method accepts a second options argument:
await client.payments.create(params, {
idempotencyKey: "order_42", // override the auto-generated key
timeoutMs: 10_000, // abort if the request takes longer
signal: controller.signal, // pair with your own AbortController
});Configuration
new Snap({
apiKey: "sk_live_...",
baseUrl: "https://api.snap.example", // or set SNAP_API_URL - no baked-in default
fetch: customFetch, // override globalThis.fetch
appInfo: { name: "my-store", version: "2.1" } // appended to User-Agent
});License
MIT
