Skip to content

Build a Handler

Janno Jaerv edited this page May 5, 2026 · 1 revision

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.

What a handler package contains

Three things, in this order of importance:

  1. PaymentHandlerAdapter implementation — the class that does the work
  2. A Medusa module wrapper — exposes the adapter to Medusa's DI
  3. (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.

The PaymentHandlerAdapter interface

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).

Minimal handler skeleton

// 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] : []
  }
}

The Medusa module wrapper

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,
})

Convention: env-precedence

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_key

Document the env-var names in your README.

Pass-through over hand-construction

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.

Publishing

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.

See also

Clone this wiki locally