---
title: "Datapack API"
url: "https://docs.mekodata.ai/reference/datapack-api/"
---

# Datapack API

Create and manage a datapack, upload and delete knowledge files, store artifacts, and choose which memories to share

Use this HTTP API to create and manage a datapack, upload files into its knowledge base, store artifacts, and choose which private memories become shared.

The base URL is `https://api.mekodata.ai/api/v1`. It is the prefix for the paths below. A request to that URL alone, with no `Authorization` header, returns `401` and `error.message` `missing or malformed authorization header`. To check that the server is up, call `GET /health` with no key and with a `User-Agent`.

A machine-readable description of these calls is the [Datapack API OpenAPI file](/downloads/datapack-api.openapi.yaml).

These calls match MCP tools that already exist. Memory search, adding a memory, conversation capture, and knowledge search stay on the [MCP server](/reference/mcp-server/).

| MCP tool                        | HTTP call                                                                                         |
|---------------------------------|---------------------------------------------------------------------------------------------------|
| `datapack_create`               | `POST /datapacks`                                                                                 |
| `datapack_list`                 | `GET /datapacks`                                                                                  |
| `datapack_describe`             | `GET /datapacks/{datapack_id}`                                                                    |
| `datapack_update`               | `PATCH /datapacks/{datapack_id}` and `PATCH /datapacks/{datapack_id}/conversation-search-opt-out` |
| `datapack_delete`               | `DELETE /datapacks/{datapack_id}`                                                                 |
| `knowledgebase_delete_document` | `DELETE /datapacks/{datapack_id}/knowledge-bases/documents/{document_id}`                         |
| `memory_promote`                | `POST /promote-memories`                                                                          |
| `artifact_put`                  | `POST /datapacks/{datapack_id}/artifacts`                                                         |
| `artifact_get`                  | `GET /datapacks/{datapack_id}/artifacts/{content_hash}`                                           |

## Authenticate

Create a key in the portal under **Settings > API Keys**. The key starts with `mko_tkn_` and is shown once. See [API keys](/reference/api-keys/).

Send the key on every call:

```text
Authorization: Bearer mko_tkn_your_key_here
User-Agent: my-app/1.0
```

`Authorization` must start with `Bearer`, including the space. A missing `User-Agent` is rejected.

A key created in the portal can read and write. The account that owns the key is the caller. A `user_id` in a body does not move the call to another account.

| Call                                                                                | Who can make it                                                |
|-------------------------------------------------------------------------------------|----------------------------------------------------------------|
| List datapacks, or create one                                                       | The account that owns the key                                  |
| Read a datapack, list files, read upload status, list memories, or read an artifact | Any member of the datapack                                     |
| Upload a file, promote a memory, or turn conversation search off                    | The owner or a maintainer                                      |
| Delete an uploaded file                                                             | The owner, a maintainer, or the account that uploaded the file |
| Rename a datapack, or delete it                                                     | The owner                                                      |
| Mark a memory to stay private, or store an artifact                                 | Any member except a viewer                                     |

## Create a datapack

`POST /datapacks`

```bash
curl -sS -X POST "https://api.mekodata.ai/api/v1/datapacks" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d '{"name":"obligations","description":"Loan obligation documents"}'
```

`name` is required. It must be 3 to 60 characters and must not contain an HTML tag. Names are unique for the account that owns the key.

`description` is optional. Omit `agent_id` to use the account's default agent. If you send `agent_id`, that agent must already belong to the same account. Omit `folder_id`. An empty folder id is accepted, and an API key cannot look one up.

A successful call returns `201`:

```json
{
  "data": {
    "datapack_id": "e02a7107-c79e-451d-b360-9cd1e13f175d",
    "agent_id": "18311fce-36c5-40fe-bd45-67f5913b0b52",
    "name": "obligations"
  }
}
```

Save `datapack_id`. Every later call on this page uses it. The JSON also includes `mcp_user_name`, `mcp_server`, and `db_access`. Those values are empty. Connect agents with the [MCP server URL](/reference/mcp-server/) and the same API key.

