Five-minute quickstart
- Create a workspace at app.agentledgersecurity.com.
- Register an agent, such as
refund-copilot, and add its allowed actions, such asissue_refund. - Generate a scoped API key. Store it as
AGENTLEDGER_API_KEYon your server. - Create or confirm a policy. The default safe policy requires approval for refunds over $250.
- Evaluate immediately before your executor runs.
const response = await fetch("https://app.agentledgersecurity.com/api/evaluate", {
method: "POST",
headers: {
"content-type": "application/json",
"x-agentledger-key": process.env.AGENTLEDGER_API_KEY
},
body: JSON.stringify({
agent: "refund-copilot",
action: "issue_refund",
amount: 40000,
payment_intent: "pi_..."
})
});
const result = await response.json();
if (result.decision === "allow") await executeAction();
if (result.decision === "block") throw new Error(result.reason);
if (result.decision === "require_approval") {
console.log("Waiting for", result.approval_id);
}40000 represents $400.00. Evaluate as close as possible to execution and never treat require_approval as permission to continue.
Evaluation API
/api/evaluateHeaders
| Header | Value |
|---|---|
content-type | application/json |
x-agentledger-key | Your scoped AgentLedger API key |
Request body
| Field | Required | Description |
|---|---|---|
action | Yes | Action to evaluate; must be allowed for the scoped agent. |
agent | No | Agent slug. If provided, it must match the key scope. |
amount | No | Monetary amount in integer cents. |
count | No | Quantity used by count-based policies. |
payment_intent | No | Stripe PaymentIntent reference associated with a refund action. |
Response
{
"decision": "require_approval",
"reason": "Refund exceeds the default $250 autonomous limit.",
"evaluation_id": "0b6d8edb-...",
"approval_id": "3d814f38-...",
"agent": { "id": "...", "slug": "refund-copilot" },
"policy": "DEFAULT-SAFE",
"usage": { "plan": "pro", "used": 1, "limit": 50000, "remaining": 49999 },
"evaluated_at": "2026-08-29T01:02:32.805Z"
}decision is one of allow, block, or require_approval. approval_id is null unless approval is required. Usage headers x-agentledger-limit and x-agentledger-remaining are also returned.
Approval status
Poll by evaluation ID, not approval ID.
/api/evaluations/{evaluation_id}const status = await fetch(
"https://app.agentledgersecurity.com/api/evaluations/" + result.evaluation_id,
{ headers: { "x-agentledger-key": process.env.AGENTLEDGER_API_KEY } }
).then(r => r.json());
if (status.terminal && status.approval_status === "executed") {
console.log(status.stripe_refund_id);
}The terminal approval statuses are executed, denied, and execution_failed.
Webhook signature verification
Configure a public HTTPS endpoint in AgentLedger. Deliveries include x-agentledger-event, x-agentledger-event-id, x-agentledger-delivery, x-agentledger-attempt, and x-agentledger-signature. The signature format is t=timestamp,v1=hex_digest.
import crypto from "node:crypto";
function verifyAgentLedger(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map(part => part.trim().split("=", 2))
);
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
const supplied = Buffer.from(parts.v1 || "", "hex");
const wanted = Buffer.from(expected, "hex");
return supplied.length === wanted.length && crypto.timingSafeEqual(supplied, wanted);
}Verify against the exact raw UTF-8 request body before parsing JSON. Reject stale timestamps and duplicate delivery IDs. Return a 2xx response promptly; AgentLedger retries retryable failures up to three attempts.
Events
| Event | Meaning |
|---|---|
approval.executed | Approved action executed successfully. |
approval.denied | An approver denied the action. |
approval.execution_failed | Approval succeeded but the configured execution failed. |
{
"id": "webhook-event-uuid",
"type": "approval.executed",
"created_at": "2026-08-29T01:04:10.000Z",
"data": {
"approval_id": "...",
"event_id": "evaluation-id",
"decision": "approved",
"decided_by": "approver@example.com",
"stripe_refund_id": "re_...",
"stripe_status": "succeeded"
}
}OpenAI, Claude, and Gemini
AgentLedger is provider-neutral. First ask the model for a structured proposal, validate it in your application, then send the normalized action to AgentLedger. Never give a model direct executor credentials.
Shared governance helper
async function govern(proposal) {
return fetch("https://app.agentledgersecurity.com/api/evaluate", {
method: "POST",
headers: {
"content-type": "application/json",
"x-agentledger-key": process.env.AGENTLEDGER_API_KEY
},
body: JSON.stringify({ agent: "refund-copilot", ...proposal })
}).then(r => r.json());
}OpenAI
const response = await openai.responses.create({
model: "gpt-5.6-luna",
input: customerMessage,
text: { format: refundProposalFormat }
});
const decision = await govern(JSON.parse(response.output_text));Claude
const message = await anthropic.messages.create({
model: process.env.CLAUDE_MODEL,
max_tokens: 500,
tools: [refundProposalTool],
messages: [{ role: "user", content: customerMessage }]
});
const proposal = message.content.find(item => item.type === "tool_use").input;
const decision = await govern(proposal);Gemini
const response = await gemini.models.generateContent({
model: process.env.GEMINI_MODEL,
contents: customerMessage,
config: { responseMimeType: "application/json", responseSchema: refundSchema }
});
const decision = await govern(JSON.parse(response.text));SDK surfaces and model names evolve. Keep provider-specific parsing in a small adapter and keep the AgentLedger request stable.
Errors and usage limits
| Status | Meaning |
|---|---|
| 400 | Invalid request, such as a missing action. |
| 401 | Missing, invalid, or revoked AgentLedger key. |
| 403 | Disabled agent, wrong key scope, or action not permitted. |
| 404 | Evaluation not found for this key and agent. |
| 429 | Monthly evaluation allowance reached. Response code: usage_limit_reached. |