Mikipage API

The Mikipage REST API is the complete feature set of the product. Everything you can do on Mikipage — create and search notes, build AI pages, manage groups, publish to the web, run AI — is available here.

The other ways to use Mikipage are deliberate subsets of this API:

  • Web is an opinionated, minimalistic surface for everyday use. It makes choices for you and hides advanced options.
  • MCP is a curated subset aimed at AI power users. Some operations are intentionally withheld (deleting notes and pages, account settings, group and member management, file bytes, comments) to keep AI usage safe and cheap. Deletion in particular stays off MCP because it is irreversible and easy for an AI agent to apply in bulk — delete from the Web or this API instead. See [1].

Anything that exists on Web or MCP is expressible through the API, with three deliberate exceptions: anonymous public content (the landing page, /docs, published pages), the chat product itself, and credentials, plan changes and purchases. Those are Web-only by decision, and the list is closed. Everything else lives here, unopinionated and with the full set of options.

The API and MCP never serve a public group's content to someone who isn't a member of it — public content is read on the website. The API answers them exactly as it would for a private group — commenting included: to comment on a public page you are not a member of, use the page on the website.

Python SDK

We provide an official Python SDK in the open-source miki_client library. It handles authentication (including the OAuth browser flow, token caching, and automatic refresh), pagination, file uploads, and the presigned-download flow for you.

miki_client currently lives in a private GitHub repository and will be published publicly (PyPI + open source) at a later date. The examples below show both the SDK and the raw HTTP calls, so you can integrate in any language today.

from miki_client import MikiClient

# OAuth (default): opens a browser to log in, then caches + auto-refreshes tokens.
client = MikiClient()

note = client.create_note("# Shopping list\n- milk\n- eggs")
print(note["id"])

Base URL and conventions

  • Base URL: https://api.mikipage.com — there is no version prefix and no path prefix. GET https://api.mikipage.com/notes lists your notes.

  • Two hostnames. The API is api.mikipage.com. Sign-in is not: the OAuth endpoints, the /.well-known/ discovery documents and the OpenAPI document all live on https://mikipage.com. You need both, and neither is derivable from the other — do not assume a shared domain.

  • JSON everywhere: request and response bodies are JSON with snake_case field names.

  • Partial updates use PATCH — send only the fields you want to change.

  • List responses are enveloped:

    {
      "items": [ /* ... */ ],
      "next_cursor": "opaque-string-or-null",
      "has_more": false
    }
    

    To page, pass next_cursor back as the cursor query parameter until has_more is false.

  • Timestamps are RFC 3339 strings (e.g. 2026-07-23T18:04:00Z).

  • Errors use RFC 9457 application/problem+json. Every error body carries a stable machine-readable code you can switch on — never string-match the human detail:

    {
      "type": "about:blank",
      "title": "Not Found",
      "status": 404,
      "code": "not_found",
      "detail": "Note not found",
      "request_id": "req_..."
    }
    

    Common codes: invalid_request (400), unauthenticated / invalid_token (401), insufficient_credits (402), permission_denied / insufficient_scope / quota_exceeded (403), not_found (404), conflict (409), content_too_large (413), rate_limited (429), internal (500), unavailable (503). A missing resource and one you're not allowed to see return an identical not_found body — there are no existence leaks.

  • OpenAPI: the machine-readable spec is served at GET https://mikipage.com/openapi.json (OpenAPI 3.1), and rendered as the interactive API reference. It is the source of truth for the wire contract: the request and response types both our TypeScript server and the Python SDK are checked against are generated from it, so a shape documented there is one the code actually produces. The SDK's methods themselves are hand-written.

Authentication

Mikipage uses OAuth 2.1 access tokens (Cognito-issued JWTs), verified at the edge. Send the token as a Bearer header on every request:

Authorization: Bearer <access-token>

How to get a token

  • OAuth authorization-code + PKCE (recommended for apps and interactive tools). Discover the endpoints at GET /.well-known/oauth-authorization-server:

    • authorization_endpoint: https://mikipage.com/oauth/authorize
    • token_endpoint: https://mikipage.com/oauth/token
    • registration_endpoint: https://mikipage.com/oauth/register (dynamic client registration)

    Refresh tokens are issued for long-lived CLI/SDK sessions. The Python SDK automates the whole loop — browser login, token cache (~/.mikipage/), and silent refresh.

    Credential lifecycle belongs to Cognito, not the API — there is no login endpoint, and no Mikipage password is ever handed to a client.

  • Headless / CI, where a browser isn't available on the machine: the same OAuth flow, with the URL printed for you to open anywhere and the redirect pasted back:

    uv run scripts/auth.py --no-browser
    

    The session that results is an ordinary renewable OAuth one, so this is a one-time step.

Scopes

