Skip to main content
OpenAPI

Enterprise Marketplace APIs

Manage enterprise collections, Plugins, Skills, Connectors, categories, and distribution policies.

Use the Enterprise Marketplace APIs to publish your team’s Plugins, Skills, and Connectors to organization collections and manage visibility, categories, and distribution.
Your organization must have the enterprise marketplace capability enabled. Use an API Key belonging to that organization.
Read Get API Key and Conventions before calling these APIs.

Requirements

  • Base URL: https://api.qoder.com.
  • Common path prefix: /v1/organizations/{organization_id}/apphub.
  • Send Authorization: Bearer <api_key>. The key must belong to organization_id.
  • Use Content-Type: application/json for JSON requests. For uploads, use multipart/form-data and let your HTTP client generate the boundary.
  • Example UUIDs are placeholders. Replace them with actual resource IDs. Organizations, keys, and content are isolated by region.
Path parameterTypeDescription
organization_idstringRequired for every endpoint; organization ID.
collection_idstringRequired when present in the path; id returned by collection APIs.
extension_idstringRequired when present in the path; id returned by extension APIs.
category_idstringRequired when present in the path; id returned by category APIs.

Visibility and distribution

A collection’s visibility controls whether members can see it in the marketplace. An extension’s distribution controls which members receive it and how it is enabled. Forced and silent enablement apply to extensions, not collections. Use the Group APIs to obtain group id values for targetGroupIds. These must be groups in the same organization, not member IDs or billing group IDs. Omitting distribution when uploading or registering defaults to scope=none and clientPolicy=configurable; it does not distribute to everyone. Package updates preserve the existing policy. When changing scope to none, omitting clientPolicy preserves the current client policy. Clients receive committed changes through their normal synchronization mechanism.

Pagination and request conventions

  • Lists use maxResults (default 20, range 1–100) and nextToken. The last page explicitly returns nextToken: ""; empty lists use [].
  • Keep the organization, path, filters, and page size unchanged on subsequent pages, replacing only nextToken. Do not parse or construct tokens. Pagination does not guarantee snapshot consistency.
  • Queries and updates return 200; creates, uploads, and imports return 201; deletes return 204 with no body.
  • Unknown fields, duplicate JSON keys, and multiple JSON values are rejected. JSON metadata is limited to 64 KiB. Text limits are measured in UTF-8 bytes.
  • Before updating Connector metadata or distribution, retrieve its details and pass revision as expectedRevision. Conflicts return 409. Plugin and Skill requests must not include this field.
  • After a lost response or timeout, query the resource before retrying, especially for generated versions and overwrite imports.

Data models

Collection

FieldTypeDescription
idstringCollection ID.
organizationIdstringOrganization ID.
namestringCollection name.
displayNamestringDisplay name.
descriptionstringCollection description.
typestringplugin, skill, or connector; immutable.
productsstring[]Target products, e.g. ["qoder"].
visibilityVisibilityMarketplace visibility.
createdAt / updatedAtstringCreation/update timestamps in RFC3339 format.

Visibility

FieldTypeDescription
scopestringnone: hidden from everyone; all: visible to everyone; group: visible to selected groups.
targetGroupIdsstring[]Required and non-empty only for group; up to 1,000 unique group IDs from this organization. Omit or use [] for other scopes.

Distribution

FieldTypeDescription
scopestringnone: no distribution; all: all members; group: selected groups.
targetGroupIdsstring[]Required and non-empty only for group; up to 1,000 unique group IDs from this organization. Omit or use [] for other scopes.
clientPolicystring / nullconfigurable: users can enable or disable; force_enabled: visible and forcibly enabled; silent_enabled: silently enabled. Required in requests unless scope is none. Responses can contain null for legacy combinations that do not map to these policies.

Extension

FieldTypeDescription
id / organizationId / collectionIdstringExtension, organization, and collection IDs.
typestringplugin, skill, or connector.
name / displayName / descriptionstringName, display name, and description.
versionstringContent version.
categoryIdstring / nullCategory ID; null when uncategorized.
clientsstring[]Target products inherited from the collection.
statusstringExtension status, e.g. active or archived.
distributionDistributionDistribution scope and client policy.
fileHashstringPackage file hash; can be empty when no package exists.
fileSizeintegerPackage size in bytes.
configConnectorConfigPublic connection configuration; omitted when unavailable.
revisionintegerReturned only for Connectors; send as expectedRevision when updating.
createdAt / updatedAtstringRFC3339 timestamps.

