Command Palette

Search for a command to run...

How to Create an Integration Module Provider

In this guide you'll learn how to create a Medusa provider that manages its settings through the Integration Module.

What is the Integration Module? 

The Integration Module lets any plugin describe its options as a schema, and store admins configure them in the Admin, with no edits and no redeploy of the Medusa app. It generates the UI, provides a CRUD API and validation, so you don't have to write your own data models, routes, forms, or workflows.

Implementation Example

When you write your own Integration Module provider, it helps to see how a finished one is put together.

For a real implementation, read the YooKassa payment provider in the Medusa Integrations repository.

1. Create the Provider Directory

Start by creating a new directory for your Integration Module provider. In a plugin it goes under , for example .

2. Create the Integration Module Provider Service

Create the file , which declares the integration's descriptor and the service itself.

providers/integration-my/services/my-integration.ts
1import {
2 AbstractIntegrationProvider
3} from "@gorgo/medusa-integration"
4
5class MyIntegrationProvider extends AbstractIntegrationProvider {
6 // TODO add methods
7}
8
9export default MyIntegrationProvider

The parent class you're extending here, , is abstract, so the subclass must implement its property (declared as a ). The class also needs a , which the loader relies on.

identifier

Every Integration Module provider has a unique identifier. The store admin configures the provider under it, and a consumer reads the provider's options by the same one.

Example

providers/integration-my/services/my-integration.ts
1class MyIntegrationProvider extends AbstractIntegrationProvider {
2 static identifier = "my"
3 // ...
4}

The loader reads this static field to build the key your instance is registered under: , or without an ID.

descriptor

The provider class must implement the property. The descriptor holds a single declaration of the options, how they group into settings sections, and an optional connection test. You create it with .

See General Concepts for an overview of the descriptor. The full list of fields (, , , , , , , and others) is declared in the exported type.

Example

providers/integration-my/services/my-integration.ts
1import {
2 AbstractIntegrationProvider,
3 defineIntegration,
4 z
5} from "@gorgo/medusa-integration"
6
7const descriptor = defineIntegration({
8 category: "payment",
9 displayName: "my.name",
10 options: {
11 apiKey: {
12 type: "string",
13 control: "secret",
14 secret: true,
15 required: true,
16 label: "my.apiKey"
17 },
18 sandbox: {
19 type: "boolean",
20 control: "switch",
21 default: false,
22 label: "my.sandbox"
23 },
24 },
25 sections: [
26 {
27 id: "credentials",
28 title: "my.credentials",
29 options: ["apiKey", "sandbox"]
30 },
31 ],
32})
33
34export type MyOptions = z.infer<typeof descriptor.optionsSchema>
35
36export class MyIntegrationProvider extends AbstractIntegrationProvider {
37 // ...
38 get descriptor() {
39 return descriptor
40 }
41}
42
43export default MyIntegrationProvider

is the resolved, validated shape: builds from the object, and since is and has a , both end up non-optional in .

validateOptions

Like other Medusa providers, an Integration Module provider can declare a static to check its configuration at load time.

Example

providers/integration-my/services/my-integration.ts
1class MyIntegrationProvider extends AbstractIntegrationProvider {
2 // ...
3 static validateOptions(options: Record<string, unknown>) {
4 // optional fail-fast check of options.providers[].options
5 }
6}

The base class's own is a no-op (), so overriding it is optional. The return type signals it either returns nothing or throws to fail the load.

In practice, is almost always for an Integration Module provider. The store admin sets the real settings in the Admin rather than in , so matters less here than it does for other Medusa providers.

testConnection

Unlike , which checks the provider's configuration in , checks the integration's own options, such as an API key, against the third-party service. The store admin runs it from the Admin, and a scheduled job re-checks configured integrations daily. You declare it directly in the descriptor.

Example

providers/integration-my/services/my-integration.ts
1const descriptor = defineIntegration({
2 // ...
3 testConnection: async ({ options }) => {
4 const res = await fetch("https://api.my.com/ping", {
5 headers: { Authorization: `Bearer ${options.apiKey}` },
6 })
7
8 return res.ok
9 ? { status: "passed" }
10 : { status: "failed", message: `My responded with ${res.status}` }
11 },
12})

is one of . Returning counts as a normal result, not an error. If the provider throws, the daily job counts it separately, as .

3. Create Module Provider Definition File

Create the file with the following content:

providers/integration-my/index.ts
1import { ModuleProvider } from "@medusajs/framework/utils"
2import { INTEGRATION_MODULE } from "@gorgo/medusa-integration"
3import MyIntegrationProvider from "./services/my-integration"
4
5export default ModuleProvider(INTEGRATION_MODULE, {
6 services: [MyIntegrationProvider],
7})

This exports the provider definition, declaring as its service.

4. Register the Provider

To use your Integration Module provider, add it to the Integration Module's array in :

medusa-config.ts
1module.exports = defineConfig({
2 // ...
3 plugins: [
4 {
5 resolve: "@gorgo/medusa-integration",
6 options: {
7 encryptionKey: process.env.INTEGRATION_ENCRYPTION_KEY,
8 providers: [
9 {
10 resolve: "./src/providers/integration-my",
11 id: "my-1",
12 options: {},
13 },
14 ],
15 },
16 },
17 // ...
18 ],
19})

The loader instantiates from this registration.

Warning: 

You set an option on the Integration Module provider, and the module assembles the final instance key from it. Without an , the provider registers as the single instance under the key . If the Integration Module provider supports multiple instances, each one gets its own .

Next, add the secret encryption key to your environment variables. The Integration Module uses it to encrypt secrets before saving them to the database:

.env
INTEGRATION_ENCRYPTION_KEY=supersecret

Any non-empty value works. The module derives it via SHA-256 into a 32-byte AES-256-GCM key, so use a high-entropy one, for example .

5. Test It Out

Start the server and open Settings → Integrations in Medusa Admin. Your integration appears in the list. Fill in the options and click Test connection to run .

To read the saved options, use and pass the same your provider class declares:

providers/payment-my/services/my-payment.ts
1import { resolveIntegrationOptions } from "@gorgo/medusa-integration"
2import type { MyOptions } from "../../integration-my/services/my-integration"
3
4const options = await resolveIntegrationOptions<MyOptions>({
5 identifier: "my",
6})

The returned is typed as , already decrypted and validated. By default the call throws a if the integration is not configured, disabled, or incomplete.

See Reading Options for how to read an integration's options from a route, subscriber, or workflow step.

References

Edited Aug 12, 2026·Edit this page