Introducing BetterPay for Indonesian developers

Payment and billing that lives inside your app.

BetterPay is an open-source TypeScript framework for Indonesian payment gateways, secure webhooks, subscriptions, invoices, entitlements, usage, billing cycles, and provider fallback. Your routes, your database, your server.

$ pnpm add @betterpay/core @betterpay/billing @betterpay/midtrans

Composable, plugin-based, and built for how Indonesian payments actually work.

src/payments.ts
await pay.createTransaction({ amount: 199_000, currency: "IDR" })
BETTERPAY CORE
Amount validation passed
Provider routing selected
Webhook signature verified
Midtrans
Xendit
Duitku
Tripay
Mayar
Pakasir

The problem

Payment code becomes product logic faster than you think.

It starts as one payment link. A few sprints later it is subscriptions, invoices, usage limits, webhook retries, and access control. That layer deserves a framework.

Every provider speaks a different language.

APIs, signature schemes, status codes, and webhook formats differ across Midtrans, Xendit, Duitku, Tripay, Mayar, and Pakasir. Integrating one takes days. Integrating a second one starts over.

Webhooks are fragile.

Callbacks retry, arrive twice, arrive late, or arrive forged. Without verification, replay protection, and state validation, a webhook can corrupt the exact record it was meant to settle.

Indonesian billing is not always auto-debit.

Virtual accounts, QRIS, e-wallets, and retail counters rarely renew themselves. Subscriptions here need invoices, billing cycles, retries, and dunning, not just a charge endpoint.

Provider failure becomes product failure.

When a single gateway degrades, checkout goes down with it, unless your app can prefer, filter, and route around providers.

The signature flow

From payment request to customer access.

BetterPay connects payment events to your business logic. When a customer pays, your app receives a normalized payment state that updates invoices, subscriptions, usage limits, and access.

  1. pay.createTransaction()

    Create transaction

    One call with a validated amount and order. BetterPay selects the provider by priority or payment method.

  2. paymentUrl

    Provider checkout

    The customer pays on the provider's hosted page: Snap, payment link, QRIS, virtual account, e-wallet, or retail counter.

  3. pay.handleWebhook()

    Secure webhook

    The callback hits your route. Signature verified, replay blocked; duplicate events deduped in-memory until you add persistence.

  4. status: "paid"

    Normalized status

    Provider payloads collapse into one status vocabulary, validated against an explicit transaction state machine.

  5. invoice / subscription

    Invoice and subscription

    The paid event settles the invoice and activates the subscription period.

  6. billing.check()

    Entitlement access

    The feature gate flips to allowed: true. Your customer gets what they paid for.

BETTERPAY / EVENT LOGorder_8f2k
createTransaction Rp 199.000pendingpaid
checkout provider hostedopened
incoming webhook
  • signature verified
  • replay blocked
  • in-memory dedupe (add DB for durable)
normalize settlement → paid1 event
INV-2026-0001 settledplan: pro
billing.check({ featureId: "messages" })

allowed: true. Access unlocked.

End-to-end in code

From payment URL to feature access.

One transaction, one webhook handler, one entitlement check. Billing and persistence wiring stay in your repo; defaults can start in-memory for development.

src/checkout.ts
// 1) Create a payment (provider-hosted checkout)
const { paymentUrl } = await pay.createTransaction({
  amount: 199_000,
  currency: "IDR",
  orderId: "order_8f2k",
});

// 2) Webhook on your route (signature + normalized status)
await pay.handleWebhook(request);

// 3) Entitlement after invoice / subscription settles
const access = await pay.billing.check({
  customerId: "user_1",
  featureId: "messages",
});
// → { allowed: true, balance: { remaining: 4999 } }

One composable layer inside your app.

Payments, webhooks, billing cycles, and customer access share one TypeScript API. Jump to what you need:

Supported today

Local gateways plug in. Your API stays createTransaction().

Ship with the Indonesian providers you already use. BetterPay normalizes checkout URLs, webhooks, and status behind one TypeScript surface in your app.

Your app One handler
createTransaction() BetterPay core
6 adapters Local gateways
  • Midtrans
  • Xendit
  • Duitku
  • Tripay
  • Mayar
  • Pakasir