Category

FieldTypeDescription
id / organizationIdstringCategory and organization IDs.
namestringCategory name.
sortOrderintegerSort order.
usageCountintegerNumber of extensions referencing the category.
isSystembooleanWhether this is a system category. uncategorized cannot be modified or deleted.
createdAt / updatedAtstringRFC3339 timestamps.

Changelog

FieldTypeDescription
idstringChangelog ID.
actionstringAction, e.g. created, updated, scope_changed, status_changed, archived.
version / scope / statusstringVersion, distribution scope, and status recorded for the change.
changeNotestringChange note.
operator.typestringuser or apiKey.
operator.idstringUser ID or organization API Key ID, never the API Key secret.
operator.emailstringUser email; omitted when unavailable.
createdAtstringRFC3339 timestamp.

MarketplaceCandidate

FieldTypeDescription
sourceTypestringqoder_plugin or qoder_skill.
sourceIdstringPublic marketplace item ID for the import request.
name / displayName / description / versionstringName, display name, description, and version of the public item.
statusstringimportable, confirm_required, or not_importable.
reasonstringReason, e.g. same_collection_name_conflict or other_collection_name_conflict; empty when no reason applies.
existingExtensionIdstring / nullConflicting enterprise extension ID; null when none exists.

ConnectorConfig

FieldTypeDescription
serversobject[]Required; exactly one server.
servers[].namestringRequired; non-empty after trimming whitespace. Control characters are not allowed.
servers[].protocolstringRequired; supported values: streamable_http, sse, stdio.
servers[].urlstringRequired for streamable_http or sse. Must be an HTTPS URL without user information, query parameters (including an empty ?), or a fragment. Do not set for stdio.
servers[].commandstringRequired for stdio; cannot be blank or contain line breaks or null characters. Do not set for remote protocols.
servers[].argsstring[]Optional command arguments for stdio only; do not configure for remote protocols.
servers[].envobjectOptional environment variables with string values for stdio only. Credentials are not accepted; do not configure for remote protocols.
servers[].enabledbooleanOptional; defaults to true.
servers[].runtimeKindstringOptional; only client_direct is supported and is the default.
servers[].descriptionstringOptional description.

Download

FieldTypeDescription
extensionId / versionstringEnterprise extension ID and version.
downloadUrlstringTemporary download URL.
expiresAtstringURL expiration timestamp in RFC3339 format.
fileHashstringPackage file hash.
fileSizeintegerPackage size in bytes.

ImportResult

FieldTypeDescription
extensionIdstringImported enterprise extension ID; retrieve its details for the full object.
sourceIdstringPublic source item ID.
name / versionstringImported item name and version.
Unavailable timestamps can be empty strings. ConnectorConfig accepts only the listed connection fields. Protocols, URLs, and runtime kinds must satisfy the product’s connection rules; OAuth credentials and authentication configurations are not accepted.

API reference

APIMethodRelative path
List collectionsGET/extension-collections
Get a collectionGET/extension-collections/{collection_id}
Create a collectionPOST/extension-collections
Update a collectionPUT/extension-collections/{collection_id}
Delete a collectionDELETE/extension-collections/{collection_id}
List extensionsGET/extensions
Get an extensionGET/extensions/{extension_id}
Upload and publish to a collectionPOST/extension-collections/{collection_id}/extensions
Update an extension packagePUT/extensions/{extension_id}/package
Update extension metadataPUT/extensions/{extension_id}/metadata
Update extension distributionPUT/extensions/{extension_id}/distribution
Delete an extensionDELETE/extensions/{extension_id}
List extension changelogsGET/extensions/{extension_id}/changelogs
Get an extension download URLGET/extensions/{extension_id}/download
List categoriesGET/extension-categories
Get a categoryGET/extension-categories/{category_id}
Create a categoryPOST/extension-categories
Update a categoryPUT/extension-categories/{category_id}
Delete a categoryDELETE/extension-categories/{category_id}
Update multiple categoriesPUT/extension-categories
List public marketplace import candidatesGET/extension-collections/{collection_id}/marketplace-items
Import a public marketplace itemPOST/extension-collections/{collection_id}/imports
Register a ConnectorPOST/extension-collections/{collection_id}/connectors

1. List collections

GET /v1/organizations/{organization_id}/apphub/extension-collections

Query parameters

