Approve A Refund Or Purchase
Let a person approve the refunds and purchases your AI agent is not allowed to make alone. Set the amounts, review each request in the Approvals inbox, and run the approved action from your own server.
Say your support agent may refund up to $20 on its own, and nothing above $50 should ever go through. Between those two numbers you want a person to look first. That is Ask For Approval.
It is an optional setting on Refund Guard and Purchase Guard. It is off until you turn it on. Traccia never moves the money itself.
How It Works
The agent asks
It tries to issue a refund or place an order, and Traccia checks the amount first.
Traccia holds it
If the amount is in the middle band, the tool does not run. A request appears in your Approvals inbox.
A person decides
Someone on your team opens the request and clicks Approve or Deny.
Your server acts
Optional. After Approve, Traccia sends the original request to your server, which runs the refund.
Approving does not resume the agent, and it does not run the refund. If you give Traccia a webhook address, it sends the approved request there so your own code can do the work. If you do not, approving only records the decision.
Before You Start
If You Set Up Policies
You need permission to edit policies. Refund Guard or Purchase Guard must be set to Block.
If You Approve Requests
You need permission to enforce policies. Teammates with read access can see the list but cannot approve or deny.
If You Build The Agent
Use Python SDK 0.1.32 or TypeScript SDK 0.1.18 or later, which include ApprovalPending. Wrap the agent with govern(), and mark the refund function as a tool. An older SDK would let the tool run.
If You Want It To Run Automatically
You need a public https endpoint on your own server. This part is optional.
Your API key comes from app.traccia.ai. Approvals work through the SDK. The Gateway does not hold these calls.
Set Up The Policy
- In the app, open Policies, click Create Policy, and choose Refund Guard or Purchase Guard.
- Type the function name, such as
issue_refund, and choose a Currency. USD is the default. - Set Deny Above, for example 50.
- Turn on Ask For Approval and set Approval Above, for example 20. It must be lower than Deny Above.
- Set the mode to Block, then click Activate Policy.
With those numbers, the agent's requests are handled like this:
| Amount | What happens |
|---|---|
| 20 or less | The tool runs. No request is created. |
| More than 20, up to 50 | The tool does not run. A request waits for a person. |
| More than 50 | The tool is denied. No request is created. |
- A plain number such as
40uses the policy currency. A different currency on the call is denied. - A missing, negative, or non-numeric amount is denied.
- Observe and Warn never hold a call. They let the tool run and only record what would have happened.
- A request that nobody decides expires after 15 minutes. Expiry never lets the tool run.
Update Your Agent
When a request is held, the SDK raises ApprovalPending. Catch it where your agent calls the tool, and return the pending result instead. Do not run the tool and do not retry. The example uses a fake refund function, so replace its body with your own.
from traccia import init, govern, observefrom traccia.governance import ApprovalPending, pending_tool_result
init( api_key="...", endpoint="https://api.traccia.ai/v2/traces", agent_id="refund-desk",)
@observe(name="issue_refund", as_type="tool")def issue_refund(order_id: str, amount: float) -> dict: return {"ok": True, "order_id": order_id, "amount": amount}
@govern(fail_open=False, name="refund_desk")def handle_ticket(order_id: str, amount: float) -> dict: try: return issue_refund(order_id, amount) except ApprovalPending as exc: return pending_tool_result(exc)
print(handle_ticket("ORD-1042", 40))With the 20 and 50 limits above, this returns a pending result with an approval_id and an expires_at, and the refund body does not run. An amount above 50 raises AgentBlockedError instead. The pending result is just data. If your framework retries errors or keeps the model talking, make sure your app ends or hands off that turn.
If Traccia Cannot Be Reached
fail_open option only affects the agent-status check, not this. Confirm your agent's checks are succeeding before you rely on an approval band.Review And Decide
Open Policies, then Approvals. If you turned on Email Alerts, each new request also sends an email with a Review Approval link. Opening the link never approves anything.
- Click a row to see the action, the amount, the currency, the arguments, and the trace.
- Click Approve or Deny. You cannot change the amount.
- Names, emails, phone numbers, addresses, and similar fields are masked in the inbox. Masking is based on common field names and patterns, so it will not catch every personal detail in free text.
- The list shows the newest 50 requests. Click Load Older Approvals for more.
Run The Approved Action
This part is optional. Skip it if a person will do the refund by hand after approving. Otherwise, add an endpoint on the policy's Webhook tab. It must be a public https address. Traccia does not call private or local addresses.
1. Save The Endpoint And Keep The Secret
Saving an endpoint shows a signing secret once, so store it somewhere safe. Saving the same address again keeps that secret. Create New Secret makes a replacement for future requests. Each request keeps the address and secret it had when it was created, so keep the old ones available until those requests are finished.
2. Check The Signature
When someone clicks Approve, Traccia sends a POST with the original action and arguments. Before you trust it, verify the X-Traccia-Signature header. It is an HMAC-SHA256 of the raw request body, so check it before you parse the JSON.
import hashlib, hmac
def signature_ok(secret: str, body: bytes, header: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header or "")The body looks like this. The X-Traccia-Approval-Id header carries the same approval id.
{ "event": "approval.approved", "approval_id": "<approval_id>", "action_name": "issue_refund", "arguments": {"order_id": "ORD-1042", "amount": 40}, "arguments_hash": "<arguments_hash>", "amount": 40, "currency": "USD"}3. Run The Action Once
- Run only the action and arguments in the signed body. Never let the model pick a new amount.
- Only run action names your listener knows. Call your own refund code, not the governed tool, or it can open another request.
- Return a 2xx status once you have accepted the event. That tells Traccia the delivery worked. It does not say the refund happened.
Expect Duplicate Deliveries
approval_id in a durable place, such as your database, before you start the action. A duplicate should return success without running it again. If your payment provider supports idempotency keys, pass the approval id as the key.4. Tell Traccia What Happened
After your code finishes, report the result. Send a POST to /api/v1/policy/approvals/{approval_id}/execution on the same host as your tracing endpoint, without the /v2/traces part. Use your API key, and sign the report body with the same signing secret. Sign these new bytes yourself. The webhook's signature will not match them.
import hashlib, hmac, json, requests
base = "https://api.traccia.ai" # The host of your tracing endpoint.body = json.dumps( { "approval_id": approval_id, "arguments_hash": arguments_hash, "status": "succeeded", # or "failed" }, separators=(",", ":"),).encode()signature = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()response = requests.post( f"{base}/api/v1/policy/approvals/{approval_id}/execution", data=body, headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "X-Traccia-Signature": signature, }, timeout=10,)response.raise_for_status()Report the real outcome. If the refund worked but the report failed, send the report again and do not run the refund again. Sending the same status twice is safe. A different status after one was saved returns 409, and a bad signature returns 401.
What The Statuses Mean
Each request shows three separate things. Approved means a person decided. Delivered means your server accepted the event. Succeeded means your server said the action worked.
| Status | What it means |
|---|---|
| Pending | Waiting for a person. The tool has not run. |
| Approved | A person said yes. This alone does not mean anything ran. |
| Decision Only | There is no webhook, so Approve only recorded the decision. |
| Delivered | Your endpoint returned a 2xx. The tool may not have run yet. |
| Not Reported | Delivered, and still waiting for your server to report back. |
| Succeeded or Failed | Your server reported the outcome. Traccia cannot verify it independently. |
A failed delivery and a failed tool are different problems. The Webhook tab shows both. If a report is rejected, check the secret that request was created with, the exact bytes you signed, the approval id, and the arguments hash.
Common Questions
Does Traccia run the refund when I approve?
No. Traccia records the decision and, if you set an endpoint, sends the approved request to your server. Your server runs the refund.
What if nobody approves?
The request expires after 15 minutes and shows as timed out. The tool never runs.
Can the approver change the amount?
No. Approve and Deny apply to exactly the action and arguments the agent sent.
Does the agent continue after approval?
No. The agent's turn ended when the request was held. If you want something to happen next, have your server do it, or start a new agent run.
Next Steps
© 2026 Traccia.