JSPM

@opensettle/sdk

0.2.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 653
  • Score
    100M100P100Q73375F
  • License MIT

Official Node SDK for the OpenSettle API. Stablecoin billing on Base, Ethereum, Polygon, Arbitrum, Solana, and Tron.

Package Exports

  • @opensettle/sdk
  • @opensettle/sdk/errors
  • @opensettle/sdk/webhooks

Readme

@opensettle/sdk

Official Node SDK for the OpenSettle API. Stablecoin billing on Base, Ethereum, Polygon, and Arbitrum.

npm types node

Status: 0.1.x — initial release. API surface is stable; we'll bump to 1.0.0 once it's been used in real merchant integrations for a quarter without breaking changes. Source at github.com/OpenSettle/opensettle-sdk-js; for urgent issues email opensettle@proton.me.

Install

npm install @opensettle/sdk
# or
pnpm add @opensettle/sdk
# or
yarn add @opensettle/sdk

Requires Node 20+ (uses built-in fetch and node:crypto).

Quickstart

import { OpenSettle } from "@opensettle/sdk";

const os = new OpenSettle({
  apiKey: process.env.OPENSETTLE_KEY!,        // sk_live_… or sk_test_…
  workspaceId: process.env.OPENSETTLE_WORKSPACE!,
});

// Create a customer
const customer = await os.customers.create({
  email: "ada@example.com",
  name: "Ada Lovelace",
});

// Bill them with a one-shot invoice paid in USDC on Base.
// Total is derived from `lineItems[].unitAmountMinor * quantity` —
// there is no top-level `amountMinor`. `currency` defaults to "USD".
const invoice = await os.invoices.create({
  customerId: customer.id,
  chain: "base",
  token: "USDC",
  lineItems: [
    { description: "Pro plan", quantity: 1, unitAmountMinor: 19_900 }, // $199.00 — minor units (cents)
  ],
  dueInDays: 14,                               // optional, default 14
});

await os.invoices.send(invoice.id);            // emails the customer the link

Configuration

new OpenSettle({
  apiKey: "sk_live_…",                          // required
  workspaceId: "ws_01HG…",                      // required
  baseUrl: "https://api.opensettle.io",         // optional override
  testMode: process.env.NODE_ENV !== "production", // refuses sk_live_ when true
  timeoutMs: 30_000,                            // per-request timeout
  maxNetworkRetries: 3,                         // retries on 5xx + 429 + network errors
  fetch: customFetch,                           // override the global fetch (rare)
});

The SDK refuses to send live traffic on a test key (and vice versa) when testMode is set explicitly — useful as a CI circuit breaker.

Errors

Every non-2xx response throws a typed subclass of OpenSettleError. Catchers can branch on either the class or the stable code property.

import {
  OpenSettleError,
  RateLimitError,
  SettlementError,
  StepUpRequiredError,
} from "@opensettle/sdk";

try {
  await os.payments.refund("pay_1", { amountMinor: 100 });
} catch (err) {
  if (err instanceof RateLimitError) {
    await sleep((err.retryAfter ?? 1) * 1000);
    return retry();
  }
  if (err instanceof StepUpRequiredError) {
    return promptUserToReauthenticate();        // refunds need AAL=2
  }
  if (err instanceof SettlementError && err.code === "insufficient_confirmations") {
    return tryAgainInAFewBlocks();
  }
  if (err instanceof OpenSettleError) {
    log.error({ code: err.code, requestId: err.requestId }, err.message);
  }
  throw err;
}
Class HTTP Error code(s)
InvalidRequestError 400 invalid_request
InvalidStateTransitionError 422 invalid_state_transition
AuthenticationError 401 unauthorized
ForbiddenError 403 forbidden
NotFoundError 404 not_found
ConflictError 409 conflict
RateLimitError 429 rate_limited (carries retryAfter)
SettlementError 422 chain_reverted, insufficient_confirmations, signing_required
StepUpRequiredError 401 aal_required
APIError 5xx internal_error (and unknown future codes)
NetworkError network_error (request never landed)

Every error carries code, status, message, requestId, and optionally param for field-level errors. requestId is the value to quote in support.

Idempotency

The SDK auto-attaches an Idempotency-Key to every money-adjacent write (create + refund + send + rotate). Pass an explicit key when you have one tied to your domain object — that's safer because retries from your own systems won't generate a fresh key:

await os.checkouts.create({
  mode: "payment",
  customerId: "cus_1",
  invoiceId: "inv_1",
  successUrl: "https://example.com/done",
}, { idempotencyKey: `checkout-${orderId}` });

(The SDK's resource methods construct the key automatically; pass-through is exposed via os.http.request(...) for advanced use.)

Webhooks

Verify signed deliveries from webhook_endpoints with the verifyWebhook helper — it checks the HMAC-SHA256 in constant time and rejects stale timestamps:

import { verifyWebhook, WebhookVerificationError } from "@opensettle/sdk";

app.post("/webhook", async (req, res) => {
  try {
    const { data } = verifyWebhook<{ id: string; type: string }>({
      rawBody: req.rawBody,                     // exact bytes received
      signatureHeader: req.header("x-opensettle-signature"),
      secret: process.env.WEBHOOK_SECRET!,
    });
    if (data.type === "payment.confirmed") {
      // ship the goods, mark the order paid
    }
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      // err.reason: "missing_header" | "malformed_header"
      //           | "stale_timestamp" | "signature_mismatch"
      //           | "invalid_body"
      return res.status(400).end(err.reason);
    }
    throw err;
  }
});

The signature format is t=<unix>,v1=<hex_hmac_sha256> over ${t}.${rawBody}. Default tolerance is 5 minutes; pass tolerance: <seconds> to override (or 0 to disable, only in test code).

Capture the raw body. Frameworks that JSON-parse before your handler sees it (Express body-parser default, etc.) destroy the original bytes — you'll get spurious signature_mismatch errors. Configure raw-body access on the webhook route only.

Resources

  • os.customerslist, retrieve, create, update, del
  • os.productslist, retrieve, create, update, delete, listPrices, createPrice, deletePrice
  • os.invoiceslist, retrieve, create, send, remind, void
  • os.checkoutscreate, retrieve
  • os.subscriptionslist, retrieve, create, pause, resume, cancel, changePlan
  • os.paymentslist, retrieve, refund, refundBroadcast
  • os.webhookEndpointslist, retrieve, create, update, del, rotateSecret, test

Each method returns the typed resource. Refer to the API reference for the full field set per resource.

Test mode

Use a sk_test_… key against the same hostname — there is no separate test host. Test-mode wallets, customers, and payments live in their own scope and don't bleed into live data.

const os = new OpenSettle({
  apiKey: process.env.OPENSETTLE_TEST_KEY!,
  workspaceId: process.env.OPENSETTLE_WORKSPACE!,
  testMode: true,                               // assert this is a test key
});

Non-custodial settlement

OpenSettle never holds customer or merchant funds — payments transfer directly from the customer's wallet to your settlement wallet on-chain. Our platform fee is accrued separately and billed once a month. The SDK reflects that: payments.refund() returns an unsigned-tx envelope your wallet signs and broadcasts; we never have keys that can move funds.

See the security architecture docs for the full posture.

License

MIT. See LICENSE.