ParameterTypeRequiredDescription
keywordstringNoSearch keyword.
typestringNoContent type: plugin, skill, or connector.
productstringNoProduct: qoder.
visibilityScopestringNoVisibility scope: none, all, or group.
maxResultsintegerNoPage size, 1–100; default: 20.
nextTokenstringNoCursor returned by the previous page; omit for the first page.

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections?maxResults=20" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "collections": [
    {
      "id": "32345678-1234-4234-8234-123456789abc",
      "organizationId": "12345678-1234-4234-8234-123456789abc",
      "name": "team-skills",
      "displayName": "Team Skills",
      "description": "Shared team skills",
      "type": "skill",
      "products": [
        "qoder"
      ],
      "visibility": {
        "scope": "all",
        "targetGroupIds": []
      },
      "createdAt": "2026-11-11T08:00:00Z",
      "updatedAt": "2026-11-11T08:00:00Z"
    }
  ],
  "maxResults": 20,
  "nextToken": ""
}

2. Get a collection

GET /v1/organizations/{organization_id}/apphub/extension-collections/{collection_id}

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections/32345678-1234-4234-8234-123456789abc" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "id": "32345678-1234-4234-8234-123456789abc",
  "organizationId": "12345678-1234-4234-8234-123456789abc",
  "name": "team-skills",
  "displayName": "Team Skills",
  "description": "Shared team skills",
  "type": "skill",
  "products": [
    "qoder"
  ],
  "visibility": {
    "scope": "all",
    "targetGroupIds": []
  },
  "createdAt": "2026-11-11T08:00:00Z",
  "updatedAt": "2026-11-11T08:00:00Z"
}

3. Create a collection

POST /v1/organizations/{organization_id}/apphub/extension-collections

Request parameters

ParameterTypeRequiredDescription
namestringYesNon-empty name, up to 128 UTF-8 bytes.
displayNamestringNoDisplay name, up to 256 UTF-8 bytes.
descriptionstringNoDescription, up to 2,048 UTF-8 bytes.
typestringYesplugin, skill, or connector; immutable after creation.
productsstring[]YesTarget products: ["qoder"]. Not editable through the update API.
visibilityobjectYesMarketplace visibility of the collection; see Visibility.

Request body example

{
  "name": "team-skills",
  "displayName": "Team Skills",
  "description": "Shared team skills",
  "type": "skill",
  "products": [
    "qoder"
  ],
  "visibility": {
    "scope": "all",
    "targetGroupIds": []
  }
}

Request example

curl --request POST \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"name":"team-skills","displayName":"Team Skills","description":"Shared team skills","type":"skill","products":["qoder"],"visibility":{"scope":"all","targetGroupIds":[]}}'

Success response (201 Created)

{
  "collection": {
    "id": "32345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "name": "team-skills",
    "displayName": "Team Skills",
    "description": "Shared team skills",
    "type": "skill",
    "products": [
      "qoder"
    ],
    "visibility": {
      "scope": "all",
      "targetGroupIds": []
    },
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z"
  }
}

4. Update a collection

PUT /v1/organizations/{organization_id}/apphub/extension-collections/{collection_id} Provide at least one field. Omitted fields are unchanged. type and products are immutable.

Request parameters

ParameterTypeRequiredDescription
namestringNoNon-empty; up to 128 UTF-8 bytes.
displayNamestringNoUp to 256 UTF-8 bytes.
descriptionstringNoUp to 2,048 UTF-8 bytes.
visibilityobjectNoNew marketplace visibility; see Visibility.

Request body example

{
  "displayName": "Engineering Skills"
}

Request example

curl --request PUT \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections/32345678-1234-4234-8234-123456789abc" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"displayName":"Engineering Skills"}'

Success response (200 OK)

{
  "collection": {
    "id": "32345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "name": "team-skills",
    "displayName": "Engineering Skills",
    "description": "Shared team skills",
    "type": "skill",
    "products": [
      "qoder"
    ],
    "visibility": {
      "scope": "all",
      "targetGroupIds": []
    },
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z"
  }
}

5. Delete a collection

DELETE /v1/organizations/{organization_id}/apphub/extension-collections/{collection_id} Only empty collections can be deleted. A non-empty collection returns 409.

Request example

curl --request DELETE \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections/32345678-1234-4234-8234-123456789abc" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (204 No Content)

No response body.

6. List extensions

GET /v1/organizations/{organization_id}/apphub/extensions

Query parameters

