@paysmith/sdk
v0.0.2
Published
Paysmith JavaScript SDK — server client for sandbox payment intents, receipts and entitlements, plus browser-safe checkout helpers that never hold a token.
Maintainers
Readme
@paysmith/sdk
The Paysmith JavaScript SDK, split into two entry points with a hard boundary between them:
@paysmith/sdk/server holds the sandbox token and talks to the Paysmith control plane;
@paysmith/sdk/browser never holds a token and only ever calls your own application's endpoints.
There is no default export worth importing — always pick /server or /browser explicitly.
All sandbox activity behind this SDK is fake money: a deterministic $100.00 balance that expires
72 hours after activation. Every server-side write is idempotent by default (see below).
Install
pnpm add @paysmith/sdk
# or
npm i @paysmith/sdkQuick start
Server (@paysmith/sdk/server)
Server-only. Importing this module in a browser bundle throws immediately at import time — it never gets the chance to leak the sandbox token.
// PAYSMITH_API_BASE and PAYSMITH_SANDBOX_TOKEN come from your sandbox activation
import { createPaysmithServerClient } from "@paysmith/sdk/server";
const client = createPaysmithServerClient(); // reads both env vars automatically
const intent = await client.createPaymentIntent({
environment: "sandbox",
amount: { value: "1.00", currency: "USD" },
product: { reference: "paysmith-demo-lifetime-access", name: "Lifetime access" },
customer: { reference: "demo-browser-session" },
metadata: { entitlement: "demo.premium" },
});
const confirmed = await client.confirmTestPayment(intent.payment_intent_id, "succeeds");
const entitlement = await client.getEntitlement("demo-browser-session", "demo.premium");Environment variables:
| Variable | Purpose |
| --- | --- |
| PAYSMITH_API_BASE | Base URL of your Paysmith sandbox API. Must be https://, unless it is localhost / 127.0.0.1. |
| PAYSMITH_SANDBOX_TOKEN | The short-lived scoped token issued for the sandbox. |
Both can also be passed explicitly as apiBase / clientToken to createPaysmithServerClient(options),
which take priority over the environment variables. A non-loopback http:// base throws — over
plaintext, an attacker on the network path could substitute the sandbox signing key and forge unlock
events.
Every write (createPaymentIntent, confirmTestPayment) automatically sends an Idempotency-Key
header. Pass one explicitly via { idempotencyKey } to control retries yourself; otherwise it is
derived deterministically from the method, path, and body, so an identical retry is naturally safe.
Pull mode
pullEvents is transport only — it fetches signed event envelopes your webhook route might never
receive (e.g. a localhost app the control plane cannot reach) but does not verify them. Run each
one through @paysmith/webhook's verifyPaysmithEvent yourself, exactly as you would for a webhook
delivery; downstream processing is deduped by event_id, so polling overlap with webhook delivery is
safe.
import { createPaysmithServerClient } from "@paysmith/sdk/server";
import { verifyPaysmithEvent } from "@paysmith/webhook";
const client = createPaysmithServerClient();
const publicKey = await client.getSandboxPublicKey();
let after = 0;
const { events, latestId } = await client.pullEvents({ after, limit: 50 });
for (const event of events) {
const result = verifyPaysmithEvent({
rawBody: event.rawBody,
headers: event.headers,
publicKeyPem: publicKey.public_key_pem,
});
if (result.ok) {
// hand result.event to the same verify -> dedupe -> grant pipeline a webhook route uses
}
}
after = latestId;Browser (@paysmith/sdk/browser)
Browser-safe. These helpers call only your own application's routes — never Paysmith directly, and never with a sandbox token.
import { startCheckout, confirmScenario, pollEntitlement } from "@paysmith/sdk/browser";
const { payment_intent_id } = (await startCheckout("/api/paysmith/checkout")) as {
payment_intent_id: string;
};
await confirmScenario("/api/paysmith/checkout/confirm", payment_intent_id, "succeeds");
const entitlement = await pollEntitlement("/api/paysmith/entitlement");
// entitlement.status === "unlocked" once your webhook route has verified the signed eventconfirmScenario's third argument is a SandboxOutcomeScenario:
"succeeds" | "declined" | "delayed" | "duplicate_webhook" | "refund_after_success".
Neither startCheckout nor confirmScenario tells you the payment outcome — their responses come
from your checkout/confirm routes, which themselves only start or nudge a sandbox payment intent.
The only trustworthy signal is what pollEntitlement eventually reports, because that reflects your
webhook route's verified state, not a client-side guess.
API
@paysmith/sdk/server
createPaysmithServerClient(options?: PaysmithServerClientOptions): PaysmithServerClient—options:{ apiBase?, clientToken?, fetchImpl? }.PaysmithServerClientmethods:createPaymentIntent(request: CreatePaymentIntentRequest, options?: WriteOptions): Promise<CreatePaymentIntentResponse>confirmTestPayment(intentId: string, scenario: SandboxOutcomeScenario, options?: WriteOptions): Promise<ConfirmTestPaymentResult>getReceipt(id: string): Promise<ReceiptWithVerification>getEntitlement(subject: string, entitlementKey?: string): Promise<Entitlement>getSandbox(): Promise<SandboxStatus>getSandboxPublicKey(): Promise<SandboxPublicKey>pullEvents(options?: { after?: number, limit?: number }): Promise<{ events: Array<{ id: number, rawBody: string, headers: Record<string, string> }>, latestId: number }>
assertSecureApiBase(apiBase: string): void— throws unlessapiBaseishttps://or loopback.PaysmithApiError— thrown on any non-2xx response; carriesstatus,code, andrequestId.
@paysmith/sdk/browser
startCheckout(appCheckoutUrl: string): Promise<unknown>confirmScenario(appConfirmUrl: string, paymentIntentId: string, scenario: SandboxOutcomeScenario): Promise<unknown>pollEntitlement(appEntitlementUrl: string, options?: PollEntitlementOptions): Promise<EntitlementStatusResponse>—options:{ intervalMs? (default 1000), timeoutMs? (default 30000) }; rejects if the timeout elapses beforestatusbecomes"unlocked".
How it fits
@paysmith/sdk/server is how your backend turns a product into a payment intent and, once
@paysmith/webhook has verified the resulting signed event, looks up the receipt it
produced. @paysmith/sdk/browser is how your frontend drives that flow and observes the resulting
entitlement — without ever seeing the sandbox token that makes any of it authoritative.
License
MIT
