The decision packet: BehalfID's verification primitive
Every verify call produces a decision packet — a structured answer to one question: is this specific action allowed right now? Here's what happens between the request and the response.
Every verify call in BehalfID produces a decision packet. It's a structured answer to one question: is this specific action allowed right now, for this agent, against this resource?
The packet is not a policy engine output or a capability token. It's a single decision record — immutable once produced, logged on both sides, and the one artifact your enforcement code should branch on.
The action request
The request side of the packet is what your integration sends to POST /api/verify or through the SDK:
const decision = await behalf.verify({
agentId: "agent_ollie",
action: "purchase",
vendor: "coachella.com",
amount: 742
});agentId identifies the agent making the request. action is the operation being attempted. vendor and amountare optional but recorded in the audit log when provided. The request is authenticated by the agent's API key, so the first thing BehalfID does is verify the caller.
What BehalfID checks
Given an authenticated request, BehalfID evaluates the agent's active passport against the requested action in order:
- Agent state. If the agent is disabled, all verify calls return
deniedimmediately. - Passport lookup. BehalfID queries active, non-expired permissions scoped to this agent.
- Scope match. The requested action is compared against allowed actions in each matching scope. If no scope covers the action, the decision is
denied. - Block list check.If the action appears in a scope's blocked actions, the decision is
deniedeven if the action is otherwise in the allowed set. - Requires-approval flag. If the matching scope is marked
requiresApproval, the decision isrequires_approval. The action should pause for human review. - Expiration. Permissions expire by date. Expired permissions are not matched.
- Revocation. Revoked permissions are excluded from the lookup entirely.
The decision record
The verify response carries everything your enforcement code needs:
{
allowed: false,
decision: "denied",
reason: "No active purchase permission",
requestId: "req_01hvz8…",
agentId: "agent_ollie",
riskLevel: "medium"
}allowed is the boolean your enforcement code gates on. decision is the semantic outcome — allowed, denied, or requires_approval. reason is the human-readable explanation. requestId links the response to its audit log entry.
requestIdis stable and unique per verify call. Store it alongside your own request identifiers so you can correlate your logs with BehalfID's audit trail.Audit events
Every authenticated verify call is written to the audit log before the response is returned. The entry captures the decision, reason, risk level, action, vendor, and amount alongside the agent and request identifiers.
Log entries are scoped: agent API keys read only their own history. Dashboard users see only logs belonging to their own agents. Manual passport previews do not produce log entries — only real POST /api/verify calls do.
Webhook subscribers receive a verification.allowed, verification.denied, or verification.requires_approval event for each decision, delivered through an outbox-backed retry system with dead-letter replay available from the console.
What to do with the decision
The enforcement pattern is identical regardless of what the decision contains: branch on decision.allowed before your executor runs.
const decision = await behalf.verify({ agentId, action, vendor, amount });
if (!decision.allowed) {
throw new Error(`BehalfID blocked: ${decision.reason}`);
}
// Executor only reaches this point when decision.allowed === true.
await executor.run(action, { vendor, amount });If BehalfID is unreachable, your code should treat the outcome as denied and not proceed. Availability gaps should not become permission grants.