| Status | `error.code`                   | What it means                                                                                                  |
|--------|--------------------------------|----------------------------------------------------------------------------------------------------------------|
| 400    | `VALIDATION_ERROR`             | The name is missing, shorter than 3 characters, longer than 60, or contains an HTML tag.                       |
| 409    | `CONFLICT`                     | This account already has a datapack with that name. The message is `a datapack with this name already exists`. |
| 402    | `FREE_TIER_DATAPACK_LIMIT`     | A Free account has already created 10 datapacks.                                                               |
| 402    | `STANDARD_TIER_DATAPACK_LIMIT` | A Standard account has already created 50 datapacks.                                                           |
| 503    | `LANGFUSE_UNAVAILABLE`         | Setup did not finish.                                                                                          |

Deleting a datapack does not give the creation slot back.

On `503`, read `error.message`. If it says `Could not finish setting up the datapack; nothing was created. Please retry.`, send the same request again. If the message does not say that nothing was created, [list your datapacks](#list-datapacks) before you retry. A retry against a datapack that was created returns `409`.

## List datapacks

`GET /datapacks`

Send `limit`. The value is an integer from 1 to 1000. The response is one page of datapacks the key's account can see.

```bash
curl -sS "https://api.mekodata.ai/api/v1/datapacks?limit=100" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0"
```

A `200` body looks like this:

```json
{
  "data": {
    "items": [
      {
        "datapack_id": "e02a7107-c79e-451d-b360-9cd1e13f175d",
        "datapack_name": "obligations",
        "description": "Loan obligation documents",
        "grant": "owner"
      }
    ],
    "has_more": false,
    "next_cursor": "",
    "limit": 100
  }
}
```

`grant` on each item is your role: `owner`, `maintainer`, `contributor`, or `viewer`. Each item also includes `created_at`, `updated_at`, and `user_id`.

When `has_more` is true, call again with the same `limit`, `sort`, and filters, and set `cursor` to `next_cursor`. A cursor from a different sort or filter returns `400` and `INVALID_CURSOR`. Drop `cursor` and start again.

`sort` is `created` (newest first, the default) or `name`. `q` matches a case-insensitive substring of the name, up to 200 characters. `name` matches the name exactly. Add `include_total=true` to include `total`. `fields=full` returns the datapack record instead of this summary. Counts such as `memory_count` come from [Read a datapack](#read-a-datapack). This list leaves them at zero.

Send `grant=owner` to list datapacks this account owns, or `grant=shared` to list the rest. Any other value returns `400` and `INVALID_GRANT`.

A request with no `limit` ignores those filters and returns every datapack you can see as an array in `data`. Send `limit` unless you need that full list.

## Read a datapack

`GET /datapacks/{datapack_id}`

Any member can call this.

```bash
curl -sS "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0"
```

A `200` body is the datapack. Use these fields:

| Field                                   | Meaning                                                                                          |
|-----------------------------------------|--------------------------------------------------------------------------------------------------|
| `datapack_name`, `description`, `grant` | The name, description, and your role: `owner`, `maintainer`, `contributor`, or `viewer`          |
| `memory_count`                          | Private memories in the datapack                                                                 |
| `learnings_count`                       | Memories still waiting for a promote or keep decision. This count is included in `memory_count`. |
| `knowledge_count`                       | Knowledge-base chunks, not files                                                                 |
| `collective_memory_count`               | Memories already promoted into shared knowledge                                                  |
| `conversation_search_opt_out`           | `true` when conversation search is off for this datapack                                         |

`universe_id`, `datapack_ysql_connection_string`, and `datapack_ysql_username` are empty.

## Change a datapack

`PATCH /datapacks/{datapack_id}`

The owner can change `name`, `description`, or both. Send only the fields you want to change. An empty `description` leaves the current description in place. `name` follows the same rules as create. Renaming `meko_default_datapack` returns `400` and `VALIDATION_ERROR`. A name this account already uses returns `409` and `CONFLICT`, with the message `a datapack with this name already exists`.

```bash
curl -sS -X PATCH "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d '{"description":"Loan obligation documents for 2026"}'
```

A successful call returns `200` and `data.message` is `datapack updated`.

A caller who is not the owner receives `404` and `NOT_FOUND`, with the message `datapack "<id>" not found`. That includes a maintainer who can read the same datapack. This call does not return `403`.

Conversation search is a separate call. The owner or a maintainer sends `PATCH /datapacks/{datapack_id}/conversation-search-opt-out`.

```bash
curl -sS -X PATCH \
  "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID/conversation-search-opt-out" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d '{"opted_out":true}'
```

`opted_out: true` turns conversation search off. `false` turns it back on. A `200` body is `{"data":{"conversation_search_opt_out":true}}`.

## Delete a datapack

`DELETE /datapacks/{datapack_id}`

Only the owner can delete a datapack. The delete is permanent. It does not return a creation slot. A caller who is not the owner receives the same `404` as [Change a datapack](#change-a-datapack).

```bash
curl -sS -X DELETE "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0"
```

A `200` body is `{"data":{"message":"datapack deleted"}}`.

## Upload a file

`POST /datapacks/{datapack_id}/knowledge-bases/upload`

Send one file per request, as `multipart/form-data`. The file field name is `file`. To upload many files, repeat the call. There is no request that accepts a batch.

```bash
curl -sS -D - -X POST \
  "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID/knowledge-bases/upload" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0" \
  -F "file=@obligation-1.pdf"
```

Leave out `source`, or set it to `local_filesystem`. Any other value returns `400`.

Accepted extensions are `.pdf`, `.txt`, `.text`, `.md`, `.json`, `.csv`, `.xml`, `.html`, and `.htm`. The file contents must match the extension. An empty file is rejected.

| Plan            | Largest file on this call | Knowledge storage for the account |
|-----------------|---------------------------|-----------------------------------|
| Free            | 5 MB                      | 100 MB                            |
| Standard        | 50 MB                     | 5 GB                              |
| Pro, Enterprise | 50 MB                     | No storage cap                    |

This call holds the file in memory, so 50 MB is the ceiling even when the plan allows a larger file in the portal. A file over that ceiling returns `413` with `VALIDATION_ERROR`, and the message names the limit. A full storage quota returns `402` with `FREE_TIER_KB_STORAGE_LIMIT` or `STANDARD_TIER_KB_STORAGE_LIMIT`.

A new file returns `202`:

```json
{
  "data": {
    "datapack_id": "e02a7107-c79e-451d-b360-9cd1e13f175d",
    "document_key": "USER_ID/DATAPACK_ID/CONTENT_HASH/obligation-1.pdf",
    "filename": "obligation-1.pdf",
    "size": 48211,
    "status": "processing",
    "status_url": "https://api.mekodata.ai/api/v1/datapacks/e02a7107-c79e-451d-b360-9cd1e13f175d/knowledge-bases/document-status?document_key=USER_ID%2FDATAPACK_ID%2FCONTENT_HASH%2Fobligation-1.pdf"
  }
}
```

The `Location` header is the same URL as `status_url`. Save `document_key`. Indexing continues after this response.

A repeat of the same bytes returns `200` when that file is already registered and its status is neither `FAILED` nor `NOT_STARTED`. `data.status` is `already_processed` when the status is `COMPLETED`, and `already_uploaded` otherwise. A repeat of a file that failed, never started, or was never registered returns `202` and `processing`, and indexing runs again. Use the `status_url` in either body.

Wait for each upload call to return before you send the next file for the same datapack owner. One API server holds that owner's uploads one at a time. Another server can accept an upload at the same time. You can poll status for files that are already accepted while the next upload runs.

### Check that a file finished indexing

Poll `data.status_url` from the upload response. That URL is already encoded.

```bash
curl -sS "$STATUS_URL" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0"
```

The same call, built by hand, is `GET /datapacks/{datapack_id}/knowledge-bases/document-status?document_key=DOCUMENT_KEY`. Encode `document_key`. Each `/` in the key has to be `%2F`.

A `200` body looks like this:

```json
{
  "data": {
    "index_name": "datapacks.knowledge_bases",
    "source_uri": "s3://BUCKET/USER_ID/DATAPACK_ID/",
    "document_name": "obligation-1.pdf",
    "document_status": "PROCESSING",
    "chunks_processed": null
  }
}
```

`document_status` is one of `NOT_STARTED`, `QUEUED`, `PROCESSING`, `COMPLETED`, `FAILED`, or `RETRY`. Stop when it is `COMPLETED` or `FAILED`.

`404` with `NOT_FOUND` means this key is not registered yet, or it does not belong to the datapack. Retry a new upload's first `404` a few times. Keep treating `404` as missing after that.

Any datapack member can read this status. The upload call itself still requires an owner or a maintainer.

## List uploaded files

`GET /datapacks/{datapack_id}/knowledge-bases/documents`

Any member can list the files in the knowledge base. `page` starts at 1. `page_size` defaults to 50 and cannot be higher than 200.

```bash
curl -sS \
  "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID/knowledge-bases/documents?page=1&page_size=50" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0"
```

A `200` body looks like this:

```json
{
  "data": {
    "items": [
      {
        "id": "8c1d0e2a-4b55-4f0a-9d11-2a6e7c8b9f10",
        "document_name": "obligation-1.pdf",
        "status": "COMPLETED",
        "size_bytes": 48211,
        "chunks_processed": 12
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 50,
    "has_more": false
  }
}
```

`id` is the document id for [Delete an uploaded file](#delete-an-uploaded-file). Each item can also include `document_type`, `uploaded_at`, and `uploaded_by_email`. A viewer sees `uploaded_by_email` only on files they uploaded.

`q` matches part of `document_name`. `type_filter` matches the end of the name, for example `.pdf`. `status_filter` is one of `NOT_STARTED`, `QUEUED`, `PROCESSING`, `COMPLETED`, `FAILED`, or `RETRY`. Any other `status_filter` value is ignored, and the list is unfiltered.

## Delete an uploaded file

`DELETE /datapacks/{datapack_id}/knowledge-bases/documents/{document_id}`

The owner, a maintainer, or the account that uploaded the file can delete it. The delete removes the file and its chunks.

Wait until `status` is `COMPLETED` or `FAILED`. A file in `NOT_STARTED`, `QUEUED`, `PROCESSING`, or `RETRY` returns `409` and `DOCUMENT_PROCESSING`. The message is `this file is still being processed and cannot be removed yet; try again once it finishes`.

```bash
curl -sS -X DELETE \
  "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID/knowledge-bases/documents/$DOCUMENT_ID" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0"
```

A `200` body looks like this:

```json
{
  "data": {
    "document_id": "8c1d0e2a-4b55-4f0a-9d11-2a6e7c8b9f10",
    "chunks_deleted": 12,
    "s3_deleted": true
  }
}
```

`s3_deleted` is `false` when the stored object was not removed. The database rows are already gone in that case. A missing id returns `404` and `NOT_FOUND`.

## Decide which memories to share

Promoting a memory moves it into the datapack's shared knowledge and removes it from the private store. Until that call, the memory stays private. After it, the memory leaves `GET /memories` and no longer counts in `memory_count`. The move cannot be undone. This API has no switch that promotes memories on its own.

### List memories that are waiting

`GET /memories?datapack_id=DATAPACK_ID&scope=learnings`

`scope=learnings` returns memories that nobody has promoted or marked to stay private. Omit `scope` to list private memories, including ones already marked to stay private.

```bash
curl -sS \
  "https://api.mekodata.ai/api/v1/memories?datapack_id=$DATAPACK_ID&scope=learnings" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0"
```

Read each id from `data.memories[].id`. `page` starts at 1. `page_size` defaults to 20 and cannot be higher than 100.

### Promote memories

`POST /promote-memories`

The caller must be the datapack owner or a maintainer. A viewer or contributor receives `403` and `FORBIDDEN`.

```bash
curl -sS -X POST "https://api.mekodata.ai/api/v1/promote-memories" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d "{\"datapack_id\":\"$DATAPACK_ID\",\"memory_ids\":[\"$MEMORY_ID\"]}"
```

`datapack_id` and `memory_ids` are required. Each memory id is a UUID with hyphens, 36 characters. An id in any other form returns `400`. Omit `agent_id`. Send it only when you want the "already shared" check limited to that agent.

A successful call returns `200` even when some ids were skipped:

```json
{
  "data": {
    "inserted_ids": ["3f1c2a40-7b6e-4d1a-9c20-6e5b8a1d0f77"],
    "updated_ids": [],
    "not_found_ids": []
  }
}
```

`inserted_ids` were moved into shared knowledge for the first time. `updated_ids` still had a private row, and a shared row for that id already existed, so the shared row was written again. Promoting an id that already moved returns that id in `not_found_ids`, because the private row is gone. An id also lands in `not_found_ids` when it is not in the datapack, has no embedding, has no conversation, or was marked to stay private.

Every member of this datapack can read the shared memory. It is not written into another datapack.

### Keep memories private

`POST /keep-as-memory`

Use this when a memory should stay private and should leave the learnings list. A viewer receives `403`. Other members can call it.

```bash
curl -sS -X POST "https://api.mekodata.ai/api/v1/keep-as-memory" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d "{\"datapack_id\":\"$DATAPACK_ID\",\"memory_ids\":[\"$MEMORY_ID\"]}"
```

The body uses the same two required fields as promote. A successful call returns `200`:

```json
{
  "data": {
    "updated": 1
  }
}
```

`updated` is the number of rows that changed. Ids that were already marked, or that are not in the datapack, are not counted. The mark stays. A later promote of the same id returns that id in `not_found_ids`.

## Store an artifact

Use the [knowledge-base upload](#upload-a-file) when the file should be indexed. Use an artifact when you need to store a file and read those same bytes back later. Plan limits and the Free-plan expiry window are in [Work with artifacts](/guides/working-with-artifacts/).

`POST /datapacks/{datapack_id}/artifacts`

Any member except a viewer can store one file. The body is JSON. `content_base64` is standard base64 of the file bytes. `filename` and `content_type` are required.

```bash
curl -sS -X POST \
  "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID/artifacts" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d '{"filename":"summary.txt","content_type":"text/plain","content_base64":"aGVsbG8="}'
```

The example bytes are the text `hello`. The same bytes always return the same `content_hash`. The default limit is 5,242,880 decoded bytes. A decoded file over that limit returns `413` and `ARTIFACT_TOO_LARGE` when the request body is still at most 10,485,760 bytes. The message names the limit. A larger body is rejected while it is read and returns `400` and `INVALID_JSON`, with the message `malformed request body`. Invalid base64 returns `400` and `INVALID_BASE64`.

The call returns `201`, including when those bytes were already stored:

```json
{
  "data": {
    "artifact_id": "ARTIFACT_ID",
    "content_hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
    "filename": "summary.txt",
    "content_type": "text/plain",
    "size_bytes": 5,
    "stored_in": "db"
  }
}
```

`stored_in` is `db` for a file under 1 MB and `s3` for a file of 1 MB or more. Save `content_hash`.

### Read an artifact

`GET /datapacks/{datapack_id}/artifacts/{content_hash}`

Any member can read it. `content_hash` is 64 hexadecimal characters.

```bash
curl -sS \
  "https://api.mekodata.ai/api/v1/datapacks/$DATAPACK_ID/artifacts/$CONTENT_HASH" \
  -H "Authorization: Bearer $MEKO_API_KEY" \
  -H "User-Agent: my-app/1.0"
```

The `200` body repeats the fields from the store call. The file bytes are base64 in `content_base64` when `stored_in` is `db`, and in `content_base64_large` when `stored_in` is `s3`.

A missing hash returns `404` and `ARTIFACT_NOT_FOUND`. An expired file returns `410` and `ARTIFACT_EXPIRED`. Store the same bytes again to make that file readable.

## Read errors

A success body has `data` and no `error` field. An error body has `data` set to `null`:

```json
{
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "name is required"
  }
}
```

`401` means the key is missing, revoked, or not sent as `Bearer`. `403` with `FORBIDDEN` means the account can sign in but cannot make that change. Rename and delete return `404` and `NOT_FOUND` when the caller is not the owner.

Two `429` responses use different bodies:

- Per key: `error.code` is `PAT_RATE_LIMITED`. The default limit is 60 requests per minute, with a burst of 10. `Retry-After` is `60`.
- Per client IP: the body is `{ "status": "error", "code": "RATE_LIMITED", "error": "..." }`. The limit is 10 requests per second, with a burst of 20.

Wait for the retry time, then send the request again.
