JSPM

@daftarhq/sdk

6.1.5
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 45
  • Score
    100M100P100Q94226F
  • License MIT

Daftar Billing Platform SDK for Node.js/TypeScript

Package Exports

  • @daftarhq/sdk

Readme

Daftar Billing SDK

The official Node.js/TypeScript SDK for integrating your SaaS application with the Daftar billing platform.

Installation

Install from npm:

npm install @daftarhq/sdk

Quick Start

import { DaftarClient } from "@daftarhq/sdk";

// Initialize the client with your API key
const daftar = new DaftarClient({
  apiKey: "sk_your_api_key_here",
  baseUrl: "https://daftarhq.com/api",
});

// Create a customer with external ID (your system's user ID)
const customer = await daftar.customers.create({
  externalId: "user_123", // Your system's user ID
  name: "Acme Corporation",
});

// Your app chooses the market key. Daftar does not infer geography.
const marketKey = "saudi-arabia";

// Fetch the checkout-ready catalog for this exact plan and market.
const catalog = await daftar.pricing.getPlanCatalog("pro", {
  marketKey,
  billingPeriod: "monthly",
});
if (!catalog.marketPrice) throw new Error("Monthly price is not configured");

// Resolve the customer's wallet in the selected market currency.
const wallet = await daftar.wallets.getOrCreate(customer.id, catalog.marketPrice.currency);

// Top up the wallet
await wallet.topUp({
  amount: 1000, // decimal amount in catalog.marketPrice.currency
  reference: "Initial deposit",
  idempotencyKey: "deposit_user_123_initial",
});

// Grant admin credit without treating it as a customer payment.
await wallet.grantCredit({
  amount: 25,
  reference: "Goodwill credit",
  idempotencyKey: "manual_credit_user_123_001",
});

// Subscribe to a plan
const subscription = await daftar.subscriptions.create({
  customerExternalId: "user_123",
  planId: "pro",
  marketKey,
  billingPeriod: "monthly",
  currentPeriodStart: new Date(),
  currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
  contract: {
    basePrice: 700,
    notes: "Enterprise launch contract",
    meterOverrides: [
      {
        eventId: "api_calls",
        pricingModel: "per_unit",
        unitPrice: 0.01,
        includedUnits: 50000,
        chargeOverage: true,
        limitType: "event_quota",
        eventQuotaWindow: "billing_period",
      },
    ],
  },
});

// Track usage with idempotency
await daftar.usage.create({
  customerExternalId: "user_123",
  eventId: "api_calls",
  value: 1000,
  idempotencyKey: "usage_user_123_2024_01_15_batch_1",
});

Stable Keys vs UUIDs

Daftar stores UUIDs internally and still returns them for audit/debugging, but project-scoped SDK calls should use stable keys:

  • customerExternalId: your app's user/account ID.
  • planId: the stable plan key, such as free, pro, or enterprise.
  • marketKey: the market selected by your app, such as global or saudi-arabia.
  • eventId: the stable meter key, such as api_calls.
const catalog = await daftar.pricing.getPlanCatalog("pro", {
  marketKey: "global",
  billingPeriod: "monthly",
});

await daftar.subscriptions.create({
  customerExternalId: "user_123",
  planId: "pro",
  marketKey: "global",
  billingPeriod: "monthly",
  currentPeriodStart: new Date(),
  currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});

await daftar.usage.create({
  customerExternalId: "user_123",
  eventId: "api_calls",
  value: 1000,
});

Authentication

The SDK uses API key authentication (Bearer token). You can create API keys through the Daftar dashboard:

// The SDK automatically handles the Authorization header
const client = new DaftarClient({
  apiKey: "sk_live_abcdef1234567890abcdef1234567890",
});

Project Scoping (projectId)

Daftar API keys are scoped to a single project.

  • For project-scoped SDK calls, the backend can infer projectId from the API key, so projectId is optional in the request.
  • If you do provide projectId, it must match the API key's project, otherwise the backend returns 403.

Project Configuration

Project creation, balance/market creation, jobs, dashboard, and API key management are session-only (cookie auth) for the admin dashboard. This SDK is intentionally API-key only, so those endpoints are not available via the SDK.

