# Second API Base URL: https://second-demo-c6bfcc.westus.cloudapp.azure.com This is a public HTTPS JSON API over one person's Second: their private user model (the people, projects, organizations, tools and places in their life, and what they are doing) plus their Linear connector. This server keeps no profile of its own. Each approved read is relayed to the user's own laptop while it is online with Second running, and reads real live data. When the laptop is offline, Second reads are answered from the last offline snapshot and say so; Linear needs the live laptop. Everything here is plain HTTP JSON that curl can drive: you need no MCP connector and no OAuth client registration. No credentials or personal records are exposed by discovery. In short: connect once (the user signs in and presses Connect in their browser), then every read is one POST that waits for the user's approval on their phone. ## Discovery - GET /tools: tool names, paths and JSON input schemas. - GET /openapi.json: HTTP request schemas and authentication handoff. - GET /agent: this same guide (also served at / and /llms.txt). ## Connect an agent or terminal 1. Generate a private random PKCE code_verifier (43-128 unreserved characters). Compute code_challenge as base64url-without-padding(SHA256(code_verifier)). 2. POST /auth/agent/start with Content-Type: application/json and {"client_name":"Your terminal or agent name","code_challenge":"","code_challenge_method":"S256"}. The response contains request_id, verification_uri, expires_in and interval. Connection requests expire after five minutes; the polling interval is two seconds. 3. Ask the user to open that exact verification_uri in their browser. The user signs in through Clerk and explicitly chooses Connect. Never enter the user's credentials, act on their login, or click Connect or an approval for them. The sign-in step runs through the Second helper on the user's own laptop (a 127.0.0.1 page), so the link must be opened in a browser on the laptop where Second is running. After signing in, the browser returns to the connection page by itself and the user chooses Connect or Deny there. Show the link exactly as issued; do not fetch it, shorten it, or hand the user any other link (only that page can connect you). 4. Keep the verifier private. Poll POST /auth/agent/token with {"request_id":"","code_verifier":""} no faster than the returned interval. A 202 authorization_pending means wait; 403 means denied, expired, already redeemed or a wrong verifier: stop. Expiry requires a new user-initiated connection. 5. Successful redemption returns a one-time-issued Bearer token and expires_in (43200 seconds: 12 hours). Use Authorization: Bearer on read requests. There is no refresh: when a read answers 401, connect again. It is not a Clerk token, a provider credential, or permission for another user. Keep it private; do not put it in URLs, logs, or shared command history. ### Copy-paste: connect with curl and openssl Each block is self-contained (state is kept in private files), so it works even when your shell does not keep variables between commands. Start, and print the link for the user. Replace "My agent" with a short name the user will recognise you by (letters, digits and spaces; at most 96 bytes): it is shown on the connection page and in every approval message. BASE='https://second-demo-c6bfcc.westus.cloudapp.azure.com' umask 077; mkdir -p "$HOME/.second-agent"; cd "$HOME/.second-agent" openssl rand -base64 48 | tr -d '=\n' | tr '+/' '-_' > verifier CHALLENGE=$(printf '%s' "$(cat verifier)" | openssl dgst -sha256 -binary | openssl base64 -A | tr -d '=' | tr '+/' '-_') curl -sS -X POST "$BASE/auth/agent/start" -H 'Content-Type: application/json' \ -d "{\"client_name\":\"My agent\",\"code_challenge\":\"$CHALLENGE\",\"code_challenge_method\":\"S256\"}" > start.json sed -n 's/.*"request_id":"\([0-9a-f]*\)".*/\1/p' start.json > request_id sed -n 's/.*"verification_uri":"\([^"]*\)".*/Ask the user to open: \1/p' start.json 200 {"expires_in":300,"interval":2,"request_id":"<64 hex>","verification_uri":"https://second-demo-c6bfcc.westus.cloudapp.azure.com/connect?r=<64 hex>"} 400 {"error":"invalid_request","detail":"..."} fix the body and send it again 429 {"error":"too_many_pending_connections"} wait a few minutes before starting again Give the user that link, then poll until the answer is not 202 (at most five minutes): BASE='https://second-demo-c6bfcc.westus.cloudapp.azure.com' umask 077; cd "$HOME/.second-agent" while :; do STATUS=$(curl -sS -o token.json -w '%{http_code}' -X POST "$BASE/auth/agent/token" \ -H 'Content-Type: application/json' \ -d "{\"request_id\":\"$(cat request_id)\",\"code_verifier\":\"$(cat verifier)\"}") [ "$STATUS" = 202 ] || break sleep 2 done echo "token endpoint answered $STATUS" 202 {"error":"authorization_pending"} the user has not chosen Connect yet: keep polling 200 {"access_token":"","expires_in":43200,"token_type":"Bearer"} connected; token.json now holds it 403 {"error":"access_denied"} the user chose Deny: stop, tell the user 403 {"error":"expired_token"} unknown or expired request_id: start again only if the user asks 403 {"error":"invalid_grant"} wrong code_verifier, or the token was already issued once 400 {"error":"invalid_request","detail":"..."} malformed body: fix it; this does not use up the request Every read below starts the same way: BASE='https://second-demo-c6bfcc.westus.cloudapp.azure.com' TOKEN=$(sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p' "$HOME/.second-agent/token.json") ## Every read needs the user's decision Send Content-Type: application/json and a specific nonempty reason with the exact parameters below. Write the reason in plain words for the user to read: one line, at most 512 characters, no links. The server sends the user an iMessage that names you, the tool, the exact parameters and your reason, followed by a poll. The user answers in iMessage by tapping Approve or Deny. Your HTTP request stays open until the user decides, for up to two minutes: use a client timeout of at least 150 seconds (curl --max-time 150) and make one read at a time. The user must approve personally. Login alone releases no data. Denial or expiry releases no result. Do not retry a denial or an uncertain response automatically. There is no unattended approval. Linear discovery, Linear issue listing and Linear issue retrieval are separate approved reads. For "my recent Linear issues" call action "list_issues" directly. They need the Linear connector of the Second app on the user's laptop; its existing connector runtime fetches the issue with the user's connected account without exporting the connector credential. A display label is not an account ID. When that Second build has no live Linear connector, linear_connections answers 503 with the detail "No live Linear connector is running in this Second build." and linear_get_issue answers only with what Second itself captured about the issue, with source "second-profile" and a first line that says so. Report that honestly; it is not the live Linear issue. ## Read tools ### second_tags POST /second/tags Read one page of the user's Second tag catalog (the people, projects, organizations, tools and places Second knows). Pagination is another approved read. Input JSON schema: {"additionalProperties":false,"properties":{"limit":{"default":20,"maximum":100,"minimum":1,"type":"integer"},"offset":{"default":0,"maximum":100000,"minimum":0,"type":"integer"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason"],"type":"object"} Example: curl -sS --max-time 150 -X POST "$BASE/second/tags" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"limit":20,"offset":0,"reason":"List the tags to find the project the user asked me about."}' Answer: {"ok":true,"tool":"second_tags","source":"laptop-live","data":{"tags":[{"tag_id":"tag_0123456789abcdef","category":"projects","title":"...","aliases":[],"summary":"...","status":"active","counts":{...}}],"total":123,"limit":20,"offset":0,"stats":{...}}} Use data.tags[].tag_id with /second/wiki. More pages: raise offset by limit while offset < data.total. ### second_tag_wiki POST /second/wiki Read one exact tag's existing wiki/detail response. Use a tag_id obtained from an approved tags read; this does not fetch additional timeline pages. Input JSON schema: {"additionalProperties":false,"properties":{"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"},"tag_id":{"pattern":"^tag_[0-9a-f]{16}$","type":"string"}},"required":["reason","tag_id"],"type":"object"} Example: curl -sS --max-time 150 -X POST "$BASE/second/wiki" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"tag_id":"tag_0123456789abcdef","reason":"Read the wiki for the project the user asked me to summarize."}' Answer: {"ok":true,"tool":"second_tag_wiki","source":"laptop-live","data":{"tag":{"tag_id":"...","category":"...","title":"...","summary":"..."},"about":{"text":"..."},"entries":{...},"insights":{...},"facts":[],"links":[],"timeline":{...},"threads":[]}} An unknown tag_id answers 404 {"ok":false,"error":"not_found"}. ### linear_connections POST /connectors/linear List connected Linear account labels and immutable connection IDs after approval. Requires a live Linear connector in the Second app on the user's laptop. Input JSON schema: {"additionalProperties":false,"properties":{"action":{"const":"connections","type":"string"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason","action"],"type":"object"} Example: curl -sS --max-time 150 -X POST "$BASE/connectors/linear" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"action":"connections","reason":"Find which Linear account to read the issue from."}' Answer: {"ok":true,"tool":"linear_connections","source":"linear-connector","data":{...}} with each account's label and immutable connection ID, or 503 {"ok":false,"error":"unavailable","detail":"No live Linear connector is running in this Second build."} ### linear_get_issue POST /connectors/linear Read one Linear issue using an exact connected account. Second's existing connector runtime owns its credential; this read needs its own approval. Without a live connector the answer is what Second itself captured about the issue, labelled as such. Input JSON schema: {"additionalProperties":false,"properties":{"action":{"const":"get_issue","type":"string"},"connection_id":{"description":"Immutable ID returned by an approved linear_connections read; never substitute a display label. Omit it only when linear_connections reported no live connector.","pattern":"^[A-Za-z0-9_-]{1,128}$","type":"string"},"issue_id":{"description":"Exact Linear issue ID or identifier.","pattern":"^[A-Za-z0-9_-]{1,128}$","type":"string"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason","action","issue_id"],"type":"object"} Example: curl -sS --max-time 150 -X POST "$BASE/connectors/linear" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"action":"get_issue","connection_id":"","issue_id":"ABC-123","reason":"Read the issue the user asked me to check."}' Answer: {"ok":true,"tool":"linear_get_issue","source":"linear-connector","text":"# ABC-123 \nState: ...\n..."} With no live connector, omit connection_id; the answer then has source "second-profile" and its text starts with "Live Linear connector is not running; this is what Second captured:". ### linear_list_issues POST /connectors/linear List the user's most recently updated Linear issues (identifier, title, state) through the live Linear connector, optionally narrowed by keywords. Use this for questions like "my recent Linear issues"; each call needs its own approval. Input JSON schema: {"additionalProperties":false,"properties":{"action":{"const":"list_issues","type":"string"},"connection_id":{"description":"Optional immutable ID returned by an approved linear_connections read; omitted, the user's default connected account is used.","pattern":"^[A-Za-z0-9_-]{1,128}$","type":"string"},"limit":{"default":10,"maximum":25,"minimum":1,"type":"integer"},"query":{"description":"Optional keywords; omitted, the most recently updated issues are listed.","maxLength":200,"type":"string"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason","action"],"type":"object"} Example: curl -sS --max-time 150 -X POST "$BASE/connectors/linear" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"action":"list_issues","limit":10,"reason":"List the recent Linear issues the user asked me to summarize."}' Answer: {"ok":true,"source":"linear-connector","text":"Most recently updated Linear issues (10):\n..."} No connections call is needed first. Add "query":"<keywords>" to narrow the list. ## More read tools from the user's laptop The laptop also offers the Second tools below. Each is POST /second/tools/<name> with that tool's own arguments plus the same required reason, the same Bearer token and the same approval. They answer with "text" (markdown). Arguments other than reason are optional unless the schema lists them as required. Example: curl -sS --max-time 150 -X POST "$BASE/second/tools/second_identity" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"reason":"Why this exact read is needed."}' Answer: {"ok":true,"tool":"second_identity","source":"laptop-live","text":"..."} ### second_identity POST /second/tools/second_identity Who the user is: their short, stable profile (the basic facts of their life and role, what they value, prioritize and habitually do). Read this first for any broad question about the user. Input JSON schema: {"additionalProperties":false,"properties":{"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason"],"type":"object"} ### second_portrait POST /second/tools/second_portrait Second's longer, evidence-grounded account of what the user values, aims at and does: the depth behind second_identity. Input JSON schema: {"additionalProperties":false,"properties":{"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason"],"type":"object"} ### second_style POST /second/tools/second_style How the user writes: their voice on each messaging channel and toward the people they write to most. Read it before drafting anything as the user. Optional person or channel narrows it. Input JSON schema: {"additionalProperties":false,"properties":{"channel":{"description":"One messaging channel's section only.","type":"string"},"person":{"description":"A person the user writes to: a wiki title, alias or first name.","type":"string"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason"],"type":"object"} ### second_wiki POST /second/tools/second_wiki Second's wiki of the people, projects, organizations, tools and places in the user's life. Without name: the catalog, most important first. With name (a title, alias or category/Title): everything Second knows about that one entity. Input JSON schema: {"additionalProperties":false,"properties":{"category":{"description":"Catalog filter: people, projects, organizations, tools or places.","type":"string"},"include_links":{"description":"Also return the entity's contact links; only when the task needs a way to reach them.","type":"boolean"},"limit":{"description":"Maximum rows to return.","type":"integer"},"name":{"description":"An entity's title, alias or category/Title.","type":"string"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason"],"type":"object"} ### second_episodes POST /second/tools/second_episodes The user's recent episodes: contiguous stretches of their real activity (a work session, a conversation, an errand), newest first. This is detailed personal activity; ask only when the task needs it. Input JSON schema: {"additionalProperties":false,"properties":{"last":{"description":"How many, newest first.","type":"integer"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason"],"type":"object"} ### second_search POST /second/tools/second_search Full-text search over the wiki entities and the user's episodes; hits are grouped by kind. Input JSON schema: {"additionalProperties":false,"properties":{"limit":{"description":"Maximum rows to return.","type":"integer"},"q":{"description":"The search text.","type":"string"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason","q"],"type":"object"} ### second_justnow POST /second/tools/second_justnow What the user was just doing: the last minutes of activity and the last hours as distilled moments, computed at read time. This is minute-by-minute personal activity; ask only when the task needs it. Input JSON schema: {"additionalProperties":false,"properties":{"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason"],"type":"object"} ### second_patterns POST /second/tools/second_patterns How the user's days usually run: the patterns Second infers from their activity, with guidance for an agent acting on the user's behalf. Input JSON schema: {"additionalProperties":false,"properties":{"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"}},"required":["reason"],"type":"object"} ### second_reminders POST /second/tools/second_reminders The user's reminders: Second's reconciled action list. With id: one reminder in full. Input JSON schema: {"additionalProperties":false,"properties":{"id":{"description":"An id from the list, for the full detail.","type":"string"},"limit":{"description":"Maximum rows to return.","type":"integer"},"q":{"description":"The search text.","type":"string"},"reason":{"description":"Explain why this exact read is needed. The user reviews this reason with the parameters.","maxLength":512,"minLength":1,"pattern":"\\S","type":"string"},"status":{"type":"string"}},"required":["reason"],"type":"object"} ## Responses Success is 200 with exactly one of "data" (a JSON object: always for second_tags and second_tag_wiki, and for Linear when a live connector answers) or "text" (markdown or plain text). Check which one is present: {"ok":true,"tool":"<tool name>","source":"<source>","data":{...}} {"ok":true,"tool":"<tool name>","source":"<source>","text":"..."} Quote what is there; do not add facts that are not in it. "source" says where it came from: - laptop-live: read from Second on the user's laptop just now. - linear-connector: read live from Linear through the user's connector. - second-profile: no live Linear connector; this is what Second itself had captured. - snapshot: the laptop is offline; this is the last offline copy and "snapshot_built_at" (RFC 3339) gives its time. Tell the user it is a snapshot and how old it is. Failures release no data: - 400 {"ok":false,"error":"bad_request","detail":"..."}: invalid request (missing or link-carrying reason, unknown field, missing required argument, wrong type). "detail" names the problem. Fix it and send it again; the user was not asked. - 401 {"ok":false,"error":"unauthorized"}: missing, invalid or expired Bearer token. Connect again. - 403 {"ok":false,"error":"denied"}: the user denied this read. Do not retry it; tell the user. - 404 {"ok":false,"error":"not_found"} or "unknown_tool": no such tag, tool or route. See GET /tools. - 405 {"ok":false,"error":"method_not_allowed"}: every read and both /auth/agent routes are POST with a JSON body; discovery is GET. - 408 {"ok":false,"error":"approval_expired"}: the user did not decide within two minutes. Nothing was read. Ask the user before trying again. - 503 {"ok":false,"error":"unavailable","detail":"..."}: cannot be served now (laptop offline with no snapshot for this read, no live connector, tool failed, or five reads already waiting). "detail" says which. Retrying at once will not help. ## Whole session, start to finish 1. GET https://second-demo-c6bfcc.westus.cloudapp.azure.com/agent (this guide). 2. Run the "Start" block; give the user the verification_uri it prints. 3. The user opens it on their laptop, signs in with Clerk, chooses Connect. 4. Run the polling block until 200; token.json holds the token. 5. POST /second/tags with a reason; the user approves on their phone; read data.tags. 6. POST /second/wiki with one tag_id and a reason; the user approves; read data. 7. Tell the user what you read and where it came from ("source").