Agent registration is in beta. For the user-facing overview of how approval works, see Agent registration. This page is the deeper protocol reference for developers building agents.
Migma follows the auth.md convention, so an agent can discover the flow and obtain its own scoped API key without a human copying a secret. The flow is user-claimed: the agent registers intent, the user approves with a short code, and the agent polls for the key.
Base URL: https://api.migma.ai
The skill file an agent reads is served at https://api.migma.ai/auth.md.
Discovery
Agents discover the endpoints and supported scopes from the well-known metadata. The agent_auth block in the authorization-server document lists the claim and identity endpoints.
GET https://api.migma.ai/.well-known/oauth-authorization-server
GET https://api.migma.ai/.well-known/oauth-protected-resource
GET https://api.migma.ai/.well-known/oauth-protected-resource/mcp
The authorization-server document includes scopes_supported, token_endpoint, grant_types_supported, and the agent_auth endpoint map.
An agent that hits a protected API without a valid key receives 401 with a WWW-Authenticate challenge that points back to the protected-resource metadata, so it can find this flow:
WWW-Authenticate: Bearer resource_metadata="https://api.migma.ai/.well-known/oauth-protected-resource"
The MCP endpoint returns the same challenge pointing at its own resource metadata (/.well-known/oauth-protected-resource/mcp).
Endpoints
| Method | Path | Auth | Rate limit | Purpose |
|---|
GET | /auth.md | None | — | The agent skill file |
GET | /.well-known/oauth-authorization-server | None | — | Authorization-server metadata (scopes, endpoints) |
GET | /.well-known/oauth-protected-resource | None | — | Protected-resource metadata for the API |
GET | /.well-known/oauth-protected-resource/mcp | None | — | Protected-resource metadata for the MCP server |
POST | /agent/identity | None | 10 / hour per IP | Step 1 — register intent, get a claim token and user code |
POST | /agent/identity/claim | None | 20 / hour per IP | Step 4 — refresh an expired user code |
POST | /oauth2/token | None | 120 / 5 min per IP | Step 3 — poll for the issued key |
The claim page endpoints (/agent/identity/claim/lookup and /agent/identity/claim/complete) are used by the Migma web UI when the user enters their code. Agents do not call them.
Step 1 — Register intent
curl -X POST https://api.migma.ai/agent/identity \
-H "Content-Type: application/json" \
-d '{
"type": "service_auth",
"login_hint": "user@example.com",
"agent_name": "Claude",
"scope": "audience:read campaign:read email:send"
}'
type — required, must be service_auth. Identity-assertion and anonymous registration are not supported yet.
login_hint — required. The email of the user the agent acts for. It is display only; it grants nothing by itself.
agent_name — optional. Shown to the user at approval. Use your product name.
scope — optional, space-delimited. Omit for a read-mostly default. Request only what you need.
Response:
{
"claim_token": "<JWT, keep private>",
"claim": {
"user_code": "XXXX-XXXX",
"verification_uri": "https://migma.ai/claim",
"verification_uri_complete": "https://migma.ai/claim?code=XXXX-XXXX",
"expires_in": 600,
"interval": 5
}
}
Step 2 — Send the user to approve
Surface verification_uri_complete (or verification_uri plus user_code) to the user. They sign in to Migma, review the agent name and requested scopes, and confirm the code.
Step 3 — Poll for the key
Poll the token endpoint every interval seconds (default 5):
curl -X POST https://api.migma.ai/oauth2/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:workos:agent-auth:grant-type:claim",
"claim_token": "<from step 1>"
}'
Responses:
| Status | error | What to do |
|---|
400 | authorization_pending | The user has not confirmed yet. Keep polling. |
400 | expired_token | The user code expired. Refresh it (Step 4), surface the new code, keep polling. |
400 | invalid_grant | The claim is no longer usable. Start over at Step 1. |
200 | — | Success. The key is in access_token. |
Success body:
{
"access_token": "sk_live_…",
"token_type": "bearer",
"scope": "audience:read campaign:read email:send",
"key_name": "Agent: Claude"
}
The access_token is a Migma platform API key and is returned exactly once — store it securely now. Further polls return invalid_grant.
Step 4 — Refresh an expired user code
Only when polling returned expired_token:
curl -X POST https://api.migma.ai/agent/identity/claim \
-H "Content-Type: application/json" \
-d '{ "claim_token": "<from step 1>" }'
Returns a fresh claim object with a new user_code. The whole registration expires one hour after Step 1; after that, start over.
Code lifecycle
- The
user_code expires after expires_in seconds (600 by default).
- Refresh an expired code with
POST /agent/identity/claim using the same claim_token.
- The whole registration expires one hour after Step 1.
- The
access_token is issued exactly once; re-polling after issuance returns invalid_grant.
Poll loop example
async function registerAgent(loginHint) {
const intent = await fetch('https://api.migma.ai/agent/identity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'service_auth',
login_hint: loginHint,
agent_name: 'Claude',
scope: 'audience:read campaign:read email:send'
})
}).then((r) => r.json());
let { claim_token, claim } = intent;
console.log('Approve at:', claim.verification_uri_complete);
while (true) {
await new Promise((r) => setTimeout(r, claim.interval * 1000));
const res = await fetch('https://api.migma.ai/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'urn:workos:agent-auth:grant-type:claim',
claim_token
})
});
const body = await res.json();
if (res.ok) return body.access_token; // sk_live_…
if (body.error === 'authorization_pending') continue;
if (body.error === 'expired_token') {
const refreshed = await fetch('https://api.migma.ai/agent/identity/claim', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ claim_token })
}).then((r) => r.json());
claim = refreshed.claim;
claim_token = refreshed.claim_token;
console.log('New code:', claim.verification_uri_complete);
continue;
}
// invalid_grant or anything else — start over
throw new Error(`Registration failed: ${body.error}`);
}
}
Scopes
Scopes are space-delimited in the scope field of Step 1. Omit the field for a read-mostly default; request only the scopes the agent needs. The available scopes are published in scopes_supported in the discovery document:
| Scope | Grants |
|---|
audience:read | View subscribers, tags, and segments |
audience:write | Add and update subscribers, manage tags, create segments |
email:read | View emails and sending history |
email:send | Send emails and campaigns |
email:validate | Run compatibility, link, and deliverability checks |
email:preview | Generate device and client previews |
domain:read | View sending domains and verification status |
domain:write | Add, verify, update, and remove sending domains |
campaign:read | View campaigns and their stats and logs |
campaign:write | Create, send, schedule, and manage campaigns |
webhook:read | List and view webhooks |
webhook:write | Create, update, and delete webhooks |
project:write | Import projects and manage project resources |
Using the credential
The access_token is an ordinary Migma API key. Send it as a bearer token and discover a project to work in:
# 1. List projects to get a projectId
curl https://api.migma.ai/v1/projects?limit=1 \
-H "Authorization: Bearer sk_live_…"
# 2. Use that projectId on project-scoped requests
curl "https://api.migma.ai/v1/campaigns?projectId=PROJECT_ID" \
-H "Authorization: Bearer sk_live_…"
The key is account-scoped, so pass projectId per request; a resource in a project you did not name returns 404. The key works everywhere a Migma key works: the REST API, the SDK, the CLI, and MCP, typically by setting MIGMA_API_KEY.
Revocation
Agent keys appear in the Migma dashboard under Settings → API Integration alongside your own keys. The user can revoke one at any time. A revoked key fails with 401; the agent should re-register to obtain a new one.