Read-only market and balance discovery is available through the SDK:

const { data: balances } = await daftar.balances.list(projectId);
const { data: markets } = await daftar.markets.list(projectId);

const market = markets.find((item) => item.name === "GCC" && item.currency === "USD");

Plan Prices and Meter Pricing

Usage pricing is scoped to the exact plan price customers subscribe to. Before showing checkout, fetch the catalog for the market your app selected:

const { data: catalogs, unavailable } = await daftar.pricing.listPlanCatalogs({
  marketKey: "global",
  billingPeriod: "monthly",
  limit: 20,
});

for (const catalog of catalogs) {
  if (!catalog.marketPrice) continue;
  console.log(catalog.plan.name, catalog.marketPrice.currency, catalog.marketPrice.basePrice);
}

if (unavailable.length > 0) {
  console.warn(
    "Some plans are not configured for this market:",
    unavailable.map((item) => item.plan.planId),
  );
}

For a single selected plan:

const catalog = await daftar.pricing.getPlanCatalog(plan.id, {
  marketKey: "global",
  billingPeriod: "monthly",
});
if (!catalog.marketPrice) throw new Error("Select a billing period");

console.log(catalog.marketPrice.currency, catalog.marketPrice.basePrice);
for (const meterPrice of catalog.meterPrices) {
  console.log(meterPrice.meterEventId, meterPrice.includedUnits);
}

When configuring a plan, create or resolve the plan price first, then attach meter pricing with planPriceId:

const planPrice = await daftar.pricing.addPriceToPlan(plan.id, {
  marketKey: "global",
  basePrice: 15,
  billingPeriod: "monthly",
});

const ksaPrice = await daftar.pricing.addPriceToPlan(plan.id, {
  marketKey: "ksa",
  basePrice: 55,
  billingPeriod: "monthly",
  copyMeterPricingFromPlanPriceId: planPrice.id,
});

await daftar.pricing.addToPlan(plan.id, {
  marketKey: "global",
  billingPeriod: "monthly",
  eventId: "api_calls",
  pricingModel: "per_unit",
  unitPrice: 0.02,
  includedUnits: 10000,
  chargeOverage: true,
  limitType: "event_quota",
  eventQuotaWindow: "billing_period",
});

Core Features

🔗 Fluent API

The SDK provides a fluent API where resource objects have methods to perform related operations:

// Create a customer
const customer = await daftar.customers.create({
  externalId: "user_123",
  name: "Acme Corp",
});

// Subscribe using fluent API
const subscription = await customer.subscribe({
  planId: "pro",
  marketKey: "global",
  billingPeriod: "monthly",
  contract: {
    basePrice: 700,
    notes: "Negotiated terms at subscription creation",
    meterOverrides: [
      {
        eventId: "api_calls",
        pricingModel: "per_unit",
        unitPrice: 0.01,
        includedUnits: 50000,
        chargeOverage: true,
        limitType: "event_quota",
        eventQuotaWindow: "billing_period",
      },
    ],
  },
});

// Track usage using fluent API
const usage = await customer.recordUsage({
  eventId: "api_calls",
  value: 1000,
});

// Get related data
const invoices = await customer.getInvoices();
const subscriptions = await customer.getSubscriptions();

// Resource methods on subscription
await subscription.cancel({ cancelAtPeriodEnd: true });
const planChange = await subscription.changePlan({
  newPlanId: "enterprise",
  billingPeriod: "monthly",
  timing: "immediate",
});
// Immediate same-period upgrades first charge any positive net adjustment from
// the customer's wallet. A low wallet balance returns code=insufficient_wallet_balance.
if (planChange.scheduledChange?.reason === "billing_period_change") {
  console.log("Plan changes at renewal:", planChange.scheduledChange.effectiveAt);
}

// Resource methods on wallet
const wallet = await daftar.wallets.getOrCreate(customer.id, "SAR");
await wallet.topUp({ amount: 100, idempotencyKey: "topup_user_123_001" });
await wallet.grantCredit({
  amount: 25,
  reference: "Goodwill credit",
  idempotencyKey: "manual_credit_user_123_001",
});
const transactions = await wallet.getTransactions();

🎯 Usage Tracking

