verify-access response contract
Authoritative shape of the POST /api/agents/verify-access response — verified-agent and anonymous branches.
Authoritative shape of the POST /api/agents/verify-access response. Two branches:
verified-agent (request body included an agentId matching a registered agent) and
anonymous (no agentId — endpoint policy resolves the call). Both branches return 200 OK on
success and emit a verification.* event for the activity feed and blockchain audit trail.
Why two linkage keys (sessionId vs correlationId)? They are not two implementations of the
same concept. sessionId identifies a verified agent's tracked session and threads through PDLSS,
runtime challenge, and token guidance. correlationId pairs anonymous events for activity-feed
linkage where no agent identity exists. Both serve the same role on their branch — the linkage key
for downstream paired events — but they represent two genuinely different identity states by
design.
Common fields (both branches)
{
"success": true,
"access": {
"allowed": boolean,
"accessLevel": "none" | "restricted" | "read-only" | "standard" | "full",
"reason": string
},
"recommendation": "grant" | "deny" | "step_up_required" | "audit",
"recommendationReasons": string[],
"advisory"?: { // present on anonymous responses
"ial": "unverified",
"registrationUrl": string,
"docsUrl": string,
"policy": "deny" | "audit" | "allow_partial" | "allow_full",
"restrictionsExplained": string[]
},
"warningHeader"?: { // present when policy = 'audit'
"name": "X-Astra-Unverified-Warning",
"value": string
}
}access.accessLevel is the band the SDK enforces routes against. v2.3.9 renamed guidance →
restricted to remove the value-name collision with the guidance: {} response object that never
existed (denial branches return none).
recommendation is the four-bucket activity-feed bucket the event is counted under. audit
(v2.3.8+) is a soft-launch grant that allows the call but stamps an X-Astra-Unverified-Warning
header on the merchant's response — distinct from grant (verified) and deny.
Verified-agent branch — request body included agentId
{
"success": true,
"access": { "allowed": true, "accessLevel": "standard", "reason": "..." },
"recommendation": "grant",
"recommendationReasons": ["..."],
"agent": {
"astraId": "ASTRA-...",
"name": string,
"trustScore": number,
"agentStatus": "active",
"blockchainStatus": "anchored",
"runtimeChallengeSupported": boolean
},
"sessionId": "uuid", // ← key field (see below)
"tokenGuidance"?: { ... }, // agent owner only (see below)
"runtimeChallenge"?: { ... } // present when challenge was issued
}Who sees the agent's limits. Spend limits and the policy behind a decision belong to the agent's
owner. When you verify someone else's agent, which is the normal case for a merchant, you get the
decision (recommendation, access.allowed, access.failures) but not access.restrictions,
access.appliedPolicy or tokenGuidance. Gate on recommendation; never on a limit value.
Your key, your endpoints. An API key can only verify requests made to an endpoint owned by its
account, or by an organisation its user is an active member of. Naming any other endpoint
(counterpartyId or counterpartyUrl) returns 403 with error: "COUNTERPARTY_KEY_MISMATCH". SDK
5.13.0+ checks this at startup when counterpartyId is configured, so a wrong key shows up when you
deploy, not on the first agent request.
Denials outside the agent's mandate. When a request is outside the agent's own permissions, the
failure names the dimension (pdlss.action, pdlss.scope, …) in plain words but never lists what
the agent is allowed. The request counts against the agent and is not retryable.
Owner approval ("Ask me") is not a deny. The agent's owner can ask to approve certain
requests with their passkey, on any PDLSS dimension. Such a request returns
recommendation: "step_up_required", a stepUpApproval object and one failures[] entry per
dimension that asked:
failures[].dimension | The owner asked to approve… |
|---|---|
pdlss.purpose.approval_required | this action |
pdlss.duration.approval_required | a request outside the agent's allowed days or hours |
pdlss.scope.approval_required | this resource, jurisdiction or counterparty |
pdlss.selfInstantiation.approval_required | creating a sub-agent |
commerce.intent.approval_required | a spend between the agent's Autonomous Limit and Hard Limit |
{
"recommendation": "step_up_required",
"access": { "allowed": true, "failures": [
{ "dimension": "pdlss.purpose.approval_required", "message": "…", "guidance": "…" }
] },
"stepUpApproval": {
"approvalId": "…",
"pollUrl": "/api/step-up-approvals/poll/…",
"approvalUrl": "https://astrasync.ai/activity/pending/…",
"expiresAt": "…",
"status": "pending"
}
}step_up_required with failures[] is not a denial: hold the request and poll
stepUpApproval.pollUrl. Once the owner approves, send the same request again (same attempt id) and
it passes — once. The owner has 30 minutes to decide by default. Failures name the dimension only,
never the rule behind it. A deny on any dimension wins over an approval, and an approval for one
rule never covers another.
Errors before a decision.
| Status | code / error | Meaning |
|---|---|---|
401 | agent_identity_not_a_credential | The request offered only an agent id (X-Astra-Id). An agent id identifies an agent; it never authenticates. Use your API key. |
401 | — | No valid credential, or a token that isn't a full session's access token (sign-in interim, refresh or passkey elevation tokens are refused). |
403 | COUNTERPARTY_KEY_MISMATCH | Your key doesn't own the endpoint you named (see above). |
429 | Rate limit exceeded | Over your key's request budget. Honour Retry-After; see SDK Authentication. |
sessionId is the verified-traffic correlation key — it ties this verification event to any
follow-up activity for the same attempt, and is used to record the counterparty's decision via
POST /api/agents/verify-access/{sessionId}/decision.
Anonymous branch — no agentId in request body
Triggered when the calling agent is not registered (or the SDK chose not to send an ASTRA-id). The
endpoint's unverifiedAgentPolicy resolves the response. All four policy values produce the same
wire shape but populate access, recommendation, and advisory.policy differently.
{
"success": true,
"access": { "allowed": boolean, "accessLevel": "...", "reason": "..." },
"correlationId": "uuid", // ← key field, anonymous-only (see below)
"advisory": {
"ial": "unverified",
"registrationUrl": "https://astrasync.ai/register",
"docsUrl": "https://astrasync.ai/docs/agent-access",
"policy": "deny" | "audit" | "allow_partial" | "allow_full",
"restrictionsExplained": ["..."]
},
"warningHeader"?: { ... }, // only when policy = 'audit'
"agent"?: { ... }, // only when caller fingerprint matched a known platform
"recommendation": "grant" | "deny" | "step_up_required" | "audit",
"recommendationReasons": ["..."]
}correlationId is the anonymous-traffic linkage key. The backend mints a fresh
correlationId per anonymous request, persists it on the emitted event, and surfaces it on the
response. SDK reads it onto EnhancedVerificationResult.correlationId. correlationId plays the
same event-pairing role on the anonymous branch that sessionId plays on the verified branch —
they are mutually exclusive by design, not transitional.
Activity-feed event types you'll see
| eventType | When it fires | Carries |
|---|---|---|
verification.completed | Verified-agent grant. | sessionId, recommendation: grant, trustScoreAtRequest |
verification.unverified_audit | Anonymous call, endpoint policy = audit. | correlationId, recommendation: audit, decision: audit |
verification.unverified_allow_partial / verification.unverified_allow_full / verification.unverified_deny | Anonymous call, matching the endpoint's unverifiedAgentPolicy. | correlationId, recommendation matching policy |
verification.agent_not_found | Request body included agentId that doesn't exist. | errorMessage, no sessionId / correlationId |
counterparty.discovery_blocked | Auto-register URL validation rejected a placeholder/non-FQDN URL (v2.3.10). | reason: url_hostname_*, counterpartyUrl |
Click any row in /activity > Recent Activity to expand the full eventData payload — useful
for inspecting the correlationId/sessionId that links events for one attempt.
Contract stability
The shape above is the v2.3.10+ contract. Older SDK releases may see narrower field sets. Specifically:
correlationIdon anonymous responses — added v2.3.10 (defect #34 fix). Mutually exclusive withsessionIdby design (anonymous vs verified branch); not a transitional field.recommendation: 'audit'— added v2.3.8 (defect #26).access.accessLevel: 'restricted'(replaces'guidance') — added v2.3.9 (defect #30).warningHeaderon audit responses — added v2.3.8.
Field additions are non-breaking — older SDKs ignore unknown keys. Removing or renaming a documented field is a breaking change and ships in a major bump.