ParameterTypeRequiredDescription
collectionIdstringNoFilter by collection ID.
keywordstringNoSearch keyword.
typestringNoplugin, skill, or connector.
statusstringNoExtension status: active or archived.
scopestringNoDistribution scope: none, all, or group.
containsstringNoPackage component type: hooks, skills, rules, mcp, agents, commands. Repeat the parameter for multiple values, e.g. contains=skills&contains=commands.
maxResultsintegerNoPage size, 1–100; default: 20.
nextTokenstringNoCursor returned by the previous page; omit for the first page.

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extensions?maxResults=20" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "extensions": [
    {
      "id": "42345678-1234-4234-8234-123456789abc",
      "organizationId": "12345678-1234-4234-8234-123456789abc",
      "collectionId": "32345678-1234-4234-8234-123456789abc",
      "type": "skill",
      "name": "code-review",
      "displayName": "Code Review",
      "description": "Review code changes",
      "version": "1.0.0",
      "categoryId": null,
      "clients": [
        "qoder"
      ],
      "status": "active",
      "distribution": {
        "scope": "none",
        "targetGroupIds": [],
        "clientPolicy": "configurable"
      },
      "fileHash": "example-file-hash",
      "fileSize": 1024,
      "createdAt": "2026-11-11T08:00:00Z",
      "updatedAt": "2026-11-11T08:00:00Z"
    }
  ],
  "maxResults": 20,
  "nextToken": ""
}

7. Get an extension

GET /v1/organizations/{organization_id}/apphub/extensions/{extension_id}

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extensions/42345678-1234-4234-8234-123456789abc" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "id": "42345678-1234-4234-8234-123456789abc",
  "organizationId": "12345678-1234-4234-8234-123456789abc",
  "collectionId": "32345678-1234-4234-8234-123456789abc",
  "type": "skill",
  "name": "code-review",
  "displayName": "Code Review",
  "description": "Review code changes",
  "version": "1.0.0",
  "categoryId": null,
  "clients": [
    "qoder"
  ],
  "status": "active",
  "distribution": {
    "scope": "none",
    "targetGroupIds": [],
    "clientPolicy": "configurable"
  },
  "fileHash": "example-file-hash",
  "fileSize": 1024,
  "createdAt": "2026-11-11T08:00:00Z",
  "updatedAt": "2026-11-11T08:00:00Z"
}

8. Upload and publish to a collection

POST /v1/organizations/{organization_id}/apphub/extension-collections/{collection_id}/extensions Each request accepts exactly one file part and one metadata part. The package must match the collection type. Register Connectors through the JSON endpoint. No separate upload URL or object-storage operation is required.

Request parameters

file and metadata are multipart fields. All remaining fields belong inside the metadata JSON, not in separate parts.
ParameterTypeRequiredDescription
filefileYesOne Plugin or Skill ZIP, up to 50 MiB.
metadataobjectYesJSON part with Content-Type application/json; use {} when no options are needed. Supports the metadata fields below.
versionstringNoOptional Skill version; generated when omitted. Plugin versions come from the package manifest and cannot be overridden.
categoryIdstringNoEnterprise category ID; omit to leave uncategorized.
distributionobjectNoDistribution scope and client policy; defaults to none/configurable. See Distribution.
changeNotestringNoChange note, up to 1,024 UTF-8 bytes.

Request example

curl --request POST \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections/32345678-1234-4234-8234-123456789abc/extensions" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --form "file=@skill.zip;type=application/zip" \
  --form 'metadata={};type=application/json'

Success response (201 Created)

{
  "extension": {
    "id": "42345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "collectionId": "32345678-1234-4234-8234-123456789abc",
    "type": "skill",
    "name": "code-review",
    "displayName": "Code Review",
    "description": "Review code changes",
    "version": "1.0.0",
    "categoryId": null,
    "clients": [
      "qoder"
    ],
    "status": "active",
    "distribution": {
      "scope": "none",
      "targetGroupIds": [],
      "clientPolicy": "configurable"
    },
    "fileHash": "example-file-hash",
    "fileSize": 1024,
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z"
  }
}

9. Update an extension package

PUT /v1/organizations/{organization_id}/apphub/extensions/{extension_id}/package Applies to Plugin and Skill. Preserves the extension ID, collection, category, and distribution. metadata does not accept categoryId or distribution. The example assumes the Skill is currently at version 1.0.0; omitting version upgrades it to 2.0.0.

Request parameters

