REST API Quick Start
This guide gets you from zero to semantic search in five minutes. Every example uses curl — no SDK required.
Setup
Set your API key and base URL:
export KEEPNOTES_API_KEY="knk_your_api_key_here"
export KEEPNOTES_URL="https://api.keepnotes.ai"Every request needs an Authorization header:
Authorization: Bearer knk_your_api_key_hereStore a note
Let's remember something about Kate.
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Kate prefers aisle seats"}' | jq{
"id": "%2a792222e4b9",
"summary": "Kate prefers aisle seats",
"tags": {},
"score": null,
"created_at": "2026-02-15T16:00:00Z",
"updated_at": "2026-02-15T16:00:00Z"
}That's it. The note is stored, embedded, and searchable. No schema, no configuration. The id is content-addressed — same text always gets the same ID.
Now add a few more things you know about Kate:
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Kate is allergic to shellfish — carries an EpiPen"}'
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Kate mentioned she is training for the Boston Marathon in April"}'
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Q3 budget review moved to Thursday 2pm"}'
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Kate loves the window table at Osteria Francescana but hates waiting for reservations"}'Five notes. Five facts. No relationships defined, no graph edges, no manual categorization. Just things you know.
Search by meaning
Here's where it gets interesting. You stored "Kate prefers aisle seats." Now ask:
curl -s "$KEEPNOTES_URL/v1/search" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "booking flights for Kate"}' | jq{
"notes": [
{
"id": "%2a792222e4b9",
"summary": "Kate prefers aisle seats",
"tags": {},
"score": 0.52
},
{
"id": "%5b7756afdf72",
"summary": "Kate mentioned she is training for the Boston Marathon in April",
"tags": {},
"score": 0.51
}
],
"count": 2
}You never said "aisle seats" in your query. You said "booking flights." The system understood that seat preferences are relevant to flight bookings.
And it pulled in the marathon too — because if Kate's flying somewhere in April, you might want to know about Boston.
Try another:
curl -s "$KEEPNOTES_URL/v1/search" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "planning a dinner for the team, Kate will be there"}' | jq{
"notes": [
{
"id": "%da0362b8bc19",
"summary": "Kate loves the window table at Osteria Francescana but hates waiting for reservations",
"tags": {},
"score": 0.53
},
{
"id": "%118ffd9a16c9",
"summary": "Kate is allergic to shellfish -- carries an EpiPen",
"tags": {},
"score": 0.47
}
],
"count": 2
}The restaurant preference. The shellfish allergy. Neither contains the word "dinner" or "team" — but both are exactly what you need to not bore Kate or kill her.
The budget meeting? Not returned. Because it's not relevant. Semantic search doesn't match keywords — it matches meaning.
Index a document
Notes are great for quick facts. But your organization has documents — policies, handbooks, specs. There are two ways to get them in.
By URL
If the document is hosted somewhere, pass the URL directly:
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"uri": "https://docs.keepnotes.ai/samples/travel-policy.pdf"}' | jqWe've included a sample travel policy PDF you can use to follow along.
{
"id": "https://docs.keepnotes.ai/samples/travel-policy.pdf",
"summary": "Corporate travel policy covering flight booking procedures, hotel guidelines, meal per-diems, and expense reporting requirements. Economy class for domestic flights under 4 hours, business class for international. Hotel cap $250/night ($350 in NYC, SF, London). Meal per-diem $75 domestic, $100 international.",
"tags": {},
"created_at": "2026-02-15T16:02:00Z",
"updated_at": "2026-02-15T16:02:00Z"
}The URL becomes the note ID — natural and stable. Re-indexing the same URL creates a new version automatically.
By file upload
For documents that aren't publicly hosted — local files, email attachments, user uploads — send the bytes as base64:
BASE64=$(base64 < quarterly-report.pdf)
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"content_base64\": \"$BASE64\",
\"content_type\": \"application/pdf\",
\"id\": \"q3-2026-report\",
\"tags\": {\"type\": \"report\", \"quarter\": \"Q3\"}
}" | jq{
"id": "q3-2026-report",
"summary": "Q3 2026 quarterly financial report covering revenue growth, operating expenses, and forward guidance.",
"tags": {"type": "report", "quarter": "Q3"},
"created_at": "2026-02-15T16:03:00Z",
"updated_at": "2026-02-15T16:03:00Z"
}The file is decoded, text-extracted, summarized, and embedded — same as a URL. The binary isn't stored; the extracted content is.
Supported file types: PDF, DOCX, PPTX, images (JPEG, PNG, TIFF, WebP), audio (MP3, FLAC, WAV, OGG, M4A), HTML, plain text, Markdown.
Size limit: 50 MB decoded (~67 MB as base64 in the JSON body).
Content source rules: Every request must include exactly one of content (inline text), content_base64 (file upload), or uri (URL). content_type is required with content_base64.
Both methods produce the same result: a searchable, summarizable note. Use URLs for documents you can point to; use upload for everything else.
That summary is useful, but a 30-page travel policy covers a lot of ground. What if you could search inside it?
Find the themes within
The analyze endpoint discovers the natural structure of a document — each theme, provision, or episode becomes independently searchable:
curl -s -X POST "$KEEPNOTES_URL/v1/notes/https%3A%2F%2Fdocs.keepnotes.ai%2Fsamples%2Ftravel-policy.pdf/analyze" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" | jq{
"id": "https://docs.keepnotes.ai/samples/travel-policy.pdf",
"parts": [
{"part": 1, "summary": "Flight booking: economy for domestic under 4hrs, business for international. Must book 14+ days in advance through approved portal. Seat upgrades at employee expense."},
{"part": 2, "summary": "Hotel policy: $250/night cap ($350 in NYC, SF, London). Loyalty program points belong to employee. Extended stays over 5 nights require VP approval."},
{"part": 3, "summary": "Meals and per-diems: $75/day domestic, $100/day international. Alcohol reimbursed up to $25/day with client entertainment only. Receipts required over $25."},
{"part": 4, "summary": "Expense reporting: submit within 30 days of travel. Corporate card required for all charges over $50. Late submissions may delay reimbursement."}
]
}Four sections, each with its own embedding. Now watch what happens:
curl -s "$KEEPNOTES_URL/v1/search" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "booking flights for Kate"}' | jq{
"notes": [
{
"id": "%2a792222e4b9",
"summary": "Kate prefers aisle seats",
"score": 0.52
},
{
"id": "%5b7756afdf72",
"summary": "Kate mentioned she is training for the Boston Marathon in April",
"score": 0.51
},
{
"id": "https://docs.keepnotes.ai/samples/travel-policy.pdf@P{1}",
"summary": "Flight booking procedures for corporate travel, including advance booking requirements, class of service rules, and seat selection policies.",
"score": 0.47
}
],
"count": 3
}One search. Three results from completely different sources:
- Kate's personal preference (a note you typed)
- The relevant section of a 30-page PDF (not the whole document — just the flight booking part)
- Context about Kate's April plans
Your agent now has everything it needs to book a flight: the policy constraints, the personal preference, and a heads-up about Boston. From one query.
Versions on documents
The travel policy gets updated — new per-diem rates. Just re-index the same URL:
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"uri": "https://docs.keepnotes.ai/samples/travel-policy.pdf"}' | jqSame URL, new content. KeepNotes detects the change, creates a new version, and re-summarizes. The previous version is kept.
View version history:
curl -s "$KEEPNOTES_URL/v1/notes/https%3A%2F%2Fdocs.keepnotes.ai%2Fsamples%2Ftravel-policy.pdf/versions" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" | jq{
"versions": [
{
"version": 0,
"summary": "Corporate travel policy... Meal per-diem $85 domestic, $110 international.",
"created_at": "2026-03-01T09:00:00Z"
},
{
"version": 1,
"summary": "Corporate travel policy... Meal per-diem $75 domestic, $100 international.",
"created_at": "2026-02-15T16:02:00Z"
}
]
}Get the previous version:
curl -s "$KEEPNOTES_URL/v1/notes/https%3A%2F%2Fdocs.keepnotes.ai%2Fsamples%2Ftravel-policy.pdf/versions/1" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" | jqVersion 0 is current, 1 is previous, 2 is two ago. History is automatic — you don't opt in. Your agent can answer "what were the old per-diem rates?" without you ever planning for that question.
Tags
Tags add structure without breaking search. Add them at creation time or later.
At creation:
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Kate approved the vendor contract — signed copy in DocuSign",
"tags": {"project": "acme-deal", "person": "kate"}
}' | jqUpdate tags on an existing note:
curl -s -X PATCH "$KEEPNOTES_URL/v1/notes/%a7c3e1f4b902/tags" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"set": {"person": "kate", "topic": "travel"}}' | jqQuery by tag:
curl -s "$KEEPNOTES_URL/v1/tags/person/kate" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" | jqReturns every note tagged person: kate. Tags are exact-match filters. Search is fuzzy. Use both: tag for structure, search for discovery.
Remove a tag:
curl -s -X PATCH "$KEEPNOTES_URL/v1/notes/%a7c3e1f4b902/tags" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"remove": ["topic"]}' | jqList all tag keys and values:
curl -s "$KEEPNOTES_URL/v1/tags" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" | jq
curl -s "$KEEPNOTES_URL/v1/tags/person" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" | jqMeta-docs: contextual surfacing
Tags become powerful when combined with meta-docs — rules that automatically surface related items based on tag context. Think of them as standing queries that activate when relevant.
The system ships with a meta/todo rule: surface open commitments and requests scoped to the current context. Let's see it in action.
First, store some open tasks tagged with the same project:
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Send Kate the revised SOW by Wednesday",
"tags": {"act": "commitment", "status": "open", "project": "acme-deal", "person": "kate"}
}'
curl -s "$KEEPNOTES_URL/v1/notes" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Kate asked for the pricing breakdown with volume discounts",
"tags": {"act": "request", "status": "open", "project": "acme-deal", "person": "kate"}
}'Now retrieve the contract note we tagged earlier:
curl -s "$KEEPNOTES_URL/v1/notes/%a7c3e1f4b902" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" | jq{
"id": "%a7c3e1f4b902",
"summary": "Kate approved the vendor contract — signed copy in DocuSign",
"tags": {"project": "acme-deal", "person": "kate"},
"meta": {
"todo": [
{
"id": "%f819a3c0e472",
"summary": "Send Kate the revised SOW by Wednesday"
},
{
"id": "%4d22b8ae9f01",
"summary": "Kate asked for the pricing breakdown with volume discounts"
}
]
},
"created_at": "2026-02-15T16:05:00Z",
"updated_at": "2026-02-15T16:05:00Z"
}You didn't ask for those tasks. The meta/todo rule saw that the contract note has project: acme-deal, found open commitments and requests tagged with the same project, and surfaced them. Your agent now knows: the contract is signed, and there are two things still owed to Kate.
This is what makes keep more than a store-and-retrieve system. Meta-docs create a layer of proactive context — the system reminds you of things you should know, scoped to what you're looking at right now.
The built-in meta-docs cover todo (open commitments and requests) and learnings (past mistakes and insights). You can define your own with custom tag queries. See Meta-Docs for the full guide.
Full-text search
When you need exact words, not meaning:
curl -s "$KEEPNOTES_URL/v1/search" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "EpiPen", "fulltext": true}' | jqFull-text search uses FTS5 for literal matching. Use it when you know exactly what you're looking for.
Find similar notes
Given a note, find what's related:
curl -s "$KEEPNOTES_URL/v1/search" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"similar_to": "%a7c3e1f4b902", "limit": 5}' | jqReturns notes most semantically similar to "Kate prefers aisle seats" — travel facts, preferences, anything in that neighborhood of meaning. Parts from analyzed documents appear here too.
Time filtering
Scope any search to a time window:
# Last 7 days
curl -s "$KEEPNOTES_URL/v1/search" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "Kate", "since": "P7D"}'
# Last hour
curl -s "$KEEPNOTES_URL/v1/search" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "Kate", "since": "PT1H"}'
# Since a specific date
curl -s "$KEEPNOTES_URL/v1/search" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "Kate", "since": "2026-02-01"}'Time values use ISO 8601 durations: P7D = 7 days, PT1H = 1 hour, P1M = 1 month.
Delete and revert
curl -s -X DELETE "$KEEPNOTES_URL/v1/notes/%a7c3e1f4b902" \
-H "Authorization: Bearer $KEEPNOTES_API_KEY" | jqIf the note has versions, this reverts to the previous version. If no history exists, the note is removed. Deletion is gentle by default.
Endpoint reference
All endpoints are under /v1/. Authentication is via Bearer token in the Authorization header. Download the OpenAPI spec for client codegen or import into your toolchain.
Notes
| Method | Path | Description |
|---|---|---|
POST | /v1/notes | Create or update a note (content, content_base64, or uri) |
GET | /v1/notes/{note_id} | Get a note by ID |
DELETE | /v1/notes/{note_id} | Delete or revert to previous version |
GET | /v1/notes | List recent notes |
PATCH | /v1/notes/{note_id}/tags | Add, update, or remove tags |
POST | /v1/notes/{note_id}/analyze | Analyze into searchable parts |
Search
| Method | Path | Description |
|---|---|---|
POST | /v1/search | Semantic, full-text, or similarity search |
POST /v1/search parameters:
| Parameter | Type | Description |
|---|---|---|
query | string | Search by meaning (semantic) or text (with fulltext: true) |
tags | object | Tag key-value filter (pre-filters results before ranking) |
similar_to | string | Find notes similar to this note ID |
fulltext | boolean | Use exact text matching instead of semantic search |
limit | integer | Max results (default 10) |
since | string | Time filter (ISO duration or date) |
Use exactly one of query or similar_to.
Tags
| Method | Path | Description |
|---|---|---|
GET | /v1/tags | List all tag keys |
GET | /v1/tags/{key} | List values for a tag key |
GET | /v1/tags/{key}/{value} | List notes with a specific tag |
Versions & Parts
| Method | Path | Description |
|---|---|---|
GET | /v1/notes/{note_id}/versions | List version history |
GET | /v1/notes/{note_id}/versions/{offset} | Get a specific version |
GET | /v1/notes/{note_id}/parts | List analyzed parts |
GET | /v1/notes/{note_id}/parts/{num} | Get a specific part |
Continuations (preview)
| Method | Path | Description |
|---|---|---|
POST | /v1/continue | Run one continuation tick (query, write, or ingest) |
POST | /v1/continue/work | Execute a pending work item within a flow |
Continuations provide stateful multi-step interactions. See the Continuations guide for the full schema and examples.
Other
| Method | Path | Description |
|---|---|---|
GET | /v1/notes/{note_id}/similar | Similar notes (display-optimized) |
GET | /v1/count | Total note count |
GET | /v1/health | Health check (no auth required) |
Response format
Every note response has the same shape:
{
"id": "%2a792222e4b9",
"summary": "Kate prefers aisle seats",
"tags": {"person": "kate", "topic": "travel"},
"score": 0.52,
"meta": {
"todo": [
{"id": "%f819a3c0e472", "summary": "Send Kate the revised SOW by Wednesday"}
]
},
"created_at": "2026-02-15T16:00:00Z",
"updated_at": "2026-02-15T16:00:00Z"
}id— Unique identifier. Content-addressed for inline notes (%hash), URI for indexed documents.summary— Human-readable summary. Short notes are stored verbatim; long documents are summarized automatically.tags— User-defined key-value pairs. System tags (prefixed_) are excluded from responses.score— Similarity score (0–1), present only in search results. Higher is more relevant.meta— Contextually surfaced items from meta-doc rules. Each key (e.g.todo,learnings) maps to a list of related notes. Only present when meta-docs match.created_at/updated_at— ISO 8601 timestamps.
Part IDs use the @P{N} suffix: https://example.com/doc.pdf@P{1} is part 1 of that document.
List endpoints return {"notes": [...], "count": N}.
For the full breakdown of all response fields including similar, parts, and prev/next version navigation, see Reading the Output.
Errors
| Status | Meaning |
|---|---|
400 | Bad request — missing required fields, invalid parameters |
401 | Invalid or missing API key |
404 | Note not found |
429 | Rate limit exceeded |
What just happened?
You stored five sentences about Kate and indexed a 30-page PDF. No schema, no relationships, no entity extraction.
Then you searched with a question you'd never planned for — "booking flights for Kate" — and got back Kate's seat preference, the flight booking section of the travel policy, and a heads-up about the Boston Marathon. Three different sources, one query, zero configuration.
That's semantic memory. Your agent accumulates knowledge — notes, documents, conversations — and retrieves it by intent rather than by lookup key. The more you store, the more connections it finds. Memory that compounds.
Everything here also works locally with the keep CLI — same engine, same semantic search, no network required. The REST API puts it all behind a single endpoint so your agents, apps, and workflows can use it at scale without managing embeddings, vector stores, or infrastructure.
Next steps
- Python SDK — Client library for Python applications
- Tagging — Structured tags, speech acts, and constrained vocabularies
- Versioning — How version history and parts work together
- Document Analysis — Deep dive on analyze and parts
- API Reference — Interactive OpenAPI documentation (download spec)
See Also
- Quick Start (CLI) — Using keep as a local CLI tool
- Agent Guide — Patterns for AI agent integration