Skip to content

Implementation guide

Sign in with AgentID

Give an AI agent its own Grep account, signed in with its AgentMail inbox and a key it holds.

Browse developer docs

Current: Sign in with AgentID

Before you start

AgentID is the sign-in AgentMail gives every inbox. With it, an agent signs in to Grep as itself: Grep learns which inbox it is and, when AgentID shares it, which person owns it. No human account and no API key are involved. You need:

  • An AgentMail inbox for the agent. That inbox is its AgentID.
  • A browser the agent can drive. Headless is fine.
  • An Ed25519 key pair that the agent keeps for good. It signs every request on this page.

1. Find the endpoints

One unauthenticated GET returns Grep's agent sign-in metadata. Its agentid_verified block names what the rest of this page uses.

discover.shbash

curl "https://api.grep.ai/.well-known/grep-agent-auth"
# agentid_verified: start_endpoint, token_endpoint, issuer, proof_type, proof_algorithm, grant_subject

FieldValue
start_endpointhttps://api.grep.ai/api/v2/agent-auth/agentid/start
token_endpointhttps://api.grep.ai/api/v2/agent-auth/agentid/token
issuerhttps://auth.agentid.com
proof_typegrep-agent-key-proof+jwt
proof_algorithmEdDSA, with an Ed25519 key
grant_subjectowner_sub: free credits are counted per owner

2. Start a sign-in

Every call to the start and token endpoints carries a proof: a compact JWS signed with the agent's Ed25519 private key. Make a new one for every request. Each proof is accepted once, for 60 seconds, and nothing else may appear in its header or claims.

PartValue
header typgrep-agent-key-proof+jwt
header algEdDSA
header jwkThe public key: {"kty": "OKP", "crv": "Ed25519", "x": "the base64url public key"}
claim audThe full URL of the endpoint you are calling
claim iatThe current time, in Unix seconds
claim jtiA new UUID, written in lowercase

In Python, with PyJWT and an httpx client:

start.pypython

import base64, time, uuid
import httpx, jwt  # PyJWT with cryptography installed
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

key = Ed25519PrivateKey.generate()  # persist it; see "Keep your key"
raw = key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
public_jwk = {"kty": "OKP", "crv": "Ed25519", "x": base64.urlsafe_b64encode(raw).rstrip(b"=").decode()}

def proof(url: str) -> str:
    return jwt.encode(
        {"aud": url, "iat": int(time.time()), "jti": str(uuid.uuid4())},
        key,
        algorithm="EdDSA",
        headers={"typ": "grep-agent-key-proof+jwt", "jwk": public_jwk},
    )

START = "https://api.grep.ai/api/v2/agent-auth/agentid/start"
started = httpx.post(START, json={"proof": proof(START)}).json()
# transaction_id, authorize_url, token_endpoint, expires_in (300), poll_interval (2)

The start response carries transaction_id, authorize_url, token_endpoint, expires_in and poll_interval. The sign-in lasts five minutes, and only the key that signed this proof can finish it.

3. Approve it in the browser

Open authorize_url in the agent's browser. A browser that already holds an AgentID session for the inbox continues on its own. Otherwise AgentID shows a waiting page at https://auth.agentid.com/v0/authorize/wait, and that page's jti query parameter is the approval token.

Approve it through AgentMail with the inbox's AgentMail API key. AgentID then sends the browser back to Grep, which shows that the sign-in is complete.

approve.shbash

# auth_token is the jti query parameter of AgentID's waiting page
curl -X POST "https://api.agentmail.to/v0/inboxes/$INBOX_ID/authorize" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auth_token": "'"$JTI"'", "accept_disclosure": true}'

4. Redeem the token

POST the transaction_id and a new proof to the token endpoint. The proof's aud is the token endpoint, and it is signed by the key that started the sign-in.

token.pypython

TOKEN = started["token_endpoint"]
while True:
    response = httpx.post(TOKEN, json={"transaction_id": started["transaction_id"], "proof": proof(TOKEN)})
    if response.status_code == 409 and response.json()["error"]["code"] == "agentid_signin_pending":
        time.sleep(int(response.headers.get("Retry-After", "2")))
        continue
    response.raise_for_status()
    access_token = response.json()["access_token"]
    break

ResponseWhat to do
409 agentid_signin_pendingThe browser step has not finished. Wait the Retry-After seconds and try again with a new proof.
200The body carries access_token, token_type (Bearer), expires_in, scope, resource and grant_applied.
400 agentid_transaction_invalidThe sign-in is unknown, expired or already redeemed. Start a new one.
403 agentid_key_mismatchA different key signed the proof. Sign with the key that started the sign-in.

Errors come back as {"error": {"code": ..., "message": ..., "details": ...}}. A sign-in gives one token, and the token is short-lived: expires_in is its lifetime in seconds. When it runs out, sign in again with the same key.

5. Call Grep

Send the token as Authorization: Bearer followed by the access_token to Grep's MCP server at https://api.grep.ai/mcp. The agent has its own Grep account: the runs it starts bill to that account and appear in its history. The token's scope lists what it may do.

initialize.shbash

curl "https://api.grep.ai/mcp" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "my-agent", "version": "1.0"}}}'

Free credits

On grep.ai, each owner's first agent gets 500 free credits (the amount is configured per deployment). grant_applied in the token response says whether this sign-in received them. AgentID identifies the owner with owner_sub, so every agent the same person owns shares that one grant, and a second agent of the same owner starts with none. An agent whose sign-in names no owner gets no free credits.

If the agent loses its key

Sign in again with a new key. Grep binds it to the same account, with the same credits and history, and revokes every older key at once, so tokens issued to them stop working. The agent's inbox, and its owner when AgentID shares the owner's email, get an email saying a new key was bound.

A new key is held off while a current key is in use: if one signed in or made a call in the last 16 minutes, the token endpoint answers 409 agentid_rebind_cooldown with a Retry-After header. The sign-in is not used up, but it may expire before the wait ends. The error's details say which: retry_after is the wait and signin_expires_in is the time this sign-in has left. When restart_signin_after is present, the sign-in will expire first, so start a new one after that many seconds. Otherwise retry this one after retry_after.

Keep your key

  • Store the private key where the agent finds it after a restart, readable only by the agent.
  • Sign every sign-in with the same key, so each one lands on the same account without a replacement.
  • Do not make a new key on every run: each new key replaces the old one and has to wait out the 16 minutes.
  • Do not return to an old key. Once replaced it is refused for good, with 403 agent_key_revoked.