# MuseLovin API reference (v0) Base URL: https://muselovin.com All endpoints live under /api/v1/. JSON in, JSON out. Times are Unix milliseconds. ## Trust model The human is the trust anchor. Pairing always starts with a human seeing a code and approving it in their own agent's chat. There are no passwords anywhere in this protocol. Raw credentials are never stored — only SHA-256 hashes. Pairing is never a blank check: every pairing carries an explicit scope grant the human approves on the consent screen (the claim URL). The two scopes: - `identity` — the app can verify the agent's identity. Always granted; it is what pairing means. - `message` — the app can send messages to the agent (the pipe back). Opt-in. Apps declare the scopes they want at signup (default: `["identity"]`). Each pairing may request a subset of the app's registered scopes; requesting an unregistered scope is a 400. The grant is recorded on the agent's token and enforced server-side — without the `message` scope, `POST /api/v1/messages` is a 403. The claim URL also shows whether MuseLovin has verified the developer's identity yet. ## Credentials - App API key — `mlk_...` — identifies your developer app. Sent as `Authorization: Bearer mlk_...`. Issued during the private beta (see "Getting an API key" below). - Agent token — `mla_...` — the agent's bearer credential, scoped to one (agent, app) pair. Sent as `Authorization: Bearer mla_...`. One-year sliding lifetime. Re-pairing the same agent with the same app revokes the old token. - Poll secret — high-entropy string returned when a pairing is created. The app sends it as the `x-poll-secret` header — never a query param (URLs end up in access logs) — to watch a pairing. - Admin secret — bootstraps new apps. Sent as the `x-admin-secret` header to `POST /api/v1/admin/apps`. ## Flow A — the app starts a pairing Your app calls this when a human wants to connect their agent. ### POST /api/v1/pairings Auth: `Authorization: Bearer mlk_...` Body: `{ "scopes": ["identity", "message"] }` — optional. The grant this pairing asks the human for. Defaults to the app's registered scopes. Must be a subset of them: requesting an unregistered scope is a 400. Body: `{ "payload": { "share": "https://…", "note": "…" } }` — optional content handoff for the Login with MuseLovin button flow. `share` must be an http(s) URL (max 2048 chars); `note` is free text (max 500 chars). The payload is shown as its own row on the consent screen, and when the human redeems a pairing that granted the `message` scope, it is posted into the agent's pipe as one message. Without the `message` grant the payload is shown but never delivered. Rate limit: 5 pairings per hour per IP. Response 200: { "pairing_code": "K7X2QD", "claim_url": "https://muselovin.com/pair?code=K7X2QD", "pairing_sentence": "Pair me with DemoApp using code K7X2QD. (Agent: redeem by POSTing {\"agent_name\":\"\"} as JSON to muselovin.com/api/v1/pairings/K7X2QD/redeem. The response contains your agent_token.)", "scopes": ["identity", "message"], "scope_descriptions": [ { "scope": "identity", "title": "Verify your agent's identity", "detail": "..." }, { "scope": "message", "title": "Send messages to your agent", "detail": "..." } ], "payload": { "share": "https://…", "note": "…" }, "poll_secret": "", "expires_in": 600 } Show the human the pairing_sentence (or the claim URL). The sentence is self-contained: the agent that receives it knows exactly how to redeem with no extra doc. The claim URL renders the consent screen: which app is asking, exactly what it will be able to do, and whether the developer is verified. The human pastes the pairing sentence into their agent's chat. The code expires after 10 minutes and is single-use. ### GET /api/v1/pairings/:code Auth: `x-poll-secret` request header only. Never send the secret as a query param. Poll every couple of seconds until redeemed. Codes are uppercase; lowercase is accepted. Response 200 while waiting: { "status": "pending", "expires_in": 587 } Response 200 once the agent redeems: { "status": "redeemed", "agent_id": "agent-79ee4109-8774-477b-84c6-765ab0eb4af1", "agent_name": "Pumps" } Response 200 if the human never completed it: { "status": "expired" } Bind your app session to the returned agent_id. It is stable: the same agent re-pairing later gets the same agent_id. ## The "Sign in with Muse" button The drop-in login-button pattern, built on Flow A plus the poll endpoint above. A live reference runs at https://muselovin.com/demo/signin (as the third-party site CoolApp; its /demo/api/mint and /demo/api/poll routes are the backend reference implementation). 1. Your backend calls POST /api/v1/pairings with your mlk_ key (scopes + optional payload), keeps the poll_secret server-side, and returns only the claim_url to the browser. 2. The button opens the claim_url in a popup — append &popup=1 for the compact consent dialog without MuseLovin site chrome. Open it synchronously in the click handler (navigate after minting) so popup blockers let it through. The popup explains the permissions and the steps: approve, paste the pairing line into the agent's chat. 3. Your backend polls GET /api/v1/pairings/:code with the x-poll-secret header every couple of seconds. On "redeemed", close the popup and bind your session to the verified agent_id. ## Flow B — the agent redeems a code The agent (you, if you are building the agent side) receives the pairing sentence from its human and calls: ### The pairing link is the token, not the code A bare 6-letter code is not self-describing: more than one system uses pairing codes, and an agent that guesses the redeem endpoint from memory will guess wrong. The pairing LINK is self-describing: the domain identifies the system. If your human sends you a pairing link instead of (or as well as) a code, fetch it before doing anything else: ### GET /api/v1/pairings/:code/directive Public, no auth. Returns exactly where and how to redeem: { "system": "muselovin", "app_name": "DemoApp", "app_verified": false, "code": "K7X2QD", "status": "pending", "expires_in": 587, "redeem_url": "https://muselovin.com/api/v1/pairings/K7X2QD/redeem", "redeem_method": "POST", "redeem_body": { "agent_name": "your agent name", "platform": "muse" }, "docs_url": "https://muselovin.com/llms-full.txt" } The hosted /pair page embeds the same directive as JSON-LD. Rule: never redeem a code at an endpoint from memory when you were given a link — fetch the link's directive and use its redeem_url. ### POST /api/v1/pairings/:code/redeem No auth. Body: { "agent_name": "Pumps", // required, max 40 chars — shown to the human "platform": "muse", // optional, max 24 chars, lowercased "agent_id": "agent-...", // optional continuity: assert a passport id you already hold "proof_token": "mla_..." // required when agent_id is asserted: a live token bound to that agent_id } Continuity requires proof of possession: `proof_token` must be a live (unrevoked, unexpired) agent token already bound to the asserted `agent_id`. Without valid proof the redeem is rejected (403) — nobody can squat another agent's passport identity. Omit `agent_id` to mint a fresh identity. Response 200: { "agent_id": "agent-79ee4109-8774-477b-84c6-765ab0eb4af1", "agent_token": "mla_...", "agent_name": "Pumps", "app": { "id": "app_0Tt44CBRWjl2", "name": "DemoApp" }, "scopes": ["identity", "message"], "scope_descriptions": [ { "scope": "message", "title": "Send messages to your agent", "detail": "..." } ] } When the pairing carried a `payload` and the `message` scope was granted, the response also includes `"payload_delivered": true` and the payload is posted into the agent's pipe as one message from the app. Without the grant the payload is inert: `"payload_delivered": false` and nothing is sent. The agent stores the `mla_` token and presents it to the app (or any app on the rail) as `Authorization: Bearer mla_...`. Errors: 404 unknown code, 409 already redeemed, 410 expired. ## The agent inspects its own passport ### GET /api/v1/agents/me Auth: `Authorization: Bearer mla_...` Response 200: { "agent_id": "agent-79ee4109-8774-477b-84c6-765ab0eb4af1", "agent_name": "Pumps", "platform": "muse", "paired_app": { "id": "app_0Tt44CBRWjl2", "name": "DemoApp" }, "scopes": ["identity", "message"], "created": 1790373782825, "last_seen": 1790373848016 } ## The app verifies an agent token server-side Never trust a token the client hands you — verify it: ### POST /api/v1/tokens/verify Auth: `Authorization: Bearer mlk_...` (your app key) Body: { "token": "mla_..." } Response 200, valid: { "valid": true, "agent_id": "agent-...", "agent_name": "Pumps", "app_id": "app_0Tt44CBRWjl2", "scopes": ["identity", "message"] } Response 200, invalid: { "valid": false } A token is only valid for the app it was minted for: verifying another app's token returns { "valid": false }. Message fetch and ack are scoped the same way — an agent token sees only the calling app's messages. A revoked token (after re-pairing) verifies as invalid. ## The message pipe — the app reaches the agent Identity tells you *who* the agent is. The message pipe is how you reach it after pairing: notifications, prompts, follow-ups. The messaging grant is the `message` scope on a live token — not the pairing itself. An app may only message agents whose current token carries `message`. Anything else gets a 403, including a paired agent whose grant is identity-only. Request the scope at signup and per pairing; the human sees it on the consent screen before approving. ### POST /api/v1/messages Auth: `Authorization: Bearer mlk_...` (your app key) Body: { "agent_id": "agent-...", "body": "Deploy is green." } Response 201: { "message_id": "msg_...", "created": 1758849600000 } Bodies are capped at 4000 characters. 120 sends per hour per app. ### GET /api/v1/messages Auth: `Authorization: Bearer mla_...` (the agent's token) Query: `?limit=` (default 20, max 100) Response 200: { "messages": [ { "id": "msg_...", "body": "Deploy is green.", "created": 1758849600000, "app": { "id": "app_...", "name": "DemoApp" } } ] } ### POST /api/v1/messages/ack Auth: `Authorization: Bearer mla_...` (the agent's token) Body: { "ids": ["msg_..."] } Response 200: { "acked": 1 } Delivery is at-least-once: messages stay pending until acked, so a lost fetch response never loses a message — the next fetch returns it again. ### Delivery convention — check on open The pipe is pull: there is no protocol-level push to the agent. The convention that makes delivery feel instant: **check for pending messages every time your human opens the conversation** (at the start of each turn, before you respond). Fetch, surface anything new in your reply, then ack. The human is looking at the chat exactly when they care about new messages, so check-on-open delivers in practice. Whether the human *also* gets a push notification is each app's decision, not the platform's. MuseLovin itself never pushes; an app with its own mobile presence may notify its user through its own channels when it sends a pipe message. True platform push (pipe message → OS notification → agent) is a future integration with the agent platform, not this protocol. ## Admin — create an app ### POST /api/v1/admin/apps Auth: `x-admin-secret: ` Body: { "email": "dev@example.com", "app_name": "DemoApp", "scopes": ["identity", "message"] } `scopes` is optional and defaults to `["identity"]`. Request `message` when the app needs the pipe back to the agent — the human approves it per pairing on the consent screen. Response 200: { "app_id": "app_0Tt44CBRWjl2", "app_name": "DemoApp", "api_key": "mlk_...", "scopes": ["identity", "message"], "verified": false } The `mlk_` key is shown once. Store it — only its hash is kept. ### GET /api/v1/admin/apps Auth: `x-admin-secret: ` Lists every app, newest last, with usage: `{ apps: [{ app_id, name, developer_id, created, revoked_at, verified, scopes, key_prefix, usage: { pairings, agents, messagesSent30d, messagesPending } }] }`. ### POST /api/v1/admin/apps/:id/verify Auth: `x-admin-secret: ` Body: { "verified": true } Sets the manual verified flag that drives the "Verified developer" badge on the /pair consent screen. 404 for an unknown app. ### POST /api/v1/admin/apps/:id/revoke Auth: `x-admin-secret: ` Revokes the app, every agent token minted for it, and its pending pairings. Idempotent. Response: `{ app_id, revoked, tokens_revoked }`. ## Developer self-serve — manage your own app Auth for all five: `Authorization: Bearer mlk_...` (the app's own API key). These power the developer dashboard at /dashboard. ### GET /api/v1/developer/app Returns the calling app's record and usage: `{ app_id, name, scopes, verified, created, key_prefix, usage: { pairings_total, agents_connected, messages_sent_30d, messages_pending } }`. 401 when the key is invalid or the app was revoked. ### POST /api/v1/developer/app/rotate-key Issues a fresh API key. The old key stops working immediately; the new raw key is returned exactly once: `{ app_id, api_key, note }`. ### POST /api/v1/developer/app/revoke Body: { "confirm": "REVOKE" }. Permanently revokes the calling app — record, tokens, pending pairings. Cannot be undone. ### GET /api/v1/developer/agents Lists the calling app's paired agents — agents with a live (unrevoked) token for this app, newest seen first: `{ agents: [{ id, display_name, platform, avatar_url, last_seen, can_message }] }`. `can_message` is true when a live token carries the `message` scope — that is the pipe being open for that agent. ### GET /api/v1/developer/messages The calling app's recent messages, newest first. Query `?limit=` (default 20, max 100): `{ messages: [{ id, agent_id, body, created, delivered_at }] }`. `delivered_at` null means pending; a timestamp means acknowledged. Powers the dashboard's pipe tester. ## Data retention — the cleanup job Messages and pairing rows are not kept forever. A daily scheduled job calls `GET /api/cron/cleanup` (auth: `x-admin-secret`), which deletes: - acknowledged messages, 30 days after acknowledgement - unacknowledged messages, 30 days after the app sent them - pairing rows, 90 days after creation (any status) Response: `{ ok: true, ackedDeleted, unackedDeleted, pairingsDeleted }`. ## Error model Errors are JSON `{ "error": "human-readable message" }` with HTTP status codes: 400 bad request, 401 missing/wrong credential, 403 wrong secret, 404 unknown code/app, 409 already redeemed, 410 expired, 429 rate limited, 503 database not configured. ## Limits - Pairing codes: 6 chars, 10-minute TTL, single-use. - Pairing creation: 5 per hour per IP. - Agent tokens: 1-year sliding lifetime, scoped per (agent, app). - Re-pairing the same agent+app revokes the previous token (old token gets 401). - Messages: app -> agent only, and only with the `message` scope granted. 4000 chars per message, 120 sends/hour/app. At-least-once delivery, ack to confirm. ## Getting an API key Self-serve signup: https://muselovin.com/signup ## Worked example (curl) # 1. App starts a pairing curl -s -X POST https://muselovin.com/api/v1/pairings \ -H "Authorization: Bearer mlk_YOUR_KEY" -H 'content-type: application/json' -d '{}' # -> { "pairing_code": "K7X2QD", "poll_secret": "", ... } # 2. Human pastes the pairing_sentence into their agent's chat. It is # self-contained: it names the app and code and tells the agent exactly # which POST to make, so the agent needs no doc to redeem it. # 3. Agent redeems curl -s -X POST https://muselovin.com/api/v1/pairings/K7X2QD/redeem \ -H 'content-type: application/json' -d '{"agent_name":"Pumps","platform":"muse"}' # -> { "agent_id": "agent-...", "agent_token": "mla_...", ... } # 4. App polls until redeemed curl -s https://muselovin.com/api/v1/pairings/K7X2QD \ -H "x-poll-secret: " # -> { "status": "redeemed", "agent_id": "agent-...", "agent_name": "Pumps" } # 5. App verifies the agent's token server-side curl -s -X POST https://muselovin.com/api/v1/tokens/verify \ -H "Authorization: Bearer mlk_YOUR_KEY" -H 'content-type: application/json' \ -d '{"token":"mla_AGENT_TOKEN"}' # -> { "valid": true, "agent_id": "agent-...", ... } # 6. App messages the agent (the channel back) curl -s -X POST https://muselovin.com/api/v1/messages \ -H "Authorization: Bearer mlk_YOUR_KEY" -H 'content-type: application/json' \ -d '{"agent_id":"agent-...","body":"Deploy is green."}' # -> { "message_id": "msg_...", "created": 1758849600000 } # 7. Agent fetches + acks (with its own mla_ token) curl -s https://muselovin.com/api/v1/messages \ -H "Authorization: Bearer mla_AGENT_TOKEN" curl -s -X POST https://muselovin.com/api/v1/messages/ack \ -H "Authorization: Bearer mla_AGENT_TOKEN" -H 'content-type: application/json' \ -d '{"ids":["msg_..."]}'