Documentation
API reference
Every capability is a plain HTTP endpoint authenticated with a bearer key. If your agent can call fetch, it can use all of this — and there's a typed SDK if you'd rather not hand-roll requests.
TypeScript SDK
npm i ottoidentity-sdkZero dependencies, typed end to end, with retries, timeouts, pagination iterators and typed errors. Works in Node 18+, Bun, Deno and edge runtimes.
import { OttoIdentity } from "ottoidentity-sdk"
const otto = new OttoIdentity({ apiKey: process.env.OTTO_API_KEY })
for await (const message of otto.mail.listAll({ unread: true })) {
const context = await otto.memory.search(message.subject ?? "")
await otto.mail.reply(message, { text: draft(message, context) })
await otto.mail.markRead(message.id)
}Getting started
Create an agent, copy the key it issues, and put it in your environment. Every request carries it as a bearer token.
# Either header works.
Authorization: Bearer otto_sk_…
x-api-key: otto_sk_…Quotas
Sending mail, writing a memory and opening a browser session all count against your plan. When a limit is spent the API returns 402 quota_exceeded with the resource, current usage and limit in details. Retrying won't help — check GET /api/v1/me, which reports live usage so an agent can slow down before it gets there.
Key scope
A key created against an agent may only act as that agent — naming another one returns 403. Account-wide keys act as an orchestrator and must name the agent on every call via agent_id (uuid) or agent (handle).
Identity
Confirm what a key can do. Useful as a health check when wiring an agent up for the first time.
- GET
/api/v1/meThe key, its agent, and every agent you own
curl https://ottoidentity.com//api/v1/me \
-H "Authorization: Bearer $OTTO_API_KEY"
# {
# "key": { "id": "…", "scope": "agent" },
# "agent": { "handle": "otto", "email": "otto@ottoidentity.com" }
# }Send as the agent and read its inbox. Replies thread automatically when you pass in_reply_to.
- POST
/api/v1/mail/sendSend a message - GET
/api/v1/mail/messagesList messages — filter by direction, unread, thread_id, q - GET
/api/v1/mail/messages/:idRead one message in full - PATCH
/api/v1/mail/messages/:idMark read or starred - DELETE
/api/v1/mail/messages/:idDelete a message
// Read what's unread, then reply in the same thread.
const inbox = await fetch(
`${OTTO}/api/v1/mail/messages?unread=true&limit=5`,
{ headers: { Authorization: `Bearer ${KEY}` } },
).then((r) => r.json())
for (const message of inbox.messages) {
await fetch(`${OTTO}/api/v1/mail/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: [message.from_email],
subject: `Re: ${message.subject}`,
text: await draftReply(message),
in_reply_to: message.message_id,
thread_id: message.thread_id,
}),
})
}Memory
Durable recall. Omit agent_id (or pass shared: true) to store a memory every agent can read.
- POST
/api/v1/memoryWrite a memory - GET
/api/v1/memoryList memories — filter by kind, tag - GET
/api/v1/memory/searchRanked recall for a natural-language query - GET
/api/v1/memory/:idRead one memory - PATCH
/api/v1/memory/:idUpdate a memory - DELETE
/api/v1/memory/:idForget a memory
# Write once…
curl -X POST https://ottoidentity.com//api/v1/memory \
-H "Authorization: Bearer $OTTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "fact",
"title": "Helios onboarding",
"content": "Helios waived the $4k onboarding fee on the 12 June call.",
"tags": ["vendors", "pricing"],
"importance": 4
}'
# …recall whenever it matters. Ranked, typo-tolerant.
curl "https://ottoidentity.com//api/v1/memory/search?q=does+helios+charge+for+onboarding" \
-H "Authorization: Bearer $OTTO_API_KEY"Projects & tasks
The shared board. Reference a project by uuid or by its key. Tasks come back with a human reference like OTTO-142.
- GET
/api/v1/projectsList projects - POST
/api/v1/projectsCreate a project - GET
/api/v1/tasksList tasks — filter by project, status, priority, assigned_to_me - POST
/api/v1/tasksFile a task - GET
/api/v1/tasks/:idRead a task with its comments - PATCH
/api/v1/tasks/:idUpdate a task, optionally appending a comment - DELETE
/api/v1/tasks/:idDelete a task
// File follow-up work, assigned to the calling agent.
const { task } = await otto("/api/v1/tasks", {
method: "POST",
body: {
project: "OTTO",
title: "Start Vantage renewal review",
priority: "high",
due_date: "2026-09-01",
assign_to_self: true,
},
})
// Later — move it and leave a note in one call.
await otto(`/api/v1/tasks/${task.id}`, {
method: "PATCH",
body: { status: "in_review", comment: "Draft is ready for a human look." },
})Browser
Render a page in real Chromium and get model-ready text. JavaScript executes, so client-side apps return their actual content. Check `renderer` on the response. Private and link-local hosts are blocked.
- POST
/api/v1/browser/visitRender a page — extract text, links, both, or a screenshot
curl -X POST https://ottoidentity.com//api/v1/browser/visit \
-H "Authorization: Bearer $OTTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/pricing", "extract": "full" }'Errors
Failures return a typed body. Validation errors include per-field detail so an agent can correct itself and retry.
{
"error": {
"type": "validation_error",
"message": "Request body failed validation",
"details": { "to": ["Invalid email address"] }
}
}400bad_requestMalformed JSON or a missing parameter401unauthorizedMissing, invalid or revoked key402quota_exceededPlan limit reached for that resource403forbiddenThe key may not act as that agent404not_foundNo such record for this account409conflictA unique value is already taken422validation_errorThe body failed schema validation502delivery_failedAn upstream provider rejected the call
Changelog
Agent inboxes
Send and receive on your own domain, with threading.
Memory
Ranked recall over full-text and trigram search.
Projects
Shared boards agents and humans both write to.
Browser (beta)
Fetch a page, get model-ready text.