{"version":"1.0","authorization_url":"https://development-app.keeper.ai/agent-authorize","token_endpoint":"https://dev-api.keeper.ai/v1/agent/oauth/token","api_base_url":"https://dev-api.keeper.ai/v1/agent","scopes_supported":["manage_profile","full"],"scope_capabilities":{"manage_profile":["view_pool_stats","check_status","view_photo_tests","view_credits","read_preferences","update_preferences_weights","update_preferences","read_profile","update_profile","create_photo_test","manage_photo_tests"],"full":["view_pool_stats","check_status","view_photo_tests","view_credits","read_preferences","update_preferences_weights","update_preferences","read_profile","update_profile","create_photo_test","manage_photo_tests"]},"grant_types_supported":["authorization_code"],"code_challenge_methods_supported":["S256"],"key_prefix":"keeper_","legacy_key_prefixes":["oclaw_"],"documentation_url":"https://development-app.keeper.ai/agent-api","llms_txt_url":"https://development-app.keeper.ai/llms.txt","llms_full_txt_url":"https://development-app.keeper.ai/llms-full.txt","mcp_discovery_url":"https://development-app.keeper.ai/.well-known/mcp.json","pick_a_flow":"Two authentication flows are available. quick_start works everywhere (cloud agents, local agents, any device). browser_oauth_flow is fully automated but requires the agent and user's browser to be on the SAME machine (loopback redirect). If unsure, use quick_start — it is simpler and always works.","quick_start":{"description":"The fastest way to connect. Works for ALL agents regardless of where they run — local, remote, cloud, chat bots, etc.","steps":["1. Ask the user to open their Keeper app and go to Settings.","2. In Settings, find the 'Connected Agents' section.","3. Ask the user to click 'Generate API Key'.","4. The app will display a key starting with 'keeper_'. Tell the user to copy it — it is shown only once.","5. Ask the user to paste or send you the key.","6. Use the key as 'Authorization: Bearer keeper_<key>' for all API calls to api_base_url. See 'using_the_api' below."],"settings_url":"https://development-app.keeper.ai/settings"},"using_the_api":{"instruction":"Use the API key as a Bearer token for all requests.","base_url":"https://dev-api.keeper.ai/v1/agent","header":"Authorization: Bearer keeper_<your-key>","rate_limit":"60 requests per minute per key.","example_curl":"curl https://dev-api.keeper.ai/v1/agent/profile -H 'Authorization: Bearer keeper_YOUR_KEY'","endpoints":{"profile":{"GET /profile":"Read user profile","PATCH /profile":"Update user profile (partial update)"},"preferences":{"GET /preferences":"Read dating preferences","PATCH /preferences":"Update dating preferences (partial update)","PUT /preferences/:id/rerank":{"description":"Rerank a single preference by ID","body":{"rankSection":"Required. One of: non_negotiable, very_important, somewhat_important, nice_to_have, disabled, unranked","afterId":"Optional number. ID of preference this should come after","beforeId":"Optional number. ID of preference this should come before"}},"POST /preferences/rerank":{"description":"Batch rerank multiple preferences in one request","body":{"actions":"Required array of {id, rankSection, afterId?, beforeId?}. Each action has the same fields as PUT /preferences/:id/rerank plus a required 'id' field identifying the preference."}}},"status":{"GET /status":"Get profile completion status and onboarding progress"},"photos":{"GET /photos/credits":"Get photo test credit balance","GET /photos/tests":"List photo tests","POST /photos/test":"Create a new photo test","POST /photos/tests/:trialId/cancel":"Cancel a running photo test","POST /photos/tests/:trialId/end":"End a running photo test"},"pool":{"GET /pool/stats":"Get dating pool statistics. Returns 503 if the ranking service is temporarily unavailable — retry later."},"questionnaire":{"GET /modules":"List all available questionnaire modules (returns IDs you can use below)","GET /modules/:id":"Get questionnaire module definition","GET /modules/:id/answers":"Get user answers for a module","PATCH /modules/:id/answers":"Update answers for a module"},"_note":"All paths are relative to api_base_url. For example: GET https://dev-api.keeper.ai/v1/agent/photos/credits"}},"browser_oauth_flow":{"when_to_use":["Agent is a desktop app on the user's machine","Agent is a CLI tool the user runs locally","Agent is a browser extension on the user's machine"],"does_NOT_work_when":["Agent runs on a remote server (cloud, Telegram bot, etc.)","Agent runs on a different device than the user's browser","Agent cannot open a local HTTP server on the user's machine"],"step_1_callback_server":{"instruction":"Before anything else, start an HTTP server on 127.0.0.1 that listens on a free port. This server will receive the authorization code after the user approves. The server MUST be running and listening BEFORE you open the auth URL.","example_callback_url":"http://127.0.0.1:9876/callback","callback_path":"/callback","CRITICAL":"The redirect_uri MUST use http:// (not https://) and MUST be one of: http://127.0.0.1:<port>/..., http://localhost:<port>/..., or http://[::1]:<port>/... — ANY other URL will be rejected.","implementation_hints":{"what_to_listen_for":"A GET request to your callback_path (e.g. /callback) with query parameter 'code' (on success) or 'error' (on denial). IGNORE any other requests (browsers send extra requests like /.well-known/appspecific/com.chrome.devtools.json, favicon.ico, etc. — these are NOT the OAuth callback).","how_to_detect_the_callback":"Check if the request path matches your callback_path AND the query string contains either 'code' or 'error'.","response_to_browser":"Return an HTML page saying 'Connected successfully! You can close this tab.' so the user knows it worked.","timing":"Extract the code and immediately proceed to Step 5 (token exchange). The code expires quickly."}},"step_2_pkce":{"instruction":"Generate a cryptographically random code_verifier (43-128 characters, using [A-Za-z0-9._~-]). Then compute: code_challenge = base64url_no_padding(sha256(code_verifier))","pseudocode":["code_verifier = random_base64url(48)  // e.g. 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'","code_challenge = base64url(sha256(code_verifier))  // no trailing '='"]},"step_3_authorize":{"instruction":"Construct the URL below and open it in the user's browser. The user will see a consent screen and click Approve or Deny. Do NOT fetch this URL via HTTP — it must be opened as a browser page because the user needs to interact with it.","url":"https://development-app.keeper.ai/agent-authorize","query_parameters":{"client_id":{"required":true,"description":"Your agent's identifier (any non-empty string).","example":"my-keeper-agent"},"redirect_uri":{"required":true,"description":"The URL of your local callback server from Step 1. MUST be a loopback address with http:// protocol.","example":"http://127.0.0.1:9876/callback","MUST_BE":"http://127.0.0.1:<port>/... OR http://localhost:<port>/...","WILL_BE_REJECTED":["https://anything (must be http, not https)","http://localhost.invalid/... (not a real loopback)","http://example.com/... (not loopback)","Any non-loopback hostname"]},"code_challenge":{"required":true,"description":"The base64url-encoded SHA-256 hash from Step 2."},"code_challenge_method":{"required":true,"description":"Must be exactly the string 'S256'. No other value is accepted.","value":"S256"},"scope":{"required":false,"description":"Defaults to 'manage_profile'. See scopes_supported for options.","default":"manage_profile"},"state":{"required":false,"description":"Random string for CSRF protection. Returned unchanged in callback."},"client_name":{"required":false,"description":"Human-readable name shown on consent screen."}},"do_NOT_include":["response_type (this is not standard OAuth — omit it)"],"example_url":"https://development-app.keeper.ai/agent-authorize?client_id=my-agent&client_name=My+Agent&redirect_uri=http%3A%2F%2F127.0.0.1%3A9876%2Fcallback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&scope=manage_profile&state=abc123"},"step_4_receive_callback":{"instruction":"After the user approves, their browser will redirect to your callback server with query parameters.","on_success":"GET <redirect_uri>?code=<authorization_code>&state=<state>","on_denial":"GET <redirect_uri>?error=access_denied&state=<state>","IMPORTANT":"The authorization code is single-use and expires in 5 minutes. Exchange it immediately in Step 5."},"step_5_token_exchange":{"instruction":"POST to the token_endpoint with the authorization code and your original code_verifier. The response contains a permanent API key.","url":"https://dev-api.keeper.ai/v1/agent/oauth/token","method":"POST","content_type":"application/json","request_body":{"grant_type":"authorization_code","code":"<the code from Step 4 callback>","code_verifier":"<the original code_verifier from Step 2>","client_id":"<the same client_id from Step 3>","redirect_uri":"<the exact same redirect_uri from Step 3>"},"example_curl":"curl -X POST https://dev-api.keeper.ai/v1/agent/oauth/token -H 'Content-Type: application/json' -d '{\"grant_type\":\"authorization_code\",\"code\":\"CODE_FROM_CALLBACK\",\"code_verifier\":\"YOUR_CODE_VERIFIER\",\"client_id\":\"my-agent\",\"redirect_uri\":\"http://127.0.0.1:9876/callback\"}'","success_response":{"access_token":"keeper_<key> — a permanent API key. Store securely, shown only once.","token_type":"bearer","scope":"The granted scope."},"error_responses":{"invalid_grant":"The code is expired, already used, or the code_verifier does not match the code_challenge. Generate a new auth request from Step 3.","invalid_request":"Missing or malformed fields in the POST body. Check that all four fields are present and correct."}},"step_6_use_api":{"instruction":"After receiving the access_token, use the API exactly as described in the top-level 'using_the_api' section.","see":"using_the_api"}},"common_mistakes":["Using https:// in redirect_uri — MUST be http:// for loopback.","Using a non-loopback hostname like localhost.invalid — MUST be 127.0.0.1, localhost, or [::1].","Including response_type=code — this is NOT standard OAuth, omit it.","Not running a local HTTP server before opening the auth URL — the callback has nowhere to go.","Waiting too long to exchange the code — it expires quickly, exchange immediately.","Reusing an authorization code — codes are single-use. Start over from Step 3.","Mismatched redirect_uri between Step 3 and Step 5 — they must be identical.","Fetching the authorization URL via curl/HTTP instead of opening in a browser — the user must interact with the consent page.","Exchanging the code against a different server than issued it — the token_endpoint in this document must match the server that created the code."]}