file and metadata are multipart fields. All remaining fields belong inside the metadata JSON, not in separate parts.
ParameterTypeRequiredDescription
filefileYesOne replacement ZIP, up to 50 MiB.
metadataobjectYesapplication/json part supporting version and changeNote; use {} when empty.
versionstringNoSkill only: specify a semantic version strictly greater than the current version. When omitted, the major version increases by one and minor and patch reset to zero, for example 1.0.0 becomes 2.0.0. Plugin versions come from the package manifest, must also be strictly greater than the current version, and cannot be overridden through metadata.
changeNotestringNoChange note, up to 1,024 UTF-8 bytes.

Request example

curl --request PUT \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extensions/42345678-1234-4234-8234-123456789abc/package" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --form "file=@skill.zip;type=application/zip" \
  --form 'metadata={};type=application/json'

Success response (200 OK)

{
  "extension": {
    "id": "42345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "collectionId": "32345678-1234-4234-8234-123456789abc",
    "type": "skill",
    "name": "code-review",
    "displayName": "Code Review",
    "description": "Review code changes",
    "version": "2.0.0",
    "categoryId": null,
    "clients": [
      "qoder"
    ],
    "status": "active",
    "distribution": {
      "scope": "none",
      "targetGroupIds": [],
      "clientPolicy": "configurable"
    },
    "fileHash": "example-file-hash",
    "fileSize": 1024,
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z"
  }
}

10. Update extension metadata

PUT /v1/organizations/{organization_id}/apphub/extensions/{extension_id}/metadata Provide at least one of displayName, description, categoryId, or config. Connector updates also require expectedRevision.

Request parameters

ParameterTypeRequiredDescription
displayNamestringNoDisplay name.
descriptionstringNoDescription.
categoryIdstring / nullNoCategory ID; null clears the category; omission preserves it.
configobjectNoConnector only; see ConnectorConfig.
expectedRevisionintegerNoRequired for Connector updates: use the current revision (at least 1). Not accepted for Plugin or Skill.
changeNotestringNoChange note, up to 1,024 UTF-8 bytes.

Request body example

{
  "description": "Updated review instructions"
}

Request example

curl --request PUT \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extensions/42345678-1234-4234-8234-123456789abc/metadata" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"description":"Updated review instructions"}'

Success response (200 OK)

{
  "extension": {
    "id": "42345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "collectionId": "32345678-1234-4234-8234-123456789abc",
    "type": "skill",
    "name": "code-review",
    "displayName": "Code Review",
    "description": "Updated review instructions",
    "version": "1.0.0",
    "categoryId": null,
    "clients": [
      "qoder"
    ],
    "status": "active",
    "distribution": {
      "scope": "none",
      "targetGroupIds": [],
      "clientPolicy": "configurable"
    },
    "fileHash": "example-file-hash",
    "fileSize": 1024,
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z"
  }
}

11. Update extension distribution

PUT /v1/organizations/{organization_id}/apphub/extensions/{extension_id}/distribution

Request parameters

ParameterTypeRequiredDescription
scopestringYesnone, all, or group.
targetGroupIdsstring[]NoRequired only for scope=group; up to 1,000 unique group IDs belonging to this organization.
clientPolicystringNoRequired unless scope=none: configurable, force_enabled, or silent_enabled. Omit with scope=none to preserve the existing policy.
expectedRevisionintegerNoRequired for Connector updates: use the current revision (at least 1). Not accepted for Plugin or Skill.
changeNotestringNoChange note, up to 1,024 UTF-8 bytes.

Request body example

{
  "scope": "group",
  "targetGroupIds": [
    "62345678-1234-4234-8234-123456789abc"
  ],
  "clientPolicy": "force_enabled"
}

Request example

curl --request PUT \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extensions/42345678-1234-4234-8234-123456789abc/distribution" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"scope":"group","targetGroupIds":["62345678-1234-4234-8234-123456789abc"],"clientPolicy":"force_enabled"}'

Success response (200 OK)

{
  "extension": {
    "id": "42345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "collectionId": "32345678-1234-4234-8234-123456789abc",
    "type": "skill",
    "name": "code-review",
    "displayName": "Code Review",
    "description": "Review code changes",
    "version": "1.0.0",
    "categoryId": null,
    "clients": [
      "qoder"
    ],
    "status": "active",
    "distribution": {
      "scope": "group",
      "targetGroupIds": [
        "62345678-1234-4234-8234-123456789abc"
      ],
      "clientPolicy": "force_enabled"
    },
    "fileHash": "example-file-hash",
    "fileSize": 1024,
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z"
  }
}

12. Delete an extension

DELETE /v1/organizations/{organization_id}/apphub/extensions/{extension_id} Removes the enterprise extension and performs associated package cleanup. Recovery is not guaranteed. The public marketplace source is not deleted.

