SERVER SDK

Verify & apply offers

@boolbul/sdk/server runs on your backend (Node 18+, Bun, Deno, Cloudflare Workers, Vercel Edge). It has zero dependencies and talks to your gateway directly — BoolBul never sees your gateway keys.

How trust works

  1. The widget asks BoolBul's edge for a session; the customer picks an offer.
  2. The edge signs an offerJwt with your signing secret: subscription, offer terms, single-use nonce, 5-minute expiry.
  3. Your server verifies it, checks the subscription belongs to the logged-in user, and only then calls Dodo or Stripe.
A customer can't forge or enlarge an offer: changing any byte breaks the signature, and rescues are only counted after a successful edge claim.

One-call integration

Dodo Payments
// app/api/boolbul/apply/route.ts  (Next.js; same idea in Express, Hono, Remix...)
import { redeemOffer } from "@boolbul/sdk/server";

const gateway = { gateway: "dodo_payments", apiKey: process.env.DODO_PAYMENTS_API_KEY!, environment: "live_mode" } as const;

export async function POST(req: Request) {
  const user = await requireUser(req); // YOUR auth: never trust ids from the body
  const { offerJwt } = await req.json();

  await redeemOffer(offerJwt, {
    signingSecret: process.env.BOOLBUL_SIGNING_SECRET!, // Dashboard -> Settings
    expected: { subscriptionId: user.subscriptionId },
    // Optional replay guard: return true if this nonce was already used
    seenNonce: async (nonce) => !(await redis.set(`boolbul:${nonce}`, 1, { nx: true, ex: 300 })),
    gateway,
  });
  return Response.json({ ok: true });
}

// app/api/boolbul/cancel/route.ts
import { cancelSubscription } from "@boolbul/sdk/server";

export async function POST(req: Request) {
  const user = await requireUser(req);
  const { reasonId, comment } = await req.json();
  // Cancels at period end and records the exit reason in your gateway's churn analytics
  await cancelSubscription(user.subscriptionId, gateway, { reasonId, comment });
  return Response.json({ ok: true });
}
Stripe (only the gateway line changes)
const gateway = { gateway: "stripe", secretKey: process.env.STRIPE_SECRET_KEY! } as const;

What each gateway does

ActionDodo PaymentsStripe
Pause N daysMoves next_billing_date N days later (no charge, access kept, nothing to resume). Opt into Dodo's native status: "paused" with pauseStrategy: "status_paused" and call resumeDodoSubscription later.pause_collection with behavior: void and resumes_at — auto-resumes, access kept.
Discount X% for M monthsCreates a single-use, product-restricted code (subscription_cycles: M) and re-applies the same product via Change Plan with do_not_bill.Creates a single-use coupon (repeating, duration_in_months: M) and adds it to discounts, keeping existing ones.
Cancelcancel_at_next_billing_date + cancellation_feedback + comment (or immediate).cancel_at_period_end + cancellation_details.feedback + comment (or immediate).
Dodo has no direct “add discount to subscription” endpoint yet, so the discount path uses Change Plan on the same product. Try it once with a test_mode subscription before going live.

Lower-level helpers

import {
  verifyOffer,                    // (jwt, secret, { expected, seenNonce }) -> VerifiedOffer
  applyDodoOffer, applyStripeOffer,
  pauseDodoSubscription, resumeDodoSubscription, applyDodoDiscount, cancelDodoSubscription,
  pauseStripeSubscription, applyStripeDiscount, cancelStripeSubscription,
  OfferVerificationError, GatewayError,
} from "@boolbul/sdk/server";

OfferVerificationError.code is one of malformed, bad_signature, expired, invalid_claims, identity_mismatch, replayed. GatewayError carries the gateway's HTTP status and body.

Your own billing webhooks

Keep your existing Dodo / Stripe webhooks as the source of truth for access. Paused and discounted subscriptions emit their normal events (subscription.updated on Dodo, customer.subscription.updated on Stripe), so no BoolBul-specific webhook handling is required.