Not enforced. A valid token grants full access to everything you can reach. Scopes are advisory documentation only — they describe the authorization model the API is designed around, and per-scope enforcement is not currently planned (GitHub #256). Do not build anything that depends on a token being restricted.

Each operation carries an x-required-scopes annotation, published in openapi.json and shown against each operation in the API reference. It records which capability an operation belongs to — roughly one family per resource: notes:*, pages:*, groups:*, runs:*, account:*, usage:read and publishing.

Comments have no scope of their own — they ride on the parent note's or page's :read / :write. Files ride on notes:read / notes:write. Every endpoint requires a token; there is no anonymous namespace.

Quickstart

Create a note

A note is just Markdown content. Mikipage indexes it for AI search and page-matching automatically.

SDK

note = client.create_note(
    "# Meeting notes\n\nDiscussed Q3 roadmap. #planning",
    group_ids=["<group-id>"],   # optional: share to groups on creation
)
note_id = note["id"]

HTTP

curl -X POST https://api.mikipage.com/notes \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content": "# Meeting notes\n\nDiscussed Q3 roadmap. #planning"}'

Response (Note):

{
  "id": "note_abc123",
  "content": "# Meeting notes\n\nDiscussed Q3 roadmap. #planning",
  "starred": false,
  "archived": false,
  "stale_match": false,
  "owner_id": "…",
  "state": "active",
  "created_at": "2026-07-23T18:04:00Z",
  "updated_at": "2026-07-23T18:04:00Z"
}

Retrieve a note

SDK

note = client.get_note("note_abc123")

# Pull in related data in one call:
note = client.get_note("note_abc123", expand=["groups", "pages", "files"])

HTTP

curl https://api.mikipage.com/notes/note_abc123?expand=groups,pages \
  -H "Authorization: Bearer $TOKEN"

You can read a note if you own it or if it's shared to a group you belong to.

A protected note ("is_protected": true) carries its secret text in protected as an encrypted Base64 envelope; content is its plaintext description. The server cannot decrypt it — a client holding the password can. [2] describes the format, and the OpenAPI reference documents how PATCH /notes/{id} protects a note.

Search notes

POST /notes/search ranks notes by cross-lingual semantic similarity. Any "quoted" substring in q becomes a strict phrase filter. It is a read — it needs only the notes:read scope — and takes a body so your query text stays out of URLs and server logs.

results = client.search_notes(q='roadmap "Q3"', tags=["planning"], limit=20)
for item in results["items"]:
    print(item["id"], item["content"][:60])

GET /notes is the plain listing, ordered by recency. It is a separate endpoint, and the search parameters are not accepted there.

Upload a file

A file belongs to you, not to a note. You upload it once, then reference it from any number of your notes by writing [[file:ID]] in their content — images render inline on the website, anything else as a download link. The same bytes are stored once per account: upload a file you already have and you get the existing one back, and your storage is not charged twice.

SDK

f = client.upload_file("scan.pdf")  # hashes, uploads, confirms — or reuses an identical file
client.create_note(f"Insurance policy scan [[file:{f['id']}|scan.pdf]]")

HTTP — declare the file with its SHA-256 first:

curl -X POST https://api.mikipage.com/files \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "scan.pdf", "content_type": "application/pdf",
       "file_size": 482113, "sha256": "<64-character hex SHA-256>"}'
  • 200, "state": "ready" — you already hold a file with those exact bytes. file describes it; there is nothing to upload.
  • 201, "state": "pending" — PUT the bytes to upload.url, sending upload.headers exactly as given (the URL is bound to the SHA-256, and storage rejects bytes that don't match it), then call POST /files/{id}/confirm with {"file_name": "scan.pdf"}. Confirm is idempotent: the first call answers 201, a repeat 200 with the same file.

Files up to 700KB can skip the dance: send multipart/form-data with a file part to POST /files and it uploads in one call.

A file id always means the same bytes — files are never edited in place. To change one, upload the new version and swap the reference.

Size and storage. Each file must fit your plan's per-file size, and all your files together its storage (see [3]). There is no limit on how many files a note references.

Manage files

GET /filesYour files, newest first (cursor-paginated)
GET /files/{id}Metadata: name, size, content type, sha256, standalone
GET /files/{id}/downloadA short-lived download URL — request a fresh one each time
GET /notes/{id}?expand=filesThe note, with the files it references
PATCH /files/{id}{"standalone": true} keeps a file no note references
DELETE /files/{id}Deletes the file now; [[file:ID]] refs to it stop resolving

A note's file_count is how many files its content references.

Unused files are removed automatically. When none of your notes references a file any more — you removed its last [[file:ID]], or deleted the last note that had one — it is deleted about 15 minutes later. The delay leaves room to move a reference from one note to another. To keep a file with no note, upload it with "standalone": true or set that later with PATCH. The website never does this, so it is an API-only choice. An upload that no note ever references, and that isn't standalone, is cleaned up within a day or so.

Who can read a file. You, and anyone who can read one of your notes that references it. A reference in someone else's note neither grants their readers access nor keeps your file alive. A file you can't read answers 404.

API reference

Every endpoint, with its parameters, request body and response shape, is in the interactive API reference — rendered directly from the OpenAPI document, so it is never out of date with what the API actually serves.

The raw document is at GET https://mikipage.com/openapi.json if you would rather generate a client or load it into your own tooling.

Operations are grouped there under Notes, Files, Pages, Comments, Groups, Publishing, AI runs and Account — the same families described above.

To be implemented

The following are designed but not yet available. They will require additional backend work and are tracked for a follow-up iteration — build against the current surface for now.

Chat is not on this list and is not coming: it is a web-app feature with no published API, deliberately. Its shape is tied to the UI it was built for, and freezing that into a public contract would buy nobody anything.

  • Webhooks & events: the entire webhooks family and a server-sent-events stream (GET /events).
  • Runs: run-id-addressable reads (GET /runs/{id}), a global cross-entity runs list with cursors, POST /runs/{id}/cancel, and server runs returning a run id on creation.
  • Restore: POST /notes/{id}/restore and POST /pages/{id}/restore for soft-deleted items.
  • Pictures: PUT /me/picture and PUT /groups/{id}/picture (today, clients upload directly to storage).
  • Reliability: Idempotency-Key support and rate limiting.
  • Personal access tokens (PATs): the design is PAT-ready (reserved mikip_ prefix); OAuth + refresh tokens cover current flows, including headless ones via --no-browser. A device-authorization grant is under evaluation as a smoother bootstrap for that case.
  • List refinements: a stale= filter and expand= on list endpoints.
Sep 25, 2026
Comments
Log in to comment.

No comments yet.