# @suaveplan/api-plugin-payment - Payment-lifecycle plugin for @suaveplan/api. Wraps @suaveplan/payment + caller-injected PaymentProvider (Stripe/Adyen/PayPal/Square/Mollie/custom) as a @suaveplan/api-plugin definePlugin descriptor. Mounts 6 owner-scoped OpenAPI routes (intent create/list/get/confirm/capture/cancel + refund), a HMAC-verified webhook receiver, an Idempotency-Key middleware, a typed paymentService slot in the host ServiceRegistry, and OTel instruments (5 counters + 1 histogram). Server-tier — runs in Bun and Node. Payment-lifecycle plugin for [`@suaveplan/api`](../api). Wraps `@suaveplan/payment` + a caller-injected `PaymentProvider` (Stripe / Adyen / PayPal / Square / Mollie / custom) as a single `definePlugin` consumer that ships: - **7 owner-scoped OpenAPI routes** (create / list / get / confirm / capture / cancel / refund) gated behind `requireAuth()` from `@suaveplan/api-plugin-auth`, with cross-user 404 anti-enumeration baked in at the service layer. - **`POST /payments/webhook`** — HMAC-verified webhook receiver that reads the raw body and checks `X-Signature` + `X-Timestamp` BEFORE deserialising the payload. - **`Idempotency-Key` middleware** on `POST /intents` and `POST /intents/:id/refund` so retries never double-charge. - **`paymentService` ServiceRegistry slot** exposing the full lifecycle to downstream plugins with full TypeScript inference. - **OpenTelemetry instrument bundle** — 5 counters + 1 histogram covering intent creation, captures, refunds, webhook delivery, signature failures, and intent-creation latency. - **15 stable `PAYMENT_PLUGIN_*` error codes** with typed `PaymentPluginError` class for programmatic dispatch. Server-tier — runs in Bun and Node. ## Documentation tree - [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-plugin-payment`. Every failure path in the plugin raises a `PaymentPlugi… - [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-plugin-payment`. Every failure path in the plugin raises a `PaymentPlugi… - [context-augmentation](./src/middleware/context-augmentation.md) — TypeScript module augmentation that surfaces the Hono `ContextVariableMap` slot `user: Principal | null` to every consu… - [idempotency-middleware](./src/middleware/idempotency-middleware.md) — Hono middleware enforcing the `Idempotency-Key` header on payment-mutation routes (`POST /payments/intents`, `POST /pay… - [otel](./src/otel/instruments.md) — OpenTelemetry instrument bundle for `@suaveplan/api-plugin-payment`. Exposes 5 counters and 1 histogram so operators ca… - [instruments](./src/otel/instruments.md) — OpenTelemetry instrument bundle for `@suaveplan/api-plugin-payment`. Exposes 5 counters and 1 histogram so operators ca… - [plugin](./src/plugin/plugin.md) — Factory + descriptor for the `@suaveplan/api-plugin-payment` Hono plugin. Wraps a caller-injected `PaymentProvider` (St… - [plugin](./src/plugin/plugin.md) — Factory + descriptor for the `@suaveplan/api-plugin-payment` Hono plugin. Wraps a caller-injected `PaymentProvider` (St… - [routes](./src/routes/build-routes.md) — Builds the 7 OpenAPI route entries the plugin mounts for the payment lifecycle (create / list / get / confirm / capture… - [build-routes](./src/routes/build-routes.md) — Builds the 7 OpenAPI route entries the plugin mounts for the payment lifecycle (create / list / get / confirm / capture… - [capture-intent](./src/schemas/routes/capture-intent.md) — Body schema for `POST /payments/intents/:id/capture`. Supports both full capture (empty body) and partial capture (body… - [create-intent](./src/schemas/routes/create-intent.md) — Body and response schemas for `POST /payments/intents`. The request body carries the amount (typed `MoneySchema`), opti… - [empty-body](./src/schemas/routes/empty-body.md) — Strict empty-object body schema shared by `POST /payments/intents/:id/confirm` and `POST /payments/intents/:id/cancel`.… - [intent-id-param](./src/schemas/routes/intent-id-param.md) — Schema for the `:id` path parameter shared by every per-intent route (`GET /intents/:id`, `POST /intents/:id/confirm`,… - [list-intents](./src/schemas/routes/list-intents.md) — Query and response schemas for `GET /payments/intents`. The route lists intents owned by the authenticated principal, o… - [payment-intent](./src/schemas/routes/payment-intent.md) — Wire-shape Zod schemas for the serialised `PaymentIntent`, `Refund`, and `PaymentStatus` types exposed by every route i… - [refund](./src/schemas/routes/refund.md) — Body + response schemas for `POST /payments/intents/:id/refund`. Supports full or partial refunds with optional `reason… - [webhook](./src/schemas/routes/webhook.md) — Response schema for `POST /payments/webhook`. The webhook receiver MUST read the raw request body and verify the provid… - [money](./src/schemas/money.md) — Boundary Zod schema for `MoneyLike` — `{ amount: bigint, currency: string }`. Payment APIs work in minor units (cents,… - [options](./src/schemas/options.md) — Zod schema validating `PaymentPluginOptions` at the `createPaymentPlugin` factory boundary. The schema runs once at con… - [payment-service](./src/services/payment-service.md) — Owner-scoped lifecycle service that wraps the caller-injected `PaymentProvider` with an ownership map (`PaymentIntentSt… - [registry-augmentation](./src/services/registry-augmentation.md) — TypeScript module augmentation that adds the `paymentService` slot to `@suaveplan/api-plugin/service-registry`'s `Servi… - [memory-store](./src/store/memory-store.md) — In-process default implementation of the `PaymentIntentStore` contract. Stores owner records in a `Map ## Install ```bash bun add @suaveplan/api-plugin-payment ``` ## Quick start ```ts import { defineApp } from "@suaveplan/api"; import { createAuthPlugin } from "@suaveplan/api-plugin-auth"; import { createPaymentPlugin } from "@suaveplan/api-plugin-payment"; import { StripeProvider } from "@suaveplan/payment-provider-stripe"; const app = defineApp() .use(createAuthPlugin({ betterAuth: { /* ... */ } })) .use( createPaymentPlugin({ provider: new StripeProvider({ apiKey: process.env.STRIPE_KEY! }), webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, }) ); ``` The plugin mounts under `/payments` by default. Override with `routePrefix`. ## The 3-way plug-and-play contract `@suaveplan/api-plugin-payment` is the canonical **payment** exemplar of the contract every `@suaveplan/api-plugin-*` package follows: 1. **Boundary Zod schemas** — every request body, query, and path parameter validates through a schema exported at `./schemas/*` so consumers and the plugin share one source of truth. 2. **OpenAPI route registration** — every route is built with `RouteBuilder` from `@suaveplan/api-openapi` and registered via `definePluginRoutes`, so the host's `/openapi.json` describes the payment surface with the same Zod-derived types the code accepts. 3. **ServiceRegistry slot** — the `paymentService` slot exposes the lifecycle to downstream plugins without forcing them to re-implement provider plumbing. ## Idempotency `POST /payments/intents` and `POST /payments/intents/:id/refund` honour the `Idempotency-Key` header. Retrying the same request with the same key and body returns the cached response without invoking the provider; retrying with a different body raises HTTP 409. ```ts const res = await fetch("/payments/intents", { method: "POST", headers: { "content-type": "application/json", "Idempotency-Key": `order-${orderId}`, }, body: JSON.stringify({ amount: { amount: "2500", currency: "USD" } }), }); ``` ## Webhook verification The receiver expects: - `X-Signature` — hex-encoded HMAC of `${timestamp}.${body}` using the configured `webhookSecret`. - `X-Timestamp` — Unix ms integer. Tolerance window defaults to 5 minutes (`webhookToleranceMs`). Algorithm defaults to `SHA-256` (`webhookAlgorithm: "SHA-256" | "SHA-384" | "SHA-512"`). ```ts import { signPaymentWebhook } from "@suaveplan/api-plugin-payment"; const body = JSON.stringify(event); const ts = Date.now(); const sig = signPaymentWebhook({ body, timestamp: ts, secret: process.env.STRIPE_WEBHOOK_SECRET!, algorithm: "SHA-256", }); await fetch("/payments/webhook", { method: "POST", headers: { "X-Signature": sig, "X-Timestamp": String(ts) }, body, }); ``` ## Pluggable storage The default in-process `MemoryPaymentIntentStore` records the intent-id → owner map. Inject a SurrealDB / Postgres / Redis adapter for production: ```ts import { createPaymentPlugin } from "@suaveplan/api-plugin-payment"; const plugin = createPaymentPlugin({ provider, webhookSecret, store: new MySurrealPaymentIntentStore({ db }), }); ``` ## Modules - [`./src/plugin/plugin.md`](./src/plugin/plugin.md) — factory + 6-step `onInit`. - [`./src/services/payment-service.md`](./src/services/payment-service.md) — owner-scoped lifecycle. - [`./src/services/registry-augmentation.md`](./src/services/registry-augmentation.md) — `ServiceRegistry["paymentService"]` slot. - [`./src/middleware/idempotency-middleware.md`](./src/middleware/idempotency-middleware.md) — Idempotency-Key middleware. - [`./src/webhook/verify-webhook.md`](./src/webhook/verify-webhook.md) — HMAC verifier + signer. - [`./src/routes/build-routes.md`](./src/routes/build-routes.md) — 7 owner-scoped routes. - [`./src/store/payment-intent-store.md`](./src/store/payment-intent-store.md) — store contract. - [`./src/store/memory-store.md`](./src/store/memory-store.md) — in-process default. - [`./src/schemas/money.md`](./src/schemas/money.md) — `Money` boundary schema. - [`./src/schemas/options.md`](./src/schemas/options.md) — `PaymentPluginOptionsSchema`. - [`./src/schemas/routes/payment-intent.md`](./src/schemas/routes/payment-intent.md) — intent/refund wire shape. - [`./src/schemas/routes/create-intent.md`](./src/schemas/routes/create-intent.md) — `POST /intents` body. - [`./src/schemas/routes/intent-id-param.md`](./src/schemas/routes/intent-id-param.md) — `:id` path param. - [`./src/schemas/routes/list-intents.md`](./src/schemas/routes/list-intents.md) — `GET /intents` query. - [`./src/schemas/routes/capture-intent.md`](./src/schemas/routes/capture-intent.md) — capture body. - [`./src/schemas/routes/refund.md`](./src/schemas/routes/refund.md) — refund body + response. - [`./src/schemas/routes/empty-body.md`](./src/schemas/routes/empty-body.md) — empty-body schema. - [`./src/schemas/routes/webhook.md`](./src/schemas/routes/webhook.md) — webhook response. - [`./src/otel/instruments.md`](./src/otel/instruments.md) — 5 counters + 1 histogram. - [`./src/error-codes/error-codes.md`](./src/error-codes/error-codes.md) — 15 `PAYMENT_PLUGIN_*` codes. - [`./src/types/types.md`](./src/types/types.md) — `PaymentPluginOptions` / `PaymentPluginConfig`.