Track customer usage for billing and analytics:

// Single usage record
await client.usage.create({
  customerExternalId: "user_123",
  eventId: "api_calls",
  value: 1000,
  timestamp: "2024-01-15T10:00:00Z",
});

// Batch usage records
await client.usage.createBatch([
  {
    customerExternalId: "user_123",
    eventId: "api_calls",
    value: 500,
  },
  {
    customerExternalId: "user_123",
    eventId: "seats",
    value: 10,
  },
]);

// Get usage history
const usage = await client.usage.getCustomerUsage("00000000-0000-0000-0000-000000000000", {
  startDate: "2024-01-01",
  endDate: "2024-01-31",
  limit: 100,
});

// Get aggregated usage statistics
const stats = await client.usage.getAggregatedUsage("00000000-0000-0000-0000-000000000000", {
  startDate: "2024-01-01",
  endDate: "2024-01-31",
});

🚦 Access Gating

Check whether a customer can use a feature or metered action:

const decision = await client.access.check({
  customerExternalId: "user_123",
  feature: "api_calls",
  quantity: 1,
});

if (!decision.allowed) {
  console.log(`Blocked by billing: ${decision.reason}`);
}

The same check is available from a customer resource:

const decision = await customer.can("api_calls", { quantity: 1 });

Build support dashboards or customer-facing usage pages from the pinned plan-version state:

const state = await client.access.getCustomerState("user_123");

for (const entitlement of state.entitlements) {
  console.log(`${entitlement.feature}: ${entitlement.used}/${entitlement.limit ?? "metered"}`);
}

For high-volume request paths, store access state locally and update it from Daftar webhooks such as usage.limit_reached, usage.limit_reset, subscription.past_due, subscription.recovered, and customer.access_changed.

Webhooks

Daftar sends webhooks as a stable envelope with an event-specific data object:

{
  "id": "evt_123",
  "event": "usage.limit_reached",
  "createdAt": "2026-05-15T12:00:00.000Z",
  "data": {
    "customerId": "cus_123",
    "subscriptionId": "sub_123",
    "feature": "api_calls",
    "meterId": "mtr_123",
    "used": 5000,
    "limit": 5000,
    "remaining": 0,
    "overageAllowed": false,
    "limitType": "event_quota",
    "eventQuotaWindow": "billing_period",
    "resetAt": "2026-06-15T12:00:00.000Z"
  }
}

Verify the signature against the raw request body before parsing JSON:

import {
  parseAndValidateWebhookPayload,
  toWebhookUpdate,
  verifyWebhookSignature,
  type DaftarWebhookPayload,
  type WebhookUpdate,
} from "@daftarhq/sdk";

// Express example: mount this route with express.raw({ type: "application/json" }).
app.post("/webhooks/daftar", express.raw({ type: "application/json" }), async (req, res) => {
  const payload = req.body.toString("utf8");
  const signature = req.header("X-Webhook-Signature");

  if (
    !verifyWebhookSignature({
      payload,
      signature,
      secret: process.env.DAFTAR_WEBHOOK_SECRET!,
    })
  ) {
    return res.status(400).send("Invalid signature");
  }

  const event = parseAndValidateWebhookPayload(payload);
  await handleDaftarWebhook(event);
  res.sendStatus(204);
});

async function handleDaftarWebhook(event: DaftarWebhookPayload) {
  const update = toWebhookUpdate(event);
  await applyWebhookUpdate(update);
}

async function applyWebhookUpdate(update: WebhookUpdate) {
  if (update.action === "block_access") {
    await blockAccessLocally(update.customerId!, update.feature, update.reason);
  }
  if (update.action === "allow_access" || update.action === "refresh_entitlements") {
    await refreshEntitlements(update.customerId!);
  }
}

The SDK exports WEBHOOK_EVENTS, per-event runtime schemas through webhookPayloadSchemas, strict helpers such as validateWebhookPayload / parseAndValidateWebhookPayload, and payload types such as UsageLimitReachedWebhookData. Runtime schemas require documented fields and allow additive fields for forward compatibility. The public developer docs at /docs#webhook-payloads include the full payload reference and examples for every event.

👥 Customer Management

Manage your customers:

// Create customer
const customer = await client.customers.create({
  externalId: "user_123", // Your system's user ID
  name: "John Doe",
  email: "john@example.com",
});

// List customers
const customers = await client.customers.list({
  limit: 50,
  search: "John",
});

// Update customer
await client.customers.update("user_123", {
  name: "John Smith",
});

📋 Subscription Management

Handle customer subscriptions with full lifecycle support:

// Create subscription
const now = new Date();
const subscription = await daftar.subscriptions.create({
  customerExternalId: "user_123",
  planId: "pro",
  marketKey: "global",
  billingPeriod: "monthly",
  currentPeriodStart: now,
  currentPeriodEnd: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000),
  status: "active",
  contract: {
    basePrice: 700,
    notes: "Enterprise launch contract",
    meterOverrides: [
      {
        eventId: "api_calls",
        pricingModel: "per_unit",
        unitPrice: 0.01,
        includedUnits: 50000,
        chargeOverage: true,
        limitType: "event_quota",
        eventQuotaWindow: "billing_period",
      },
    ],
  },
});

// Check subscription status
if (subscription.isActive()) {
  console.log("Subscription is active");
}

if (subscription.isTrialing()) {
  console.log(`Trial ends: ${subscription.trialEnd}`);
}

// Cancel at period end (no refund)
await subscription.cancel({
  cancelAtPeriodEnd: true,
});

// Cancel immediately
await subscription.cancel({
  cancelAtPeriodEnd: false,
});

// Upgrade/downgrade plan. Same-billing-period changes can apply immediately
// with proration after any positive net adjustment is paid from the customer's
// wallet; monthly/yearly switches are scheduled for renewal.
const { proration, scheduledChange } = await subscription.changePlan({
  newPlanId: "enterprise",
  billingPeriod: "monthly",
  timing: "immediate",
});
if (proration) {
  console.log(
    `Net adjustment: ${proration.netAdjustment} (refund=${proration.prepaidRefund}, charge=${proration.newPlanCharge}, usage=${proration.usageCharges})`,
  );
}
if (scheduledChange) {
  console.log(`Plan changes at renewal: ${scheduledChange.effectiveAt}`);
}

// Inspect subscription-specific custom contract history, if the subscription
// was created with contract terms.
const contractHistory = await subscription.listContracts();

// Inspect the effective signed terms used by billing and access checks.
const terms = await subscription.getResolvedTerms();
console.log(terms.basePrice.amount, terms.basePrice.source);

// List customer subscriptions
const subs = await daftar.subscriptions.listForCustomer("00000000-0000-0000-0000-000000000000");

💳 Wallet Operations

Manage customer wallets with idempotent operations:

const wallet = await daftar.wallets.getOrCreate("00000000-0000-0000-0000-000000000000", "SAR");

// Top up wallet with idempotency key (prevents duplicate charges)
const { transaction, _idempotent } = await wallet.topUp({
  amount: 500, // decimal amount in this wallet currency
  reference: "Payment via bank transfer",
  idempotencyKey: "payment_inv_123",
});

if (_idempotent) {
  console.log("This top-up was already processed");
}

// Grant admin credit separately from customer-paid deposits.
await wallet.grantCredit({
  amount: 50,
  reference: "SLA credit",
  idempotencyKey: "manual_credit_sla_123",
});

// Check balance
console.log(`Current balance: ${wallet.balance}`);

// Refresh balance from server
await wallet.refreshBalance();

// Check if sufficient for a purchase
if (wallet.hasSufficientBalance(100)) {
  console.log("Can afford the purchase");
}

// Get transaction history
const { data: transactions, total } = await wallet.getTransactions({
  limit: 50,
});

📄 Invoice Management

Generate and manage invoices:

// Create invoice
const invoice = await client.invoices.create({
  customerId: "00000000-0000-0000-0000-000000000000",
  periodStart: "2024-02-01",
  periodEnd: "2024-02-29",
  subtotal: 99,
  total: 99,
  dueDate: "2024-02-15",
  status: "open",
  type: "manual",
});

// Add line items
await client.invoices.addLineItem(invoice.id, {
  description: "Pro Plan - Monthly",
  quantity: 1,
  unitPrice: 99,
  amount: 99,
});

