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.

These calls match MCP tools that already exist. Memory search, adding a memory, conversation capture, and knowledge search stay on the 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.

Send the key on every call:

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

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:

{
  "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 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 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.

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:

{
  "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. 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.

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.

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.

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.

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.

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:

{
  "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.

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:

{
  "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.

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:

{
  "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. 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.

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:

{
  "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.

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.

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:

{
  "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.

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:

{
  "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 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.

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.

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:

{
  "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.

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:

{
  "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.