# Second API Base URL: https://second-cloud-737c09.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. Each person who signed up has their own account here, and an agent reads one account at a time. An approved read is answered by the user's own Mac while it is attached with Second running, and reads real live data; while the Mac is away, Second reads are answered from the user's synced copy and say so; Linear needs the attached Mac. Everything here is plain HTTP JSON that curl can drive: you need no MCP connector. No credentials or personal records are exposed by discovery. In short: connect once (the user signs in and approves you 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 the authentication handoff. - GET /.well-known/oauth-protected-resource: which authorization server issues tokens for this API. - GET /agent: this same guide (also served at / and /llms.txt). ## Connect an agent or terminal Access tokens are issued by the authorization server named in GET /.well-known/oauth-protected-resource ("authorization_servers"), through the OAuth 2.1 device authorization grant. Its endpoints are listed at /.well-known/oauth-authorization-server. 1. POST the device_authorization_endpoint with client_id (your client identifier), scope "second:read" and resource "https://second-cloud-737c09.westus.cloudapp.azure.com". The response carries device_code, user_code, verification_uri, verification_uri_complete, expires_in and interval. 2. Show the user verification_uri_complete exactly as issued and ask them to open it. The user signs in and explicitly approves you. Never enter the user's credentials, act on their login, or approve for them. Do not fetch, shorten or replace the link. 3. Poll the token_endpoint with grant_type "urn:ietf:params:oauth:grant-type:device_code", device_code and client_id, no faster than the returned interval. authorization_pending means wait; slow_down means poll more slowly; access_denied or expired_token means stop. 4. The token response carries access_token (a JWT for this API only) and expires_in. Send it as Authorization: Bearer on every read. Keep it private; do not put it in URLs, logs or shared command history. 5. Your first read answers 202 {"ok":false,"error":"pending_connect","retry_after":15}: the user is asked on their phone to name you and connect you, once. Send a "client_name" field (3 to 40 letters, digits, spaces, hyphens or periods) in that first read's body; it is the name the question shows. Retry after the given seconds until the read answers 200. A 403 {"ok":false,"error":"disconnected"} means the user did not connect you or has disconnected you: stop and tell the user. ### Copy-paste: connect with curl Each block is self-contained (state is kept in private files), so it works even when your shell does not keep variables between commands. ISSUER is the authorization server from the protected-resource document; CLIENT_ID is your client identifier. BASE='https://second-cloud-737c09.westus.cloudapp.azure.com'; ISSUER='https://just-kiwi-9861.clerk.accounts.dev'; CLIENT_ID='' umask 077; mkdir -p "$HOME/.second-agent"; cd "$HOME/.second-agent" curl -sS -X POST "$ISSUER/oauth/device_authorization" \ -d "client_id=$CLIENT_ID" -d 'scope=second:read' -d "resource=$BASE" > device.json sed -n 's/.*"device_code" *: *"\([^"]*\)".*/\1/p' device.json > device_code sed -n 's/.*"verification_uri_complete" *: *"\([^"]*\)".*/Ask the user to open: \1/p' device.json Give the user that link, then poll until the answer is neither authorization_pending nor slow_down (after slow_down, wait 5 seconds longer): BASE='https://second-cloud-737c09.westus.cloudapp.azure.com'; ISSUER='https://just-kiwi-9861.clerk.accounts.dev'; CLIENT_ID='' umask 077; cd "$HOME/.second-agent" while :; do curl -sS -o token.json -X POST "$ISSUER/oauth/token" \ -d 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \ -d "device_code=$(cat device_code)" -d "client_id=$CLIENT_ID" grep -q slow_down token.json && sleep 5 grep -q -e authorization_pending -e slow_down token.json || break sleep 5 done grep -q access_token token.json && echo connected Then make your first read, which names you to the user (3 to 40 letters, digits, spaces, hyphens or periods). It answers 202 pending_connect until the user connects you; repeat it after the given seconds until it answers 200: BASE='https://second-cloud-737c09.westus.cloudapp.azure.com'; CLIENT_NAME='' TOKEN=$(sed -n 's/.*"access_token" *: *"\([^"]*\)".*/\1/p' "$HOME/.second-agent/token.json") curl -sS --max-time 150 -X POST "$BASE/second/tags" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d "{\"limit\":20,\"offset\":0,\"client_name\":\"$CLIENT_NAME\",\"reason\":\"First read: list the tags to find the project the user asked me about.\"}" Every read below starts the same way: BASE='https://second-cloud-737c09.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 attached Mac; 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":"mac-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":"mac-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 Mac. 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. ## 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: - mac-live: read from Second on the user's own Mac 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. - cloud-copy: the user's Mac is away; this is the user's synced copy and "snapshot_built_at" (RFC 3339) gives its time. Tell the user it is a copy 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","error_description":"..."}: missing, invalid or expired Bearer token; the WWW-Authenticate header says which. Connect again. - 202 {"ok":false,"error":"pending_connect","retry_after":15}: the user has not connected you yet. Retry after that many seconds. - 403 {"ok":false,"error":"disconnected"}: the user did not connect you or disconnected you. Stop and tell the user. - 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 is 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 (Mac away with no copy 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-cloud-737c09.westus.cloudapp.azure.com/agent (this guide). 2. Run the device-authorization block; give the user the link it prints. 3. The user opens it, signs in, and approves you. 4. Run the polling block until token.json holds access_token. 5. POST /second/tags with a reason and your client_name; on 202, wait for the user to connect you on their phone and retry. 6. The user approves the read on their phone; read data.tags. 7. POST /second/wiki with one tag_id and a reason; the user approves; read data. 8. Tell the user what you read and where it came from ("source").