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#
| Driver | Config | Required Fields |
|---|---|---|
| Stripe | StripeDriverConfig | secretKey |
| Midtrans | MidtransDriverConfig | serverKey, isProduction |
| Xendit | XenditDriverConfig | secretKey |
| PayPal | PayPalDriverConfig | clientId, 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#
| Field | Type | Description |
|---|---|---|
| id | string | Provider checkout/session ID |
| status | CheckoutStatus | Normalized status |
| amount | number | Amount in smallest currency unit |
| currency | string | 3-letter currency code |
| provider | string | Provider identifier |
| checkoutUrl | string? | Redirect URL for customer |
| providerPaymentId | string? | Payment intent/session ID |
| clientSecret | string? | Client-side secret |
| metadata | Record? | Provider-specific metadata |
| expiresAt | Date? | Expiration timestamp |
| createdAt | Date | Creation 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";Payment Link#
Generate a shareable payment link:
typescript
const link = await stripe.createPaymentLink({
amount: 100000,
currency: "IDR",
description: "One-time payment",
});
// link.url → share with customerWebhooks#
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#
| Field | Type | Description |
|---|---|---|
| driver | PaymentDriver | Payment driver for verification |
| secret | string | Webhook signing secret from provider |
| signatureHeader | string? | Override signature header name |
WebhookEvent#
| Field | Type | Description |
|---|---|---|
| id | string | Event ID |
| type | WebhookEventType | Normalized event type |
| provider | string | Provider identifier |
| rawData | unknown | Original provider payload |
| entityId | string? | Payment/subscription ID |
| amount | number? | Amount if applicable |
| currency | string? | Currency if applicable |
| status | string? | Provider status string |
| metadata | Record? | Provider-specific metadata |
| createdAt | Date | Event 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";| Error | Status | Use Case |
|---|---|---|
| PaymentError | varies | Base payment error |
| PaymentProviderError | 502 | Provider API returned an error |
| PaymentVerificationError | 400 | Webhook signature verification failed |
| PaymentIdempotencyError | 409 | Idempotency key reuse detected |
| PaymentConfigurationError | 500 | Invalid driver configuration |