// Send invoice using resource method
await invoice.send();

// Pay invoice from customer's wallet
await invoice.pay();

// Get customer's invoices using fluent API
const customer = await client.customers.get("00000000-0000-0000-0000-000000000000");
const invoices = await customer.getInvoices({ limit: 10 });

High-Throughput Usage Tracking

For multi-instance deployments (e.g., Kubernetes), use the UsageBuffer for efficient batching:

import { DaftarClient, UsageBuffer } from "@daftarhq/sdk";

const daftar = new DaftarClient({ apiKey: "sk_..." });

// Create a buffer that batches events
const usageBuffer = new UsageBuffer(daftar, {
  maxBufferSize: 100, // Flush when 100 events buffered
  flushIntervalMs: 5000, // Or every 5 seconds
  maxRetries: 3, // Retry failed flushes
  instanceId: process.env.POD_NAME, // K8s pod name for deduplication
  onFlushError: (err, events) => {
    console.error(`Failed to flush ${events.length} events:`, err);
  },
});

// Track usage (non-blocking, buffers locally)
usageBuffer.track({
  customerExternalId: "user_123",
  eventId: "api_calls",
  value: 1,
});

// Graceful shutdown (flushes remaining events)
process.on("SIGTERM", async () => {
  await usageBuffer.shutdown();
  process.exit(0);
});

Redis-Backed Distributed Buffer

For cross-instance deduplication before hitting the API:

import { DaftarClient, RedisUsageBuffer } from "@daftarhq/sdk";
import Redis from "ioredis";

const redis = new Redis();
const daftar = new DaftarClient({ apiKey: "sk_..." });

const usageBuffer = new RedisUsageBuffer(daftar, {
  redis: {
    setnx: (k, v) => redis.setnx(k, v).then((r) => r === 1),
    expire: (k, s) => redis.expire(k, s).then(() => {}),
    lpush: (k, v) => redis.lpush(k, v).then(() => {}),
    lrange: (k, s, e) => redis.lrange(k, s, e),
    ltrim: (k, s, e) => redis.ltrim(k, s, e).then(() => {}),
  },
  keyPrefix: "myapp:billing:",
  idempotencyTtlSeconds: 86400, // 24 hours
});

// Returns false if duplicate (already tracked by another instance)
const isNew = await usageBuffer.trackWithDedup({
  customerExternalId: "user_123",
  eventId: "api_calls",
  value: 1,
});

Integrity Guarantees

Layer Mechanism Protection
SDK Auto-generated idempotency keys Instance-level deduplication
SDK + Redis SETNX before buffering Cross-instance deduplication
Server DB unique index on (customer_id, idempotency_key) Final guarantee

Error Handling

The SDK provides comprehensive error handling:

import { DaftarClient, DaftarApiError } from "@daftarhq/sdk";

try {
  await client.customers.get("invalid-id");
} catch (error) {
  if (error instanceof DaftarApiError) {
    console.error("API Error:", error.error.message);
    console.error("Status Code:", error.statusCode);
    console.error("Error Details:", error.error.errors);
  } else {
    console.error("Unexpected error:", error);
  }
}

Advanced Configuration

const client = new DaftarClient({
  apiKey: "your-api-key",
  baseUrl: "https://daftarhq.com/api",
  timeout: 15000, // Request timeout in ms
  retries: 3, // Number of retry attempts
});

Retries cover network failures, 5xx responses, and 429 rate-limit responses. When the API returns Retry-After, the SDK waits for that duration before retrying.

TypeScript Support

The SDK is fully typed with TypeScript. All methods have proper type definitions:

import { Customer, CreateCustomerRequest } from "@daftarhq/sdk";

const customerData: CreateCustomerRequest = {
  externalId: "user_123",
  name: "Acme Corp",
};

const customer: Customer = await client.customers.create(customerData);

Examples

Check out the examples directory for more comprehensive usage examples:

Development

# Install dependencies
npm install

# Build the SDK
npm run build

# Run tests
npm test

# Start development mode with watch
npm run dev

API Reference

For detailed API documentation, visit Daftar API Docs.

Support

License

MIT © Daftar