Building
API reference
The REST endpoints for publishing and installing plugins, and the webhook address plugin channels receive on.
Everything on this page is also a button in the console. Use the API to publish from a script or CI.
Base URL: https://services.agentparley.ai/platform
Authentication: Authorization: Bearer <token> — the same signed-in user token the console uses. Every
request acts as that user on that user's account. Ids in requests and responses are strings.
Errors: a failed request returns a JSON body { "message": "…", "traceId": "…" }. 400 is a validation
error (the message says what and where), 404 is a plugin, version or installation that is not yours or does not
exist, 409 is a conflict, 503 means retry shortly.
Get a token
POST /auth/login
Content-Type: application/json
{ "email": "you@example.com", "password": "…" }
The response is { "token": "…", "expiresAt": "…", "refreshToken": "…", "refreshExpiresAt": "…", "user": { … }, "account": { … } }. An email belongs to exactly one account.
POST /auth/refresh { "refreshToken": "…" } returns a fresh { token, expiresAt, refreshToken, refreshExpiresAt };
the old refresh token is replaced, so store the new one.
Authoring
Create a plugin
POST /plugins
Content-Type: application/json
{ "slug": "weather", "displayName": "Weather", "description": "Forecasts for your agents." }
201 Created with the plugin, including its id. slug and displayName are required. The slug matches
^[a-z0-9][a-z0-9_-]{0,63}$, must be unused (409 otherwise), and may not start with agentparley- (reserved for
official plugins) or mcp_. A new plugin is private.
List and read your plugins
GET /plugins
GET /plugins/{id}?includeFiles=true
GET /plugins takes an optional isPriced=true|false. The detail includes every version (semVer, status,
sourceSha256), visibility, and — with includeFiles — the source text of the latest published version's files.
Upload a package (create or version in one call)
POST /plugins/upload
Content-Type: multipart/form-data
file=@weather.zip
The one call a script or CI needs. The zip's readme.md frontmatter names the plugin: name is required, slug
is optional (default: the name lowercased, with runs of other characters turned into -, at most 64
characters), and version is <major>.<minor> (default 1.0). The platform creates the plugin if the slug is
new, or versions the one you own, and picks the patch itself: changed content publishes the latest patch + 1, a
raised major.minor publishes <major>.<minor>.0, unchanged content publishes nothing.
curl -X POST "$BASE/plugins/upload" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@weather.zip;type=application/zip"
200 with { "pluginId": "…", "slug": "weather", "semVer": "1.0.3", "outcome": "Created" }. outcome is
Created, NewVersion or Unchanged (then semVer is null). 409 if the slug belongs to another account, or
the readme's major.minor is lower than the latest published one. If a new plugin's package fails to build, the
empty plugin is removed again. The plugin stays private until you list it in Browse (below).
Publish a version from a zip
POST /plugins/{id}/versions/zip
Content-Type: multipart/form-data
file=@weather.zip
semVer=1.0.0
The zip holds the package — readme.md is required — optionally inside one wrapper
folder. Up to 256 MB. semVer is digits-only major.minor.patch; you choose it. Zips are the only way to publish
binary files (companion packs, portraits, icon.png, the gallery/ folder).
curl -X POST "$BASE/plugins/$PLUGIN_ID/versions/zip" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@weather.zip;type=application/zip" \
-F "semVer=1.0.1"
| Response | Meaning |
|---|---|
201 |
published; the body is { id, semVer, status, commandCount, skillCount, skillFileCount, middlewareCount, lifecycleHookCount, toolCount, hasCode } |
400 |
a validation error — the message says what and where |
409 |
that version already exists (versions are immutable) |
503 |
bundler busy — retry shortly |
Publish a version from a file list
POST /plugins/{id}/versions
Content-Type: application/json
{
"semVer": "1.0.1",
"files": [
{ "path": "readme.md", "contentText": "---\nname: Weather\n---\n# Weather\n…" },
{ "path": "index.ts", "contentText": "globalThis.tools.get_forecast = { … };" }
]
}
Text files only, up to 64 MB of request body. 201 returns the same summary as the zip publish.
Save a draft
POST /plugins/{id}/draft
Content-Type: application/json
{ "files": [ … ] }
Builds and validates like a publish, without a version number. 201 returns the same summary as a publish.
Installs on the Draft policy (your own account only) follow it.
An icon for a JSON publish or draft goes up separately, because the file list is text only:
PUT /plugins/{id}/draft/icon multipart/form-data, file=@icon.png
DELETE /plugins/{id}/draft/icon
The file is a PNG or SVG of at most 512 KB. PUT returns { "draftIconUrl": "…" }; the next draft save or publish
carries it. A zip's own icon.svg or icon.png replaces it.
The gallery works the same way. Every call below is for the plugin's owner and returns the gallery as it now stands:
POST /plugins/{id}/draft/gallery multipart/form-data, file=@screenshot.png
PUT /plugins/{id}/draft/gallery { "items": [ { "name": "screenshot.png", "caption": "The forecast card" } ] }
DELETE /plugins/{id}/draft/gallery/{name}
POST adds one picture or video to the end (at most 12 files; pictures .png, .jpg/.jpeg, .webp up to 5 MB,
videos .mp4/.webm up to 50 MB; a file that isn't really the type its extension says is refused). PUT sets the
order and the captions (at most 200 characters): items must name exactly the files the gallery holds now, in the
order to show them. Each response is { "items": [ { "name", "kind", "caption", "signedUrl" } ] }. GET /plugins/{id}
returns the same list as draftGallery, and isGalleryChanged, which is true when it differs from the latest
version's. The next draft save or publish carries the gallery. A zip with a gallery/ folder replaces it; a zip
without one keeps it.
List in Browse / remove from Browse
POST /plugins/{id}/listing
DELETE /plugins/{id}/listing { "deprecationMessage": "Replaced by weather-pro." }
Both return 204. Listing requires a published version (409 otherwise). Removing keeps existing installs
working and shows them the message.
Yank a version / delete a plugin
POST /plugins/{id}/versions/{versionId}/yank
DELETE /plugins/{id}
Yank returns the version summary. You can't yank the last published version (409). Installs on Latest that
sit on a yanked version move to the highest remaining version they have already agreed to; an install pinned to
it has no version to run until its owner picks another. Delete returns 204, and 409 for any plugin that has
ever had a published version, yanked or not: only a plugin that never published can be deleted.
Set a price
PUT /plugins/{id}/price
Content-Type: application/json
{ "price": { "model": "Monthly", "amountUsd": 4.99 }, "payoutCountry": "US" }
model is OneTime or Monthly. The amount is between $2.99 and $999. "price": null makes the plugin free.
payoutCountry (a country code) is required the first time you price a plugin. The platform keeps 30% of each
sale. An account that installs a priced plugin without a licence gets 402 with "code": "plugin_purchase_required".
GET /earnings/summary, GET /earnings/sales and GET /payouts report what you have earned.
Catalog and installs
GET /marketplace every listed plugin, plus your own private ones
GET /marketplace/{slug} readme, capabilities, what it can reach, installed or not
GET /marketplace takes q (search text), repeated kinds, categories and madeBy (Official, Verified or
Community), isPriced, hideInstalled, sort (installs by default, newest, updated or name), offset,
and limit (24 by default, 60 at most).
POST /installations { "slug": "weather", "versionPolicy": "Latest", "agentIds": ["…"] }
GET /installations optional pluginId, state (needs-you, update or pinned), q
GET /installations/{id}
POST /installations/{id}/configure { "settings": { "units": "metric" }, "secrets": { "apiKey": "…" } }
PATCH /installations/{id} { "versionPolicy": "Pinned", "pinnedVersionId": "…" }
PUT /installations/{id}/agents { "scope": "Chosen", "agentIds": ["…"] }
PATCH /installations/{id}/capabilities/{activationId} { "isEnabled": false }
POST /installations/{id}/accept-update { "versionId": "…" }
POST /installations/{id}/rerun-setup
DELETE /installations/{id}
POST /installationsreturns201with{ installationId, slug };409if the account already has the plugin.versionPolicy:Latest(default, also used when the value is missing or unrecognised),Pinned(needspinnedVersionIdonPATCH), orDraft(your own plugin only;403otherwise).agentIdslimits the install to those agents; leave it out for all agents.PUT …/agentschanges it later:scopeisAllorChosen, andagentIdsis read only forChosen.PATCH …/capabilities/{activationId}turns one of the install's capabilities on or off.configuretakes setting values as strings and replaces the installation's non-secret settings, so send every value; secrets are write-only and never returned, and a blank secret leaves the stored one untouched.accept-updateaccepts a version that asks for more access.rerun-setupsendspluginInstalledagain (after a plan upgrade, for example).DELETEuninstalls: the plugin's agents (and their chats), providers, channels, live connections, sign-ins and stored secrets are removed, andpluginUninstalledis sent.
Logs
GET /plugin-logs?installationId={id}&levels=warn&levels=error&limit=100&cursor=…
GET /plugin-logs/counts?installationId={id}&since=2026-01-01T00:00:00Z
The host.log lines your account's installations produced, newest first, kept 24 hours. Filters: installationId,
sessionId, repeated levels (debug, info, warn, error), since, search, repeated entrypoints. limit
is 50 by default; page with cursor. counts needs installationId and since.
Inbound webhooks for plugin channels
A channel plugin that receives by webhook gets one address per connected channel:
https://webhook.agentparley.ai/webhook/plugin/{routingId}
The channel's page in the console shows it (and can replace it with a new address). Everything the vendor POSTs
there reaches your channel.receive — headers, the text body, and the exact bytes for signature checks. Requests
are queued on arrival, so messages sent while AgentParley deploys are delivered afterwards rather than bounced.