File DA-042 View as Markdown

REST API

Every public Double Agent route, with the key it needs and what it returns. Most sites only need the script and token checks on the server. Come here when you call the API yourself or export data.

You want toSection
Send a beacon, get or verify a token, report from your server or edgeCollection and tokens
Read your site's stats, sessions, visitors and threatsWebsite data
Report agents acting for your customers, and audit what they didCaller identity
Read what visitors are trying to doIntent
Read your public agent's calls, tasks and revenueAgent analytics
Look up agents in the public directoryAgent directory
Manage accounts, sites, domains and keysAccounts and auth

Auth

Base URL: https://api.doubleagent.so. Errors look like { "error": { "code", "message" } }.

AuthHeader
none–
pk?k=pk_… (your public key)
sessionAuthorization: Bearer das_… (portal or CLI login)
skAuthorization: Bearer sk_… (your secret key, server only)

Rate limits: Security. Routes the portal uses for team members, billing, usage, account closure and Search Console management are not listed here.

Collection and tokens

RouteAuthReturns
POST /v1/collectnone (keyless, by Origin) or pk204 (the SDK beacon)
POST /v1/checkpk{ verdict, token, intent }; keyless → 401 key_required. intent is this beacon's (Intent), never in the token
POST /v1/pingpkLive presence
POST /v1/verifynone{ token } → { valid: true, claims }, or { valid: false, error }. For servers that can't verify the token themselves (Server)
GET /.well-known/jwks.jsonnoneSigning keys
GET /v1/install-check?url=noneInstall report (see /install.md step 5)
POST /v1/hitssk (server-side only), verified site{ accepted }, 202. Body { v: 1, hits: [...] }, at most 100 per batch. Each hit: ts (ms, within the last hour), m (method), h (host), p (path, no query), s (status), and optionally ua, ip, hd (up to 24 lowercase request headers; never cookies or credentials), cf (country, region, city, lat, lon, asn, org, tls, http) and t (the page's ticket nonce) and qf (up to 8 query pattern codes: sqli, xss, trav, ssrf, cmd, tpl; never the query). Report from your edge or middleware, after the response is sent. 503 hits_unavailable means drop the batch
POST /v1/evaluateskReport an outcome from your server and get a fresh verdict and token. See Evaluate
GET /v1/visitor-config?k=none or pk{ vid }: on, consent or off, whether this browser may keep a visitor id. The SDK calls it once per session
GET /v1/signatures/:filenoneThe engine's signature bundle, <version>.json. latest.json redirects to the current one

Collection telemetry: POST /v1/collect answers with header DA-Telemetry-Accepted: 0 when the event could not be queued for storage (detection still succeeded). POST /v1/check always includes telemetryAccepted: true|false in its JSON. Both only say whether storing the session was queued; they never affect the verdict.

Website data

These routes answer 403 site_not_verified until the site has a verified domain (Claim a domain). Add ?data=test to read only localhost sessions, which works before the domain is verified.

RouteAuthReturns
GET /v1/stats?from&tosk, or session + ?site=st_…Totals, series, families, profiles and breakdowns per class. breakdowns.campaign: the top 200 source | medium | campaign rows (first touch; untagged visits use the referrer host or (direct), the channel and (not set)), each with source, medium, campaign, agent_verified and paid, the rest summed as (other). breakdowns.click_id: visits per ad-click id. coverage_from (unix seconds) when part of the range predates campaign rows. sources=0 leaves out breakdowns.campaign, breakdowns.click_id and coverage_from, and answers faster on long ranges
GET /v1/stats/campaign?from&to&source&medium&campaignsk, or session + ?site=One campaign from sessions (range ≤ 90 days, quarantined traffic excluded): totals and breakdowns by utm_term, utm_content, entry_path, ref_host, country, click_ids, agent_id and operator, each split by class. source, medium and campaign are a campaign row's parts: medium and campaign 1–100 characters, source up to 255 (a referrer host). Source and medium match in any case; the response gives the parts as stored
GET /v1/stats/friction?from&to&classsk, or session + ?site=How visitors struggle, from sessions that carry interaction measures (SDK 0.7.0 and later, range ≤ 90 days, quarantined traffic excluded): totals, page_types and the 50 busiest paths, each with sessions, rates per state (for example frustrated, confused), cue_rate, means and conversion, plus the definitions. class: human (default), bot, agent or all
GET /v1/sessions?limit&class&cursor&quarantined&since&untilsk, or session + ?site={ sessions, nextCursor }. since and until are unix seconds: sessions updated at or after since and created before until, so together they list the sessions active in a range
GET /v1/sessions/:sidsk, or session + ?site=One session, as on the portal's session page (The portal)
GET /v1/sessions/:sid/searchessk, or session + ?site=Likely searches for an organic Google visit, from Search Console
GET /v1/sessions/:sid/rawsk, or session + ?site= (Owner or Admin){ sid, retentionDays: 30, raw }: up to 20 raw rows for one session, with the raw IP, full user agent, request headers and Cloudflare facts. Kept 30 days. Every read is written to the site's audit log. Other roles get 403 forbidden
GET /v1/visitors?from&to&limit&cursorsk, or session + ?site={ visitors, total, nextCursor }: visitors with a session in the window (default the last 30 days, at most 90), newest first, limit ≤ 200
GET /v1/visitors/:visitorKey/journeysk, or session + ?site={ sessions, truncated }: one visitor's retained sessions in order, each with its channel, campaign tags and pages
GET /v1/hits/stats?from&tosk, or session + ?site=Edge hits (requests your edge adapter reported): total, classes (bot, agent, browser), agents (catalog id, class, operator, behaviour, hits and identity counts: signed_claim, ip, declared, spoofed, none), statuses, top non-browser paths, robots (robots.txt readers), unknown user-agent tokens and a series. Agents and unknown tokens carry probe: their hits with a probing intent. The portal shows them under Crawlers
GET /v1/threats?from&tosk, or session + ?site=Hostile activity, probing, scraping, credential attacks and floods (range ≤ 90 days, Intent): summary (requests, sessions, actors, last_seen_at, risk), series, paths (with evidence), networks, countries, matrix (class × intent), actors (edge clients by network and user agent, with max_not_found, max_requests and max_pages in one 10-minute window and their intent), recent hostile sessions, and edge.reporting (false: no edge adapter reports, so scanners that never run JavaScript are not seen)
GET /v1/threats/labels?accountsession{ labels }: each of your sites with hostile activity in the last 7 days (site, label: "probing_seen", last_seen_at, requests, sessions, high)
GET /v1/live?window=60sk, or session + ?site=Live now. last_ping is the newest ping in the past hour (unix seconds), or null: pk_live_ keys ping only with data-live
GET /v1/sessions/:sid/events?limit&cursorsk, or session + ?site={ events, nextCursor }: every recorded revision of one session (collect, check, judge, join), limit ≤ 200
POST /v1/sessions/:sid/feedbacksession + ?site= (any member; a secret key gets 403 session_required){ feedback: { mine, latest, count } }. Body { label, source }: label is human, bot or agent; source is dossier, sessions_table or live_table. Appends a label with a server-side snapshot of the verdict. Labels never change a verdict. 404 session_not_found while the session isn't stored yet; 409 feedback_limit once it has 500 labels
GET /v1/sessions/:sid/feedbacksession + ?site={ entries, feedback }: every label, oldest first: label, at (unix seconds), user_id, mine (yours), role, source. No snapshots or emails
GET /v1/events/export?limit&cursorsk, or session + ?site={ events, nextCursor }: all events for the site, oldest first, limit ≤ 200 (default 100)

Every session is stored, humans included, and appears in these routes within about 30 seconds. /v1/stats accepts ranges up to 365 days. After a domain claim merges keyless history, data routes answer 503 claim_pending (retry shortly) until it is consolidated.

Filter sessions by campaign with ?utm_source=, ?utm_medium=, ?utm_campaign=, ?utm_term=, ?utm_content=, ?click_id= (for example gclid) or ?source_key= (a campaign row's key, URL-encoded, up to 512 characters; other filters take up to 256). utm_source, utm_medium and the source and medium of a source_key match in any case.

Filter by how the visit arrived with ?channel= (values), and by intent with ?intent= (any intent code, for example task or scrape), ?intent_risk= (low, medium, high) or ?intent_evidence= (an evidence code).

Each GET /v1/sessions row carries intent (or null for sessions stored before it) and feedback: { mine, latest, count }: the caller's own label (null for a secret key), the latest label anyone gave, and how many there are. It is null when labels can't be read; the list still loads.

Channels

visitor.channel is Google Analytics 4's default channel group, worked out from the first touch: paid_search, paid_social, paid_video, paid_shopping, paid_other, display, cross_network, search (Organic Search), social (Organic Social), organic_video, organic_shopping, ai_assistant, email, referral, affiliates, audio, sms, mobile_push, direct, internal (from your own site) and unassigned. Sessions stored before 2026-10-07 may carry paid, every paid visit before the split.

Session detail

GET /v1/sessions/:sid also carries the landing title, what the visitor searched on your site and basic ecommerce:

Field
visitor.entry_titleThe landing page's title (SDK 0.9.0 and later)
journey.site_searchesTerms searched on your site, in order, at most 10 (Site search)
journey.ecommerceproduct_views, item_ids, cart_adds, checkouts, purchases, purchase_value, purchase_currency, or null (Ecommerce)

Pages in journey.pages carry search, title (first page of a page load), item (the product: item_id, item_name, price, currency), qf (query pattern codes, SDK 0.13.0) and forms (form submits by form role, SDK 0.14.0) when known.

Visit attribution

Every session, event and export row carries four roles, each with its own evidence:

Field
visitor.agent_id, visitor.agent_evidenceWho acts: a catalog id (anthropic.claudebot) or a declared ERC-8004 name. Never an automation tool.
visitor.operator, visitor.operator_evidenceWho runs it (anthropic). A shared published IP list proves the operator, not a product.
visitor.controller_id, visitor.controller_evidenceWhat drove the visit (playwright.automation, python.requests).
visitor.browser, browser_version, osThe client the visit presented.
visitor.attribution_conflicttrue when the declared agent's operator disagrees with the verified operator; the agent is then left unnamed.
visitor.attribution_version2. Absent on older rows.

Evidence is signed, authenticated, ip, rdns, declared, marker, detected or spoofed. visitor.identity keeps its meaning: the strongest proof for the whole visitor. Filter sessions with ?agent_id=, ?operator= or ?controller_id=; /v1/stats breaks traffic down by each.

Edge hits (/v1/hits) resolve the same roles. A shared published IP list proves only the operator, so a Chrome hit from one has identity: ip and the list's operator but no agent_id, and a declared UA from one (ClaudeBot from an Anthropic address) has detected_by: catalog, not ip_list.

Declared and signed visit identity

A visit can declare an ERC-8004 identity in its User-Agent, for example acme.shopping-assistant/1.0, with an optional registry reference in a comment: (erc8004=eip155:1:0x8004a169fb4a3325136eb29fa0ceb6d2e539a432:22). Double Agent keeps it in visitor.declared_agent. A declaration alone proves nothing.

/v1/collect and /v1/check also check an ERC-8128 request signature (a draft standard) when one is sent. It must be single-use and bound to the URI, method, body, content type and User-Agent or Origin, valid for at most 300 seconds with 30 seconds of clock skew. Plain wallets (EOAs) and deployed ERC-1271 accounts are supported.

The result is in visitor.request_auth: status (verified, invalid or unavailable), the wallet, chain, check time and an optional registry binding (owner, agent-wallet, mismatch, unavailable or not-requested) with the block. /v1/check returns it as requestAuth. null means no signature was sent. A verified signature proves the beacon request, not the browsing it reports, and it never changes the verdict. Edge hits keep declarations only. How an agent signs its requests: Verified agents.

Agents on your site

The agents that visited one site. Auth: sk, or session + ?site=. from and to cover up to 365 days.

RouteReturns
GET /v1/agents?from&to&cursorThe agents seen in the range, with sessions, classes and behaviours, their registry listing and directory link, and configured: ERC-8004 identities set on the site that have no visits yet
GET /v1/agents/profile?agent_ref|agent_name|unknown=1&from&to&cursorOne agent's visits, aliases, registry listing and directory entry. 404 not_found when the site never saw it
GET /v1/agents/reviews?agent_ref&reviewer&tag&status&cursorERC-8004 reviews of an agent this site has seen. status: active or revoked

Caller identity

Who acted, for whom, proven how: Callers.

Evaluate

POST /v1/evaluate (sk) reports what happened on your server: { sid, subject, observationId, action, outcome, request }, plus optional caller fields. outcome is ok, auth_failed, action_denied or confirmed_trap, and request.url must be on one of the site's verified domains. The body may be up to 16 KB (a PACT proof). Example: Report what happened.

Field
actor_kindself (the customer acted) or agent (an agent acted for the customer)
principal_hash64 lowercase hex: your own hash of the customer. Without it, Double Agent hashes subject with your account's key
scope_requiredThe scopes the action needs, at most 32 OAuth scope tokens
delegationThe grant: protocol (oauth, pact, a2a, ap2), issuer, actor, client_id, scopes, scopes_used, access (read, write), expires_at (ISO 8601), grant_id (raw, up to 256 characters, hashed with your account's key) or grant_id_hash, verification ({ status: verified|failed, by: "reporter", reason? }), proof (pact only: { kind: pact-receipt|pact-delegation|pact-agent, jws }, verified and dropped)

A bad caller field is 400 invalid_caller, a bad delegation 400 invalid_delegation; protocol: "pap" is refused until the Personal Agent Protocol is published. The response adds caller (always):

"caller": { "level": "delegated", "verified_by": "reporter", "failures": [], "principal_hash": "3f…",
            "actor_kind": "agent", "in_grant": "no",
            "grant": { "protocol": "oauth", "grant_id_hash": "9a…", "scopes": ["orders:read"], "access": "read",
                       "expires_at": null, "status": "unverified" } }

The signed token adds lvl (the level) when you sent a caller field, and grant ({ id, protocol, access }, id being the grant id hash) when you sent a delegation. Tokens for requests without caller fields are unchanged. An action outside the grant (in_grant: "no") sends the site's caller.scope_violation webhook.

Agent events

POST /v1/agent-events (the agent's ingest key, see Record your public agent) accepts the caller fields of the recorder: counterparty.authenticated client_id, actor and scopes; counterparty.delegation; counterparty.signature with verified_by: "double_agent" and the forwarded request; access and scope_required on operation.started; insufficient_scope on operation.finished; mandate_ref on transaction.recorded. Shapes and limits: the observe README. A pap delegation, or a proof on any protocol but pact, rejects the event (invalid_counterparty).

Stored operations carry caller_level, caller_verified_by, caller_failures, principal_hash, principal_issuer, client_id, actor_id, grant_id_hash, grant_protocol, grant_scopes, grant_status, access, scope_required, required_scopes (from insufficient_scope), in_grant, scope_violation and intent_evidence.

Sessions and website Callers

Each GET /v1/sessions row and session detail carries caller (null before caller identity): level, verified_by, failures ({ code, words }), principal_hashes, actor_kind, grant (protocol, grant_ids, scopes, access, status) and in_grant. Filter sessions with ?verification_level=, ?principal=, ?grant= (64-hex hashes), ?actor_kind= or ?in_grant=.

GET /v1/callers?from&to (sk, or session + ?site=; range ≤ 90 days): { site, from, to, items, counts, truncated }, one row per actor × principal hash (≤ 500) with actor, level, verified_by, failures, actor_kind, grant, sessions, in_grant_no, intent, first_seen and last_seen (unix seconds). Filters: level, principal, grant, actor_kind, class, intent. An unknown parameter is 400.

Audit

RouteAuthReturns
GET /v1/audit/search?account&principal|grant&from&to&surface&limit&cursorsession, audit.read{ items, next }: agent operations and website /v1/evaluate events for one principal or grant hash (exactly one), newest first. surface: all (default: what you may read), agent (needs agents.read) or site (needs analytics.read). from, to: ISO dates or unix times, at most 90 days (the default). limit ≤ 200 (default 50). 60 a minute
GET /v1/audit/export?…&formatsession, audit.readThe same rows as an attachment, format ndjson (default) or csv, at most 50,000 rows. Audited as audit_export. 10 a minute
GET /v1/accounts/:accountsession (any member){ account } with audit_retention_days (null: the default) and audit_retention_caps ({ agent: 30, site: 90 })
PATCH /v1/accounts/:accountsession, account.updateAlso takes audit_retention_days: 1–90 or null. Each store keeps the smaller of it and its cap

Agent rows: surface: "agent", at, agent_id, operation_id, counterparty_id, kind, method, target, outcome. Website rows: surface: "site", at, site, sid, event_id, actor, action, recommendation, policy_decision. Both: principal_hash, grant_id_hash, grant_protocol, grant_status, access, in_grant, scope_violation, caller_level, caller_verified_by.

Intent

GET /v1/sessions/:sid carries intent: the session's intent over all its pages, { code, confidence, risk, evidence, task_kind, purpose, source, version }, plus paths (up to 5 probe paths, for probe only) and path_count. null for sessions stored before intent. Intent is a tag: it never changes the verdict.

code is one of browse, research, read, task, support, crawl, ai_fetch, monitor, probe, scrape, cred_attack, flood or unknown. task_kind (signup, login, contact, book, purchase, download) is set for task and purpose (search, seo, training, feed, preview) for crawl; both are null otherwise. risk is none for benign intents.

GET /v1/intents?from&to (sk, or session + ?site=; range ≤ 90 days): the site's sessions in the range by class and intent, { total, rows: [{ class, intent, sessions }], kinds: [{ intent, kind, sessions }] } (kinds: task kinds and crawl purposes). Sessions stored before intent count as unknown; edge requests are in /v1/threats and /v1/hits/stats.

Agent analytics

Session auth. Every route is under /v1/managed-agents/:id, where :id is one of your account's agents (another account's agent is 404). The caller needs agents.read, except where the table says otherwise.

Shared query parameters:

  • env: live or test.
  • The range is either range (24h, 7d, 30d or 90d) or from and to (ISO-8601 instants), never both (400 invalid_range).
  • The comparison is compare (prev, the default; week, month, off or custom) and compare_from (YYYY-MM-DD, with compare=custom only).

Lists page with the opaque cursor from the previous page's next. A cursor is bound to the query that made it; reused with a different query it is 400 invalid_cursor. An unknown query parameter is 400.

RouteQueryReturns
GET /summarysharedKPIs with change against the comparison, the chart, and coverage (complete is false while a day is still being rebuilt or events were dropped in the last 24 h)
GET /panelssharedCounterparty, target, task and error panels for the Overview; fetch in parallel with summary
GET /filtersenv, rangeValues the screens offer as filters
GET /operationsenv, range (optional; default is everything retained), direction, kind, outcome (an outcome or errors), protocol (<name>|<version>|<binding>), protocol_name, target, conversation, cp, task (any, open, stalled, conflict or a task state), sort (newest, slowest, fastest), limit ≤ 100 (default 25), cursor; caller filters principal, grant (64-hex hashes), level, access (read, write, destructive), in_grant (yes, no, unknown), scope_violation=true{ items, next, counterparties, retained_from }. Each row (and each conversation timeline row) carries the caller fields. More than 10,000 tasks matching task is 422 refine_filter
GET /conversationsenv, range, show (all, errors, open), q, limit ≤ 100 (default 20), cursorConversations by last activity, with counters. At most the 1,000 most recently active are ranked; truncated is set beyond that
GET /conversations/:conversationcursor (timeline)Header, a page of the timeline, its tasks with transitions, the linked conversation in the other direction, and whether content is captured
GET /conversations/:conversation/content–Captured message content (agents.content.read; every read is audited)
GET /operations/:operation/content–{ messages } for one operation (agents.content.read; every read is audited)
GET /counterpartiesenv, range, kind (agent, script, bot, human, unknown or all), q, sort (operations, errors, last_seen), order (asc, desc), limit ≤ 100 (default 25), cursorRows with range counts, and counts per kind
GET /counterparties/:cpenv, rangeIdentifiers, activity for the range, and recent conversations; verification (level, verified_by, failures), principals (the 10 busiest, with their worst grant status), access_split and registry (the caller's directory listing: directory_id, name, url, status, signed_card; null when it has none). A merged counterparty answers 308 to its canonical one, with { canonical_id }
GET /callersenv (required), range (default 7 days, at most 90), level, kind, principal, grant, access, in_grant, intent, q, sort (operations, errors, last_seen, risk), order, limit ≤ 100 (default 25), cursorCallers: { items, next, counts, truncated, retained_from }, one row per counterparty × principal hash with level, verified_by, failures, client_ids, actor_ids, grant, access_split, scope_violations, intent and registry. At most 500 rows (truncated); counts per level
GET /threatsenv, range (at most 90 days)Callers with a hostile intent: { summary, items, targets, truncated, retained_from }
POST /counterparties/:cp/labelsbody { label }: agent, human, script or bot201. Any member can label, and the label applies account-wide, on the canonical counterparty
GET /tasksshared, status (a derived status, open or all), sort (last_update, age), limit ≤ 100 (default 20), cursorStatus chips, which equal the Overview's task outcomes, and rows. Without status the list shows open tasks, or all tasks when none is open
GET /tasks/:task–One task with transitions; source health only for a stalled task
GET /transactionsenv, range, currency, method, status, basis, cp, sort (time, amount), limit ≤ 100 (default 25), cursorTransactions (agents.revenue.read)
GET /transactions/:transaction–One transaction with its version history (agents.revenue.read)

Closed days are read from daily rollups; yesterday and today are read from raw data. Test operations are listed but never counted.

PATCH /v1/managed-agents/:id (agents.manage) takes webhook_url (https) and webhook_secret (16–200 characters) for the agent's caller.scope_violation webhook; null clears either. The agent view shows webhook: { configured, url }, never the secret.

To add an agent, its sources and ingest keys, see Record your public agent.

Agent revenue

Owners and Admins hold agents.revenue.read; other roles need it granted. Without it, the routes below are 403, and the analytics routes above leave money out: no money field, and the Overview panel says revenue: { locked: true }.

Amounts are minor units of the account currency (GET /v1/me), with up to 4 decimals. Every response with money names its currency and currency_exponent. Revenue is charges plus refunds (refunds are negative), never failed ones; cost is recorded costs plus fees; margin is revenue minus cost. Money that no exchange rate covers is counted in unconverted and never summed. Money converted into a former account currency is re-converted at its own day's rate and counted in other_currency.

basis picks which money counts: settled, reported (reported and settled) or all (also estimated, the default). Transactions and costs are kept 13 months.

RouteQueryReturns
GET /revenue/summaryshared, basisKPIs (revenue, cost, margin, settled_share, paying_counterparties, revenue_per_task) with change against the comparison, the chart, and splits by cost category, currency and payment method
GET /revenue/breakdownshared, by (tasks, the default; conversations, counterparties or tools), basis, limit ≤ 100 (default 25), cursoritems ranked by revenue, each with revenue, cost and margin. by=tasks adds without_task (money linked to no task); by=counterparties adds each party's comparison revenue and change. truncated is set past 5,000 keys
GET /transactionsenv, range (default 7 days), currency (the original currency), method, status, basis, cp, task (none: only transactions linked to no task, the breakdown's without_task), sort (time, the default, or amount), limit ≤ 100 (default 25), cursor{ items, next, counterparties, retained_from, currency, currency_exponent }. Failed transactions are listed with their status. sort=amount ranks by the amount in the account currency; amounts in another currency, or with no rate, come after
GET /transactions/:transaction–One transaction with its versions, oldest first

With agents.revenue.read, these routes also carry money:

RouteAdds
GET /operationsmoney on each row (null when none was recorded), and currency, currency_exponent on the page
GET /conversations/:conversationmoney: per operation (<source_id>:<operation_id>), its charges without a task and its costs; per task, its money; without_operation for money linked to no operation; truncated past 500 transactions or costs
GET /counterparties/:cpmoney: revenue, cost and margin on every basis for the range, the counterparty's merged IDs included
GET /tasksmoney on each row, and sort=revenue (without the permission, 403 forbidden)
GET /tasks/:taskmoney on the task, its transactions and costs (a cost's amount is null when no rate covers it), and money_truncated past 500 of either

A task's or operation's money adds every basis: { currency, currency_exponent, in_account_currency, revenue, cost, transactions, settlement, unconverted, other_currency }. in_account_currency is false for money converted before the account currency changed. Its transactions count stops at 64 (16 for an operation), so it can be lower than the task's transactions list.

Agent directory

Public: no key, CORS *. Each IP can make 60 requests a minute across these routes. Tighter limits apply to check (10 a minute), submissions (10 a minute) and reviews (30 a minute).

RouteAuthReturns
GET /v1/directory/agents?q&skill&registry&card&status&trait&category&service&sort&facets&limit&cursornone{ agents, total, next_cursor, facets? }. card: with (agents with their own Agent Card) or without. status: live, stale, unverified, gone, comma-separated, or active (default) or all. trait (repeatable, all must match): streaming, push_notifications, extended_card, signed, auth:none, auth:<kind>, input:<kind>, output:<kind>, a2a:<version>, transport:<binding>. category: coding, search, data, commerce, finance, travel, content, productivity, support, security, devops, health, education or other. service: the provider. registry: catalog, a2a or erc8004. sort: name (default), reputation, newest or updated. facets=1 adds counts over the whole search. limit ≤ 50
GET /v1/directory/agents/:idnoneOne agent: registrations (with link: self, bidirectional or one_way), skills, endpoints, traits, categories, service and, where available, reputation (score is null when unknown)
GET /v1/directory/agents/:id/versions?limit&cursornone{ versions, next_cursor, complete_since }: how the agent's own Agent Cards changed, newest first. Each version has version_id, observed_at, agent_version and changed_paths. limit 1–50 (default 20)
GET /v1/directory/agents/:id/versions/:versionIdnoneOne version: the summary plus the normalized profile, provenance, warnings and document (sha256, byte_count, media_type). Never the raw card
GET /v1/directory/agents/:id/reviews?limit&cursornone{ reviews, next_cursor, status }: the agent's ERC-8004 reviews, each with registration, reviewer, reviewer_agent_id, weight, value, value_decimals, tag1, tag2, revoked and feedback_index. status is unavailable when the chain could not be read. limit 1–50 (default 20)
GET /v1/directory/check?urlnoneA live diagnosis of an Agent Card URL or https origin: { card, domain_file, tokens, listing, problems }. Each problem has a code and a message that says how to fix it. 10 per minute per IP
GET /v1/directory/statsnone{ total, agents, bots, self_identified, live, skills, new_this_week }
POST /v1/directory/submissionsnone{url} (an Agent Card URL or https origin) → 202 { queued, url }
GET /v1/directory/submissions?urlnone{ url, state: "pending" }, or { url, state: "listed", agent } once the card has been read

Every agent with its own card, for agents: GET /v1/directory/agents?card=with&status=live&limit=50, paged with cursor.

The same data, for people: /agents/.

Accounts and auth

RouteAuthPurpose
POST /v1/auth/loginnone{email} → 204 (never says whether the email exists)
POST /v1/auth/verifynone{token} → { session, user, accounts }
POST /v1/auth/logoutsession{all?} → 204
GET /v1/mesessionUser, accounts (each with its report currency and currency_exponent), sites, and viewer_country (where the request came from, for the dashboard map)
POST /v1/auth/device, /device/token, /device/approvenone / sessionCLI device flow
POST /v1/accountsnone + DA-PoWCreate account + site + keys (Accounts)

Sites, domains, keys

Session auth. Viewers can only read.

RoutePurpose
GET/POST /v1/sitesList; create {name, domain?, profile?}
GET/PATCH/DELETE /v1/sites/:idSettings; webhook and integration secrets are write-only
POST /v1/sites/:id/domains{hostname} → token and methods
POST /v1/sites/:id/domains/:host/verify{method} → { verified, detail, claimed }
DELETE /v1/sites/:id/domains/:hostRemove a domain
GET/POST /v1/sites/:id/keysList (prefixes only); create {kind, env}, sk shown once
POST /v1/sites/:id/keys/:keyId/rotateNew key; the old one lasts 24 h
DELETE /v1/sites/:id/keys/:keyIdRevoke now
DELETE /v1/sites/:id/preclaimDiscard pre-verification data
GET /v1/sites/:id/auditAudit log
GET /v1/sites/:id/agent-identitiesList ERC-8004 associations; optional agent_name filter
POST /v1/sites/:id/agent-identitiesResolve and save/refresh a registry association (owner/admin)
DELETE /v1/sites/:id/agent-identities/:linkRemove that site's association (owner/admin)

Adding or removing an ERC-8004 identity needs the Owner or Admin role. POST takes agent_name (for example acme.shopping-assistant) and token_id (a decimal string), with optional chain_id, registry_address, reviewers (up to five addresses), reputation_registry, tag1 and tag2. Ethereum mainnet and its official registry are the defaults, and Double Agent picks the RPC endpoint. A new association answers 201, a refresh 200, each with id, snapshot (block, owner, wallet and registration URI), association: "site-configured", visit_binding: "unverified" and updated_at. GET returns { identities } and DELETE answers 204. A failed chain read is 503. Saving an association never authenticates a visitor or changes detection. The same from the command line: CLI.