Payment#

Pluggable payment gateway integration supporting Stripe, Midtrans, Xendit, and PayPal. All providers share a unified PaymentDriver interface with normalized types.

Setup#

Use the createPayment factory to instantiate drivers:

typescript
import { createPayment } from "@buntok/core/payment";

const stripe = createPayment.stripe({
  secretKey: process.env.STRIPE_SECRET_KEY!,
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
});

const midtrans = createPayment.midtrans({
  serverKey: process.env.MIDTRANS_SERVER_KEY!,
  isProduction: false,
});

const xendit = createPayment.xendit({
  secretKey: process.env.XENDIT_SECRET_KEY!,
});

const paypal = createPayment.paypal({
  clientId: process.env.PAYPAL_CLIENT_ID!,
  clientSecret: process.env.PAYPAL_CLIENT_SECRET!,
  mode: "sandbox",
});

Driver Configuration#

DriverConfigRequired Fields
StripeStripeDriverConfigsecretKey
MidtransMidtransDriverConfigserverKey, isProduction
XenditXenditDriverConfigsecretKey
PayPalPayPalDriverConfigclientId, clientSecret, mode

Checkout#

Create a checkout session to collect payments:

typescript
const result = await stripe.createCheckout({
  amount: 100000,
  currency: "IDR",
  description: "Order #123",
  customerEmail: "user@example.com",
  successUrl: "https://myapp.com/success",
  cancelUrl: "https://myapp.com/cancel",
}, {
  idempotencyKey: "order-123",
});

// result.checkoutUrl → redirect customer to pay
// result.status → "pending" | "processing" | "completed" | "failed" | ...

CheckoutResult#

FieldTypeDescription
idstringProvider checkout/session ID
statusCheckoutStatusNormalized status
amountnumberAmount in smallest currency unit
currencystring3-letter currency code
providerstringProvider identifier
checkoutUrlstring?Redirect URL for customer
providerPaymentIdstring?Payment intent/session ID
clientSecretstring?Client-side secret
metadataRecord?Provider-specific metadata
expiresAtDate?Expiration timestamp
createdAtDateCreation timestamp

CheckoutStatus#

typescript
type CheckoutStatus =
  | "pending"
  | "processing"
  | "requires_action"
  | "completed"
  | "failed"
  | "expired"
  | "cancelled";

Refund#

Create a refund for an existing payment:

typescript
const refund = await stripe.createRefund({
  paymentId: "pi_xxx",
  amount: 50000,      // partial refund (omit for full)
  reason: "customer_request",
});

// refund.status → "pending" | "completed" | "failed" | "partially_refunded"

Subscription#

Create and manage recurring subscriptions:

typescript
const sub = await stripe.createSubscription({
  planId: "price_xxx",
  customerEmail: "user@example.com",
  trialPeriodDays: 14,
});

// Cancel
await stripe.cancelSubscription(sub.id);

SubscriptionStatus#

typescript
type SubscriptionStatus =
  | "active"
  | "past_due"
  | "cancelled"
  | "suspended"
  | "expired"
  | "incomplete";

Generate a shareable payment link:

typescript
const link = await stripe.createPaymentLink({
  amount: 100000,
  currency: "IDR",
  description: "One-time payment",
});

// link.url → share with customer

Webhooks#

Handle webhook events with signature verification:

typescript
import { paymentWebhook } from "@buntok/core/payment";

app.post("/webhooks/stripe",
  paymentWebhook({
    driver: stripe,
    secret: process.env.STRIPE_WEBHOOK_SECRET!,
  }),
  (ctx) => {
    const event = ctx.store.paymentEvent;
    // event.type → "payment.completed" | "payment.failed" | ...
    return ctx.json({ received: true });
  }
);

WebhookMiddlewareOptions#

FieldTypeDescription
driverPaymentDriverPayment driver for verification
secretstringWebhook signing secret from provider
signatureHeaderstring?Override signature header name

WebhookEvent#

FieldTypeDescription
idstringEvent ID
typeWebhookEventTypeNormalized event type
providerstringProvider identifier
rawDataunknownOriginal provider payload
entityIdstring?Payment/subscription ID
amountnumber?Amount if applicable
currencystring?Currency if applicable
statusstring?Provider status string
metadataRecord?Provider-specific metadata
createdAtDateEvent timestamp

WebhookEventType#

typescript
type WebhookEventType =
  | "payment.completed"
  | "payment.failed"
  | "payment.expired"
  | "payment.created"
  | "subscription.created"
  | "subscription.cancelled"
  | "subscription.payment_succeeded"
  | "subscription.payment_failed"
  | "subscription.suspended"
  | "refund.completed"
  | "refund.failed";
Info
Default signature headers: Stripe (stripe-signature), Midtrans (x-signature), Xendit (x-callback-token), PayPal (paypal-transmission-sig).

Error Handling#

Payment errors extend HttpError for consistent error handling:

typescript
import {
  PaymentError,              // base class (extends HttpError)
  PaymentProviderError,      // 502 - provider API error
  PaymentVerificationError,  // 400 - webhook signature mismatch
  PaymentIdempotencyError,   // 409 - idempotency key reuse
  PaymentConfigurationError, // 500 - invalid driver config
} from "@buntok/core/payment";
ErrorStatusUse Case
PaymentErrorvariesBase payment error
PaymentProviderError502Provider API returned an error
PaymentVerificationError400Webhook signature verification failed
PaymentIdempotencyError409Idempotency key reuse detected
PaymentConfigurationError500Invalid driver configuration