Publish a plugin to the marketplace
This guide takes a finished plugin folder and turns it into a catalog entry users can find and install. You will package the plugin into a distributable .zip, register it in the marketplace catalog, and cut a tagged GitHub Release that serves the download.
The marketplace is the openwa-plugins repository. Publishing means opening a pull request that adds your plugin folder; the catalog (plugins.json) and the release .zip are generated from your folder's manifest.json and CHANGELOG.md, not written by hand.
- A working plugin folder that builds and installs locally. See Building a plugin.
- Node.js and the
zipCLI on yourPATH(the package script shells out tozip). - A fork of the
openwa-pluginsrepository.
How publishing fits together
Three artifacts come out of one plugin folder. Each is derived from the manifest and changelog, so those two files are the single source of truth.
The catalog entry's download field points at a GitHub Release asset whose URL is fully predictable from the plugin id and version. Get those two right and everything else lines up.
1. Meet the marketplace standard
Every plugin in the catalog follows the same layout so tooling stays consistent. Before you package, confirm your folder has these files:
| File | Purpose |
|---|---|
manifest.json | Machine-readable metadata. The single source of truth for id, name, version, and type. |
README.md | Human docs, with a generated Details block between <!-- BEGIN DETAILS --> and <!-- END DETAILS --> markers. |
CHANGELOG.md | Dated version history. The top released heading sets the release date and must match the manifest version. |
index.ts | Entry source that default-exports an IPlugin class. |
dist/index.js | The built artifact referenced by manifest.main (produced by the package step). |
The full standard — manifest fields, the configSchema vocabulary, and README section order — is documented in PLUGIN-STANDARD.md. This guide covers only the publishing path.
Required manifest fields
OpenWA rejects an install without these five fields, and the package script checks them before building:
| Field | Constraint |
|---|---|
id | Matches /^[a-z0-9][a-z0-9._-]*$/i, unique, and not a reserved id (whatsapp-web.js, baileys, auto-reply, translation). |
name | Display name shown in the dashboard. |
version | SemVer. Must equal the top released CHANGELOG.md heading. |
type | Must be extension — the only user-installable type. |
main | A require()-able file present in the package, e.g. dist/index.js. |
The catalog also surfaces description, author, license, keywords, status, minOpenWAVersion, and testedOpenWAVersion. Set them in the manifest; the catalog generator reads them from there.
2. Version and set compatibility
OpenWA does not yet negotiate host-version compatibility, so the version fields are a convention you maintain, not a gate the host enforces. Set them honestly.
| Field | Meaning |
|---|---|
version | The plugin's own SemVer version. MAJOR = breaking change for operators, MINOR = new capability, PATCH = fix. |
minOpenWAVersion | The lowest OpenWA version the plugin is known to work on. |
testedOpenWAVersion | The OpenWA version you actually verified against. |
The version must agree with your changelog. The package step fails with a version drift error if manifest.json says one version and the top CHANGELOG.md heading says another:
✗ my-plugin: version drift: manifest is 1.0.0 but CHANGELOG top is 0.9.0
Keep CHANGELOG.md in Keep a Changelog form: an ## [Unreleased] section at the top, then ## [MAJOR.MINOR.PATCH] — YYYY-MM-DD headings in descending order. To release 1.0.0, move the unreleased notes under a dated ## [1.0.0] — 2026-06-26 heading and set the manifest version to 1.0.0.
3. Build the distributable .zip
Package the plugin from the repository root. Pass the plugin folder name:
node package.mjs my-plugin
The script validates the manifest, bundles index.ts into a single CommonJS dist/index.js with esbuild, then zips manifest.json plus the built dist/ into my-plugin.zip. It finishes by printing the size and SHA-256 of the package:
✓ Packaged my-plugin v1.0.0 → my-plugin.zip (12.4 KB)
sha256: 9f2c0a7e8b5d4c3a1f6e0b9d8c7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f
The build npm script is a shortcut that runs the same packager against one specific plugin:
# package.json: "build": "node package.mjs gsheets-logger"
npm run build
To build your own plugin, call node package.mjs <your-id> directly rather than npm run build.
OpenWA refuses a package over 5 MB compressed, 200 files, or 20 MB uncompressed. The packager also fails locally if the .zip exceeds the 5 MB compressed limit. There is no npm install at install time, so bundle every dependency into dist/index.js.
4. Add the catalog entry
The catalog (plugins.json) and the README tables are generated — never edit them by hand. After your manifest.json and CHANGELOG.md are final, regenerate them:
npm run catalog
This rewrites plugins.json, the catalog table in the root README.md, and each plugin README's Details block, all from the manifests and changelogs it discovers:
Catalog written (6 plugin(s)); updated 3 file(s).
Your plugin's catalog entry is built from the manifest, with the release date pulled from the top CHANGELOG.md heading:
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"type": "extension",
"status": "stable",
"description": "What the plugin does, one sentence.",
"author": "Your Name <you@example.com>",
"license": "MIT",
"keywords": ["whatsapp", "openwa"],
"minOpenWAVersion": "0.7.0",
"testedOpenWAVersion": "0.7.6",
"releasedAt": "2026-06-26",
"repoPath": "my-plugin",
"repoUrl": "https://github.com/rmyndharis/OpenWA-plugins",
"homepage": "https://github.com/rmyndharis/OpenWA-plugins/tree/main/my-plugin",
"download": "https://github.com/rmyndharis/OpenWA-plugins/releases/download/my-plugin-v1.0.0/my-plugin.zip"
}
Two fields are deliberately absent from the catalog: package size and SHA-256. Those are per-release artifacts published on the GitHub Release, so keeping them out of plugins.json keeps the catalog deterministic and the CI drift check stable.
The download URL is predictable
The generator computes download from the repository, plugin id, and version:
<repoUrl>/releases/download/<id>-v<version>/<id>.zip
So my-plugin at version 1.0.0 resolves to the asset at the release tagged my-plugin-v1.0.0. The catalog points at that URL before the release exists — you create the matching release in the next step.
Verify the catalog is current
CI runs a drift check that fails if the committed catalog is stale or a manifest version disagrees with its changelog. Run it yourself before opening the pull request:
npm run catalog:check
A clean tree reports:
Catalog up to date (6 plugin(s)).
If it fails, it lists the stale files. Run npm run catalog to regenerate them and commit the result:
Catalog is out of date. Run `npm run catalog` and commit:
- plugins.json
- my-plugin/README.md
5. Open the pull request and release
- Commit your plugin folder, the regenerated
plugins.json, and the updated README files to a branch on your fork. - Open a pull request against
openwa-pluginsdescribing what the plugin does and the OpenWA version you tested against. - Once merged, tag the release as
<plugin-id>-vX.Y.Z— the version must match the manifest and changelog (for examplemy-plugin-v1.0.0).
The release GitHub Action takes over from the tag. It builds the plugin, attaches <id>.zip and <id>.zip.sha256 to a GitHub Release, and uses the matching CHANGELOG.md section as the release notes. The asset lands at exactly the download URL already recorded in the catalog.
The tag, the manifest version, and the top CHANGELOG.md heading must all be the same value. A mismatch points the catalog's download URL at a release asset that does not exist.
How users install your published plugin
Until the in-dashboard marketplace ships, users download the release .zip and upload it. An admin-scoped API key manages every plugin route. The install path is the same one covered in the overview:
# 1. Install the uploaded package
curl -X POST "http://localhost:2785/api/plugins/install" \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@my-plugin.zip"
# 2. Configure it (secrets are masked on read, preserved on write)
curl -X PUT "http://localhost:2785/api/plugins/my-plugin/config" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "config": { "apiKey": "..." } }'
# 3. Enable it — plugins install disabled and never auto-enable
curl -X POST "http://localhost:2785/api/plugins/my-plugin/enable" \
-H "X-API-Key: YOUR_API_KEY"
The planned in-dashboard marketplace reads this same plugins.json and installs the release asset by URL, so a correct catalog entry is what makes one-click install work later. The design is described in OPENWA-MARKETPLACE.md.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
manifest.json missing required field(s) | One of id, name, version, type, main is absent. | Add the missing field, then re-run node package.mjs <id>. |
type must be "extension" | The manifest declares a non-installable type. | Set "type": "extension". Engine, storage, queue, and auth plugins are first-party built-ins. |
version drift: manifest is X but CHANGELOG top is Y | The manifest version and the top dated changelog heading disagree. | Make both the same SemVer value. |
CHANGELOG.md has no released "## [x.y.z] — YYYY-MM-DD" heading | The changelog has only an ## [Unreleased] section. | Add a dated ## [X.Y.Z] — YYYY-MM-DD heading for this release. |
package is N KB, over the 5 MB install limit | The bundle is too large. | Trim dependencies; the runtime needs only Node built-ins plus your bundled code. |
catalog:check fails in CI | plugins.json or a README was edited by hand or left stale. | Run npm run catalog and commit the regenerated files. |
zip failed (is the \zip` CLI installed?)` | The zip binary is missing. | Install zip and re-run the package step. |
Next steps
- Build a plugin — the authoring workflow that produces the folder you publish here.
- Plugin architecture — the runtime, hooks, and capabilities your plugin runs against.
- Plugin catalog — the published plugins your entry joins.