Methods vary by adapter. Pakasir is QRIS-focused in the current package. You keep provider accounts and credentials; BetterPay is not a new payment brand.

The differentiator

Define your billing model in code.

Gateway wrappers stop at the payment link. Plans, subscriptions, invoices, usage, and entitlements stay in your repo as TypeScript you can review and ship.

src/billing.ts
import { billing, feature, plan } from "@betterpay/billing";

const messages = feature({ id: "messages", type: "metered" });

const pro = plan({
  id: "pro",
  name: "Pro",
  price: { amount: 199_000, currency: "IDR", interval: "month" },
  includes: [messages({ limit: 5_000, reset: "month" })],
});

await pay.billing.subscribe({ customerId: "user_1", planId: "pro" });

await pay.billing.check({
  customerId: "user_1",
  featureId: "messages",
});
// → { allowed: true, balance: { remaining: 4999 } }

Primitives

  • plan() / feature()

    Pricing and metered limits live next to the code that uses them.

  • subscription + invoice

    Explicit lifecycle and per-period invoices settled by payment events.

  • check() / report()

    Gate features and meter usage with lazy limit resets.

  • cycles + dunning

    Renewals, retries, and cron helpers for Indonesian billing reality.

The billing plugin ships with in-memory repositories so you can start immediately. For production, wire persistence with the Drizzle adapter or your own repositories.

Webhook reliability

Webhooks should update your state, not corrupt it.

Every callback runs the same pipeline before your business logic ever sees it. These are framework capabilities you configure, not magic that replaces your database and workers.

Incoming callback → verify → normalize → your handler

Callback
Verify
Normalize
Your handler

Verify signature

Provider scheme checked before state updates.

Block replay

Stale callbacks rejected, not re-applied.

Dedupe events

In-memory today; add DB for durable dedupe.

Normalize payload

One status vocabulary across six dialects.

Validate transition

State machines guard illegal changes.

Log & reconcile

Audit hooks; you wire queries and workers.

Developer experience

Built for modern TypeScript backends.

Bring your own framework, database, auth, and deployment. Every handler wraps the same core Request to Response function, so there is no framework lock-in.

app/api/pay/[...all]/route.ts
import { payHandler } from "@betterpay/next";
import { pay } from "@/lib/pay";

export const { GET, POST } = payHandler(pay);
Drizzle adapter

PostgreSQL repositories for transactions and billing.

CLI

betterpay init, status, and encrypted credential commands.

Client SDK

A typed, proxy-based client for your frontend.

Credential encryption

Provider keys stored AES-256-GCM encrypted.

Use cases

For products that need more than a payment button.

SaaS subscriptions

Monthly and yearly IDR plans with subscriptions, invoices, billing cycles, and entitlements.

API products

Meter API calls, messages, credits, or any usage-based limit per customer.

Digital products

Courses, templates, communities, and paid tools sold through local payment methods.

E-commerce checkout

One-time transactions with provider webhooks normalized into your app state.

Agency projects

One reusable payment layer across client apps; swap provider configuration per project.

Provider redundancy

Add fallback providers when your app needs broader payment coverage.

Membership & tools

Seat-based access, credits, and renewals tied to invoices and local payment methods.

Honest scope

A framework you embed. Not another black box.

BetterPay runs inside your application. You own the routes, the database, the deployment, and the payment logic. Checkout itself happens on provider-hosted pages such as Snap or payment links.

BetterPay is

  • An open-source payment and billing framework, MIT licensed
  • A TypeScript API for Indonesian payment workflows
  • A plugin layer for local payment providers
  • A webhook reliability layer
  • A billing cycle and entitlement engine
  • A framework-agnostic backend package

BetterPay is not

  • Not a hosted payment SaaS
  • Not a checkout UI builder today
  • Not a global Stripe replacement
  • Not a marketplace or multi-merchant platform out of the box
  • Not a guaranteed refund API for every local payment method
  • Not a replacement for your payment gateway account

Start with one provider. Grow into billing.

Use one gateway today. Add fallback, subscriptions, invoices, usage, and entitlements when your product needs them, without leaving your codebase.

$ pnpm add @betterpay/core @betterpay/billing @betterpay/midtrans