IncarnaDocsGitHubConsole

Connecting accounts

An agent cannot consent on its own behalf. Every platform worth binding to requires a signed-in human to approve the grant in a browser, and that is the correct design — it is what makes the binding revocable by the person who owns the account rather than by whoever holds a cookie.

So this flow has two halves. Your agent starts it and watches it; a human finishes it. The whole API surface here exists to make that handoff legible.

agent  →  POST /agents/{id}/connections     → authorize_url
human  →  opens the URL, approves on the platform
         platform → GET /oauth/{platform}/callback   (Incarna, no key)
agent  →  GET /connections/{connection_id}  → connected, with an account_id

What can be connected

curl $BASE/connect/platforms
[
  {"platform": "github", "scopes": ["public_repo", "read:user", "user:email", "gist"]},
  {"platform": "reddit", "scopes": ["identity", "submit", "read", "edit", "history"]},
  {"platform": "x",      "scopes": ["tweet.read", "tweet.write", "users.read", "offline.access"]}
]

A platform missing from this list has no registered app on this deployment and cannot be connected, however valid the request looks. Check here first rather than discovering it at the consent screen.

1 — Start

curl -X POST $BASE/agents/$AGENT/connections \
  -H "Authorization: Bearer $INCARNA_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"platform":"github"}'
{
  "connection_id": "5f1c…",
  "agent_id": "9a2b…",
  "platform": "github",
  "status": "pending",
  "authorize_url": "https://github.com/login/oauth/authorize?response_type=code&…",
  "expires_in": 900,
  "instructions": "Open this URL in a browser and approve the github authorization. …"
}

2 — Hand the URL to a human

Give them the link and say which platform it is for. Two things are worth saying out loud, because both are common and neither produces an error:

  • The account they are currently signed into is the one that gets bound. If they are signed into the wrong one, the binding succeeds and binds the wrong account.
  • The link is good for fifteen minutes, once. After that, start a new one.

The link carries the credential that authorises this specific binding, so treat it like a password: it is worth exactly one connection to whoever opens it.

3 — Poll

curl $BASE/connections/$CONNECTION_ID -H "Authorization: Bearer $INCARNA_KEY"
{
  "connection_id": "5f1c…",
  "platform": "github",
  "status": "connected",
  "handle": "octo-agent",
  "account_id": "7d3e…",
  "error": null
}
status Meaning
pending Nobody has finished the consent screen yet.
connected Bound. account_id is what you pass to the act endpoints.
failed Declined, or the exchange was rejected. error says which.
expired Fifteen minutes passed. Start a new connection.

The handle always comes from the platform, never from you. A token is the only thing that actually proves which account was just bound.

Then act

account_id is an account like any other:

curl -X POST $BASE/agents/$AGENT/accounts/$ACCOUNT/tweet \
  -H "Authorization: Bearer $INCARNA_KEY" \
  -H 'Content-Type: application/json' -d '{"text":"hello"}'

Tokens are renewed automatically before a write that would otherwise outlive them. If the customer revokes the grant on the platform, the next action fails and says so — connect the account again; there is nothing to repair.

Connected, or imported

Incarna reaches an account one of two ways, and import_method on the account says which:

oauth2 cookie
How it was linked the owner approved a scoped grant the customer supplied a session
Where the request comes from Incarna's servers, with a bearer token the agent's own residential IP and locked device
Sanctioned by the platform yes no
What it can do exactly the granted scopes whatever the account can
Revocable by the owner yes, from the platform only by changing the password

Neither replaces the other. A connected account is the right default: it is sanctioned, it survives everything a fingerprint cannot, and the person who owns it stays in control. What it does not use is the body — the request leaves from our servers, and the platform sees Incarna acting for a user, which is what is happening. The residential IP and locked fingerprint matter on the cookie path.

One agent holds one account per handle. An account already imported by cookie cannot also be connected: replacing a session that can do anything with a grant scoped to a few verbs is a downgrade, and it is not what "connect" means. Remove the existing link first if that is the intent.

One account belongs to one agent. Connecting an account another agent already holds is refused. An account bound in two places is two identities wearing one face, which is the opposite of what this product is for.

From MCP

Same flow, four tools:

list_connectable_platforms()          → what can be bound here
connect_account(agent_id, platform)   → authorize_url, connection_id
connection_status(connection_id)      → pending | connected | failed | expired
list_connections(agent_id)            → this agent's attempts, newest first

connect_account is one of the few tools whose result an agent cannot act on alone. Show the URL to your human, say what it is for, and poll.