Request example

curl --request DELETE \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extensions/42345678-1234-4234-8234-123456789abc" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (204 No Content)

No response body.

13. List extension changelogs

GET /v1/organizations/{organization_id}/apphub/extensions/{extension_id}/changelogs

Query parameters

ParameterTypeRequiredDescription
maxResultsintegerNoPage size, 1–100; default: 20.
nextTokenstringNoCursor returned by the previous page; omit for the first page.

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extensions/42345678-1234-4234-8234-123456789abc/changelogs?maxResults=20" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "changelogs": [
    {
      "id": "72345678-1234-4234-8234-123456789abc",
      "action": "created",
      "version": "1.0.0",
      "scope": "none",
      "status": "active",
      "changeNote": "Initial release",
      "operator": {
        "type": "apiKey",
        "id": "22345678-1234-4234-8234-123456789abc"
      },
      "createdAt": "2026-11-11T08:00:00Z"
    }
  ],
  "maxResults": 20,
  "nextToken": ""
}

14. Get an extension download URL

GET /v1/organizations/{organization_id}/apphub/extensions/{extension_id}/download Plugin and Skill only; Skill downloads contain the original Skill ZIP. URLs are temporary. The response uses Cache-Control: no-store; do not cache or publicly share the URL.

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extensions/42345678-1234-4234-8234-123456789abc/download" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "extensionId": "42345678-1234-4234-8234-123456789abc",
  "version": "1.0.0",
  "downloadUrl": "https://example.com/package.zip?signature=example",
  "expiresAt": "2026-11-11T08:10:00Z",
  "fileHash": "example-file-hash",
  "fileSize": 1024
}

15. List categories

GET /v1/organizations/{organization_id}/apphub/extension-categories

Query parameters

ParameterTypeRequiredDescription
keywordstringNoSearch keyword.
maxResultsintegerNoPage size, 1–100; default: 20.
nextTokenstringNoCursor returned by the previous page; omit for the first page.

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-categories?maxResults=20" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "categories": [
    {
      "id": "52345678-1234-4234-8234-123456789abc",
      "organizationId": "12345678-1234-4234-8234-123456789abc",
      "name": "Engineering",
      "sortOrder": 10,
      "usageCount": 0,
      "isSystem": false,
      "createdAt": "2026-11-11T08:00:00Z",
      "updatedAt": "2026-11-11T08:00:00Z"
    }
  ],
  "maxResults": 20,
  "nextToken": ""
}

16. Get a category

GET /v1/organizations/{organization_id}/apphub/extension-categories/{category_id}

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-categories/52345678-1234-4234-8234-123456789abc" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "id": "52345678-1234-4234-8234-123456789abc",
  "organizationId": "12345678-1234-4234-8234-123456789abc",
  "name": "Engineering",
  "sortOrder": 10,
  "usageCount": 0,
  "isSystem": false,
  "createdAt": "2026-11-11T08:00:00Z",
  "updatedAt": "2026-11-11T08:00:00Z"
}

17. Create a category

POST /v1/organizations/{organization_id}/apphub/extension-categories

Request parameters

ParameterTypeRequiredDescription
namestringYesNon-empty name, up to 128 UTF-8 bytes.
sortOrderintegerNoSort order (int32).

Request body example

{
  "name": "Engineering",
  "sortOrder": 10
}

Request example

curl --request POST \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-categories" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"name":"Engineering","sortOrder":10}'

Success response (201 Created)

{
  "category": {
    "id": "52345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "name": "Engineering",
    "sortOrder": 10,
    "usageCount": 0,
    "isSystem": false,
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z"
  }
}

18. Update a category

PUT /v1/organizations/{organization_id}/apphub/extension-categories/{category_id} Provide at least one field. The system category uncategorized cannot be modified.

Request parameters

ParameterTypeRequiredDescription
namestringNoNon-empty category name, up to 128 UTF-8 bytes.
sortOrderintegerNoSort order (int32).

Request body example

{
  "sortOrder": 20
}

Request example

curl --request PUT \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-categories/52345678-1234-4234-8234-123456789abc" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"sortOrder":20}'

Success response (200 OK)

{
  "category": {
    "id": "52345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "name": "Engineering",
    "sortOrder": 20,
    "usageCount": 0,
    "isSystem": false,
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z"
  }
}

19. Delete a category

