> ## Documentation Index
> Fetch the complete documentation index at: https://noorle.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload Plugin

> POST /v1/capabilities/upload takes a .npack archive as multipart form-data and registers a new plugin version

Upload a `.npack` archive to register a plugin, or add a version to one that
already exists.

```
POST https://api.noorle.com/v1/capabilities/upload
Content-Type: multipart/form-data
```

## Request

The form field must be named **`archive`**, and the filename must end in
`.npack`. Any other field name is ignored; a request with no `archive` field is
rejected.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.noorle.com/v1/capabilities/upload \
    -H "Authorization: Bearer ak-YOUR_KEY" \
    -F "archive=@dist/my-plugin.npack"
  ```

  ```python Python theme={null}
  import requests

  with open("dist/my-plugin.npack", "rb") as f:
      resp = requests.post(
          "https://api.noorle.com/v1/capabilities/upload",
          files={"archive": ("my-plugin.npack", f)},
          headers={"Authorization": "Bearer ak-YOUR_KEY"},
          timeout=120,
      )
  result = resp.json()["data"]
  ```

  ```typescript TypeScript theme={null}
  const form = new FormData();
  form.append("archive", npackBlob, "my-plugin.npack");

  const resp = await fetch(
    "https://api.noorle.com/v1/capabilities/upload",
    {
      method: "POST",
      headers: { Authorization: "Bearer ak-YOUR_KEY" },
      body: form,
    },
  );
  const { data } = await resp.json();
  ```
</CodeGroup>

| Field     | Type | Required | Notes                                                        |
| --------- | ---- | -------- | ------------------------------------------------------------ |
| `archive` | file | Yes      | Must have a filename ending in `.npack` and a non-empty body |

## Response

Wrapped in a `data` envelope, like every management API response.

```json theme={null}
{
  "data": {
    "capability_id": "0198f0c2-1a3d-7c41-9b2e-8f5a6d3c1e07",
    "capability_name": "my-plugin",
    "version": 2,
    "files_processed": ["wasm", "noorle", "wit"]
  }
}
```

| Field             | Type      | Description                                                            |
| ----------------- | --------- | ---------------------------------------------------------------------- |
| `capability_id`   | UUID      | The capability this version belongs to                                 |
| `capability_name` | string    | Resolved plugin name (see below)                                       |
| `version`         | integer   | Version number, incremented per upload                                 |
| `files_processed` | string\[] | Which kinds of entry were recognized — short tokens, **not filenames** |
| `schema_warnings` | array     | Non-fatal tool-schema warnings; omitted when empty                     |

`files_processed` uses one fixed token per entry kind, in this order:
`wasm` (always present), `noorle`, `env`, `wit`. Do not parse it for
filenames — the archive's actual filenames are not echoed back.

## Archive contents

`.npack` is a gzip-compressed tar archive. Entries are matched by **pattern, not
by fixed filename**:

| Entry         | Required | Matched by                                              |
| ------------- | -------- | ------------------------------------------------------- |
| The component | **Yes**  | Any `*.wasm`. If several are present, the last one wins |
| Plugin config | No       | `noorle.yaml` or `noorle.yml`                           |
| WIT interface | No       | Any `*.wit`                                             |
| Environment   | No       | `.env` or `env`                                         |

Directories are skipped; anything else is ignored.

The plugin name is resolved in this order: `metadata.name` from `noorle.yaml`,
then the archive filename minus `.npack`, then the WASM file's stem.

<Note>
  Tool discovery does not depend on the `.wit` file. Tools are extracted by
  reflecting over the component's exported functions; the WIT file, when
  present, only enriches tool descriptions.
</Note>

### Admission checks

The upload is rejected unless the WASM passes validation:

1. It must be a valid **Component Model** binary. A core WASM module is rejected.
2. Every import must begin with one of nine allowed WASI prefixes:
   `wasi:clocks/`, `wasi:random/`, `wasi:cli/`, `wasi:sockets/`, `wasi:io/`,
   `wasi:filesystem/`, `wasi:http/`, `wasi:config/`, `wasi:keyvalue/`.
3. It must export at least one callable function.

`noorle.yaml`, when present, must declare `schema_version: "1.0"` exactly, and
must pass config validation.

## Limits

| Limit            | Value                                           |
| ---------------- | ----------------------------------------------- |
| Request body     | **20 MB** — enforced by the server, returns 413 |
| Advisory warning | Logged above 20 MB of WASM binary               |

The 20 MB body limit is the management API's `upload_max_size_mb` setting and is
the value in effect in every shipped environment. Uploading through the
[Portal](https://platform.noorle.com) instead allows a larger per-file size.

## Activation

A newly registered version is created **dormant**. The upload writes the
version row, pushes its metadata, and then — in a single transaction — records
the content hashes and flips the active version. If that final step fails, the
new version stays dormant and the previously active version keeps serving.

A plugin whose `active_version` is `0` has no activated version; tool dispatch
against it fails until one is activated.

## Status codes

| Code | Meaning                                                                                                                    |
| ---- | -------------------------------------------------------------------------------------------------------------------------- |
| 200  | Version registered                                                                                                         |
| 400  | Missing `archive` field, wrong extension, empty body, unreadable archive, invalid `noorle.yaml`, or failed WASM validation |
| 403  | Missing, invalid, or insufficiently privileged credential                                                                  |
| 413  | Body exceeds 20 MB                                                                                                         |

Error bodies use the standard management API shape — see
[Errors and rate limits](/docs/reference/errors-and-rate-limits).

<Note>
  **There is no upload rate limit on this endpoint.**
</Note>

## Related

* [List Capabilities](/docs/reference/rest/capabilities-list)
* [CLI: `noorle plugin deploy`](/docs/reference/cli/commands)
