-
Notifications
You must be signed in to change notification settings - Fork 0
Build a Handler
How to author your own payment handler package for medusa-plugin-agentic-commerce. This is the path Stripe, Klarna, or any other payment provider would use — the in-tree @financedistrict/medusa-plugin-prism-payment is the reference implementation.
Handler packages live in their own repos, not in this monorepo. There's no PR to this repo required to ship a new handler.
Three things, in this order of importance:
-
PaymentHandlerAdapterimplementation — the class that does the work - A Medusa module wrapper — exposes the adapter to Medusa's DI
- (Optional) A Medusa payment provider — for the standard authorize/capture/refund flow if your payment scheme participates in Medusa's normal payment lifecycle
Most handlers ship all three, though the third is only relevant if you also want Medusa's admin/dashboard payment flow to work with your handler.
Defined in @financedistrict/medusa-plugin-agentic-commerce:
export interface PaymentHandlerAdapter {
/** Reverse-DNS handler-type identifier, e.g. "com.stripe.agent_payment" */
readonly id: string
/** Human-readable handler name */
readonly name: string
/** UCP discovery — keyed by namespace */
getUcpDiscoveryHandlers(): Promise<Record<string, unknown[]>>
/** ACP discovery — flat array of PaymentHandler objects */
getAcpDiscoveryHandlers(): Promise<unknown[]>
/** Called when a checkout is created or updated. Store result on the cart. */
prepareCheckoutPayment(input: CheckoutPrepareInput): Promise<unknown | null>
/** Reads the prepare result from cart metadata, returns UCP-shaped block */
getUcpCheckoutHandlers(cartMetadata?: Record<string, unknown>): Record<string, unknown[]>
/** Reads the prepare result from cart metadata, returns ACP-shaped array */
getAcpCheckoutHandlers(cartMetadata?: Record<string, unknown>): unknown[]
}The plugin doesn't define a settlement method on the adapter — settlement is Medusa's payment-provider concern. If your handler also acts as a Medusa payment provider, that's a separate Medusa interface (AbstractPaymentProvider).
// src/modules/my-payment-handler/service.ts
import type {
PaymentHandlerAdapter,
CheckoutPrepareInput,
} from "@financedistrict/medusa-plugin-agentic-commerce"
export const MY_HANDLER_ID = "com.example.my_payment"
const STORAGE_KEY = "my_payment_data"
export type MyPaymentHandlerOptions = {
api_url?: string
api_key?: string
}
export default class MyPaymentHandlerAdapter implements PaymentHandlerAdapter {
readonly id = MY_HANDLER_ID
readonly name = "My Payment"
private apiUrl: string
private apiKey: string
constructor(_container: Record<string, unknown>, options: MyPaymentHandlerOptions = {}) {
// Convention: env wins over passed options. Lets merchants override
// dashboard-stored config with deploy-time secrets when they need to.
this.apiUrl = process.env.MY_API_URL ?? options.api_url ?? "https://api.example.com"
this.apiKey = process.env.MY_API_KEY ?? options.api_key ?? ""
}
async getUcpDiscoveryHandlers() {
if (!this.apiKey) return {}
return {
[MY_HANDLER_ID]: [{
id: "default",
version: "2026-01-15",
spec: "https://example.com/spec.md",
schema: "https://example.com/schema.json",
config: {},
}],
}
}
async getAcpDiscoveryHandlers() {
if (!this.apiKey) return []
return [{
id: "default",
name: MY_HANDLER_ID,
version: "2026-01-15",
spec: "https://example.com/spec.md",
requires_delegate_payment: false,
requires_pci_compliance: false,
psp: "example",
config_schema: "https://example.com/config_schema.json",
instrument_schemas: ["https://example.com/instrument_schema.json"],
config: {},
}]
}
async prepareCheckoutPayment(input: CheckoutPrepareInput) {
// Call your gateway, return whatever shape your handler needs
// back at request time.
const result = { /* ... */ }
// Persist on cart metadata so getUcpCheckoutHandlers /
// getAcpCheckoutHandlers can read it.
const cartService = input.container.resolve("cart") as any
await cartService.updateCarts(input.cart.id, {
metadata: {
...(input.cart.metadata || {}),
[STORAGE_KEY]: result,
},
})
return result
}
getUcpCheckoutHandlers(cartMetadata?: Record<string, unknown>) {
const data = cartMetadata?.[STORAGE_KEY]
if (!data) return {}
return { [MY_HANDLER_ID]: [data] }
}
getAcpCheckoutHandlers(cartMetadata?: Record<string, unknown>) {
const data = cartMetadata?.[STORAGE_KEY]
return data ? [data] : []
}
}Exposes the adapter to Medusa's DI container so the agentic-commerce plugin can find it via payment_handler_adapters.
// src/modules/my-payment-handler/index.ts
import { Module } from "@medusajs/framework/utils"
import MyPaymentHandlerAdapter from "./service"
export const MY_PAYMENT_HANDLER_MODULE = "myPaymentHandler"
export default Module(MY_PAYMENT_HANDLER_MODULE, {
service: MyPaymentHandlerAdapter,
})Make your handler tolerant of both Medusa-config-supplied options and env vars, with env winning. This lets merchants:
- Set everything in
medusa-config.ts(simple deployments) - Or set sensitive values via env (compliance / runtime secret rotation)
- Or both, in which case env wins
Use the same pattern Prism uses:
this.apiKey = process.env.MY_API_KEY ?? options.api_keyDocument the env-var names in your README.
Don't hand-fabricate the UCP/ACP discovery or prepare responses if your upstream service can give you the right shape directly. Hard-coding requires_delegate_payment: false, psp: "...", etc. means your handler lies whenever your service evolves.
Prism originally hand-constructed everything; the v0.7.0 release switched to passing through Prism's own protocol-specific responses verbatim. If your service has equivalent endpoints, do the same. See packages/prism-payment/src/modules/prism-payment-handler/service.ts for the reference shape.
Publish to npm under your own scope. Set a peer dependency on @financedistrict/medusa-plugin-agentic-commerce:
{
"name": "@your-org/medusa-plugin-my-payment",
"version": "0.1.0",
"peerDependencies": {
"@financedistrict/medusa-plugin-agentic-commerce": "^0.2.0",
"@medusajs/framework": "^2.13.0",
"@medusajs/medusa": "^2.13.0"
}
}That's it — no PR to this repo, no central registry to update. Merchants npm install @your-org/medusa-plugin-my-payment, register the module in their medusa-config.ts, and reference it in payment_handler_adapters.
- Architecture — how handlers fit into the gateway pattern
- UCP and ACP — exact response shapes per protocol
- Authentication — convention for handler-side auth
- The reference implementation:
packages/prism-payment