DELETE /v1/organizations/{organization_id}/apphub/extension-categories/{category_id} Returns 409 while referenced by extensions; update their categories first. The system category uncategorized cannot be deleted.

Request example

curl --request DELETE \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-categories/52345678-1234-4234-8234-123456789abc" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (204 No Content)

No response body.

20. Update multiple categories

PUT /v1/organizations/{organization_id}/apphub/extension-categories Batch operations apply only to category management, not ZIP uploads. Unspecified categories are not deleted.

Request parameters

ParameterTypeRequiredDescription
itemsobject[]Yes1–1,000 items with unique IDs; only specified items are updated.
items[].idstringYesCategory ID; cannot be uncategorized.
items[].namestringNoNon-empty, up to 128 UTF-8 bytes. Provide name or sortOrder, or both.
items[].sortOrderintegerNoSort order (int32).

Request body example

{
  "items": [
    {
      "id": "52345678-1234-4234-8234-123456789abc",
      "sortOrder": 20
    }
  ]
}

Request example

curl --request PUT \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-categories" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"items":[{"id":"52345678-1234-4234-8234-123456789abc","sortOrder":20}]}'

Success response (200 OK)

{
  "categories": [
    {
      "id": "52345678-1234-4234-8234-123456789abc",
      "organizationId": "12345678-1234-4234-8234-123456789abc",
      "name": "Engineering",
      "sortOrder": 20,
      "usageCount": 0,
      "isSystem": false,
      "createdAt": "2026-11-11T08:00:00Z",
      "updatedAt": "2026-11-11T08:00:00Z"
    }
  ]
}

21. List public marketplace import candidates

GET /v1/organizations/{organization_id}/apphub/extension-collections/{collection_id}/marketplace-items Lists candidates for the target collection type. Check status, reason, and existingExtensionId before importing.

Query parameters

ParameterTypeRequiredDescription
keywordstringNoSearch keyword.
maxResultsintegerNoPage size, 1–100; default: 20.
nextTokenstringNoCursor returned by the previous page; omit for the first page.

Request example

curl --request GET \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections/32345678-1234-4234-8234-123456789abc/marketplace-items?maxResults=20" \
  --header "Authorization: Bearer $QODER_API_KEY"

Success response (200 OK)

{
  "items": [
    {
      "sourceType": "qoder_skill",
      "sourceId": "82345678-1234-4234-8234-123456789abc",
      "name": "code-review",
      "displayName": "Code Review",
      "description": "Review code changes",
      "version": "1.0.0",
      "status": "importable",
      "reason": "",
      "existingExtensionId": null
    }
  ],
  "maxResults": 20,
  "nextToken": ""
}

22. Import a public marketplace item

POST /v1/organizations/{organization_id}/apphub/extension-collections/{collection_id}/imports Imports one item per request. overwrite may create a new enterprise extension ID and does not preserve the old distribution policy; the default scope is none. The public source remains unchanged. After failure or timeout, query the collection before retrying.

Request parameters

ParameterTypeRequiredDescription
sourceTypestringYesqoder_plugin or qoder_skill.
sourceIdstringYesPublic item ID from the candidate list.
categoryIdstringNoEnterprise category ID; omit to leave uncategorized.
onConflictstringNoreject (default) or overwrite.

Request body example

{
  "sourceType": "qoder_skill",
  "sourceId": "82345678-1234-4234-8234-123456789abc",
  "onConflict": "reject"
}

Request example

curl --request POST \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections/32345678-1234-4234-8234-123456789abc/imports" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"sourceType":"qoder_skill","sourceId":"82345678-1234-4234-8234-123456789abc","onConflict":"reject"}'

Success response (201 Created)

{
  "extensionId": "42345678-1234-4234-8234-123456789abc",
  "sourceId": "82345678-1234-4234-8234-123456789abc",
  "name": "code-review",
  "version": "1.0.0"
}

23. Register a Connector

POST /v1/organizations/{organization_id}/apphub/extension-collections/{collection_id}/connectors The collection type must be connector. Uses JSON, not a ZIP upload. OAuth credentials and authentication configurations are not accepted.

Request parameters

ParameterTypeRequiredDescription
namestringYesConnector identifier, up to 128 UTF-8 bytes. Use lowercase letters and digits, optionally separated by single hyphens, for example docs-search. Leading, trailing, and consecutive hyphens are not allowed. Use displayName for the human-readable name.
displayNamestringYesDisplay name.
descriptionstringNoDescription.
categoryIdstringNoEnterprise category ID; omit to leave uncategorized.
configobjectYesSee ConnectorConfig; exactly one server is required.
distributionobjectNoDistribution scope and client policy; defaults to none/configurable. See Distribution.
changeNotestringNoChange note, up to 1,024 UTF-8 bytes.

