snap-payments-sdk
v1.0.0
Published
Official TypeScript SDK for the SNAP payments API — Somalia's developer-first payment infrastructure.
Maintainers
Readme
snap-payments-sdk
Official TypeScript SDK for the SNAP payments API. Isomorphic — runs in Node 18+, modern browsers, Bun, Deno, and edge runtimes.
Package name note. The
@snapnpm scope is owned by Snap Inc., so we publish as the unscopedsnap-payments-sdk. If we later claim a SNAP-owned scope (e.g.@snap-payments) the unscoped name stays available as a compat alias.
Install
npm install snap-payments-sdk
# or
bun 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.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, scope? }, options?)
client.revenue.listSchedules(params?, options?)
client.revenue.retrieveSchedule(id, options?)
client.reports.get({ name, rangeStart, rangeEnd, format? }, options?)
client.radar.createRule(params, options?)
client.radar.listRules({ active? }, options?)
client.radar.archiveRule(id, options?)
client.radar.listEvaluations({ action?, limit? }, options?)
client.radar.approveEvaluation(id, options?)
client.radar.rejectEvaluation(id, 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.object → Payment / Refund / CheckoutSession
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
