@skalpgg/sdk
v1.1.0
Published
Official Skalp SDK for Node.js and edge runtimes: checkout, subscriptions, webhooks and license verification.
Maintainers
Readme
skalp
The official Skalp SDK for Node.js and edge runtimes: hosted checkout, subscriptions, webhook verification and license checks, with types for every response.
Full documentation with Next.js, Express, NestJS, Hono and Fastify examples: https://skalp.gg/docs/sdk
npm install @skalpgg/sdkRuns on Node 20+, Bun, Deno, Vercel Edge and Cloudflare Workers. No dependencies; ships ESM, CommonJS and types.
Quickstart
SKALP_API_KEY=sk_live_xxx
SKALP_WEBHOOK_SECRET=your_webhook_secretimport { Skalp } from '@skalpgg/sdk'
const skalp = new Skalp()
const session = await skalp.checkout.create({
productId: 'prod_xxx',
customerEmail: '[email protected]',
returnUrl: 'https://example.com/thanks',
})
console.log(session.url)Send the buyer to session.url, then confirm the payment from the order.paid webhook.
new Skalp() reads SKALP_API_KEY and SKALP_WEBHOOK_SECRET. You can also pass them in: new Skalp('sk_live_xxx') or new Skalp({ apiKey, webhookSecret, baseUrl, timeout, maxRetries, fetch }).
Create the key on the API keys page of your dashboard. Each key is bound to one store.
Methods
| Method | Scope |
| --- | --- |
| checkout.create({ productId, customerEmail?, metadata?, couponCode?, returnUrl? }) | checkout:write |
| checkout.get(id) | checkout:read |
| orders.get(id) | orders:read |
| products.list() | products:read |
| subscriptions.createCheckout({ productId \| productSlug, externalRef?, customerEmail?, currency?, ... }) | checkout:write |
| subscriptions.getCheckout(id) | checkout:read |
| subscriptions.list({ externalRef?, status?, updatedSince?, limit?, offset? }) | subscriptions:read |
| subscriptions.get(id) | subscriptions:read |
| subscriptions.changePlan(id, { productId \| productSlug }) | subscriptions:write |
| subscriptions.cancel(id) | subscriptions:write |
| subscriptions.listInvoices(id) | subscriptions:read |
| licenses.verify({ licenseKey, serverIp, serverKey, serverPort? }) | none |
| licenses.heartbeat({ licenseKey, serverIp }) | none |
| webhooks.constructEvent(rawBody, headers) | none |
| webhooks.verify(rawBody, headers) | none |
Webhooks
Pass the raw request body, not parsed JSON, together with the request headers. constructEvent checks the Skalp-Signature header, an HMAC-SHA256 over the timestamp and the exact bytes Skalp sent, and rejects deliveries older than 5 minutes.
export async function POST(request: Request) {
const event = await skalp.webhooks.constructEvent(await request.text(), request.headers).catch(() => null)
if (!event) return new Response('Invalid signature', { status: 400 })
if (event.type === 'order.paid') {
await grantAccess(event.data.buyerEmail, event.data.licenses ?? [])
}
return new Response(null, { status: 200 })
}A webhook with the default events receives every purchase as both order.paid and its legacy name purchase.completed. Handle order.paid and deduplicate on data.idempotencyKey.
Errors
A refusal from the API throws SkalpError with status, code (for example STORE_REQUIRED) and the parsed body. A network failure or a timeout throws SkalpConnectionError, naming the call and keeping the original in cause, so the two are easy to tell apart:
try {
await skalp.orders.get(id)
} catch (error) {
if (error instanceof SkalpError) return reportRefusal(error.code, error.status)
if (error instanceof SkalpConnectionError) return retryLater()
throw error
}A 429 is retried up to maxRetries times (default 2) when the limit resets within 10 seconds. constructEvent throws SkalpSignatureError with reason: 'stale' | 'invalid' — 'stale' means the delivery is outside the 5 minute replay window, 'invalid' means the bytes or the secret do not match.
The constructor throws if it finds an API key in a browser, so the mistake shows up the first time you run the code rather than after a secret key has shipped to users. Pass dangerouslyAllowBrowser: true to override it.