Request body example

{
  "name": "docs",
  "displayName": "Docs",
  "config": {
    "servers": [
      {
        "name": "docs",
        "protocol": "streamable_http",
        "url": "https://example.com/mcp"
      }
    ]
  }
}

Request example

curl --request POST \
  "https://api.qoder.com/v1/organizations/12345678-1234-4234-8234-123456789abc/apphub/extension-collections/32345678-1234-4234-8234-123456789abc/connectors" \
  --header "Authorization: Bearer $QODER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"name":"docs","displayName":"Docs","config":{"servers":[{"name":"docs","protocol":"streamable_http","url":"https://example.com/mcp"}]}}'

Success response (201 Created)

{
  "extension": {
    "id": "42345678-1234-4234-8234-123456789abc",
    "organizationId": "12345678-1234-4234-8234-123456789abc",
    "collectionId": "32345678-1234-4234-8234-123456789abc",
    "type": "connector",
    "name": "docs",
    "displayName": "Docs",
    "description": "Review code changes",
    "version": "",
    "categoryId": null,
    "clients": [
      "qoder"
    ],
    "status": "active",
    "distribution": {
      "scope": "none",
      "targetGroupIds": [],
      "clientPolicy": "configurable"
    },
    "fileHash": "",
    "fileSize": 0,
    "createdAt": "2026-11-11T08:00:00Z",
    "updatedAt": "2026-11-11T08:00:00Z",
    "revision": 1,
    "config": {
      "servers": [
        {
          "name": "docs",
          "protocol": "streamable_http",
          "url": "https://example.com/mcp",
          "enabled": true,
          "runtimeKind": "client_direct"
        }
      ]
    }
  }
}

Upload limits

Each request accepts one ZIP containing one standalone Plugin or Skill. A Plugin can contain multiple nested Skills. The file limit is 50 MiB; the full multipart request limit is 51 MiB. Uncompressed ZIP contents are limited to 256 MiB and 10,000 entries. Path traversal, symbolic links, corrupted packages, and batches of independent extensions are rejected. Use the management endpoints to change categories or distribution separately from a package update.

ZIP directory structure

A standalone Skill must contain exactly one top-level directory. Its directory name becomes the Skill name, and it must contain a case-sensitive SKILL.md file. Do not place SKILL.md directly at the ZIP root. The archive must contain exactly one SKILL.md and must not contain .qoder-plugin/plugin.json. For example:
code-review/
└── SKILL.md
Run the following command from the parent directory of code-review to preserve this layout:
zip -r skill.zip code-review/
A Plugin must contain exactly one .qoder-plugin/plugin.json, either at the ZIP root or inside a single top-level directory. If a wrapper directory is used, all package files must be inside it; additional wrapper levels are not supported. This example uses the ZIP root:
.qoder-plugin/
└── plugin.json
The name in plugin.json supplies the Plugin name, and version supplies its package version. Updates require a version strictly greater than the current version. Minimal example:
{
  "name": "team-tools",
  "version": "1.0.0"
}
Run the following command from the directory containing .qoder-plugin. Include any additional skills, commands, or other Plugin files in the archive as well. Nested Skills within a Plugin do not count as multiple independent uploads.
zip -r plugin.zip .qoder-plugin/

Error responses

Errors follow the OpenAPI conventions. Keep requestId for troubleshooting. Common HTTP statuses are listed below; use the actual code from the response.
{
  "requestId": "example-request-id",
  "code": "BadRequest",
  "message": "invalid request body"
}
HTTPDescription
400Invalid parameters, package format, or type; missing Connector expectedRevision.
401Authentication failed.
403Organization mismatch, capability unavailable, or insufficient permissions.
404Collection, extension, category, or import source not found.
409Name/version conflict; non-empty collection or referenced category cannot be deleted; import conflict.
413Upload request or file exceeds the size limit.
429Too many requests or upload concurrency limit reached.
500Internal server error.
503A dependent service is temporarily unavailable.
For marketplace imports, name conflicts return 409/AlreadyExists; a missing source returns 404/NotFound; a type mismatch, delisted item, or parsing failure returns 400/BadRequest; download/upload dependency failures return 503/DEPENDENCY_UNAVAILABLE. Overwrite imports do not guarantee atomic rollback: query the collection after a failure.