Human-in-the-Loop for AI Customer Support Agents
A support agent that drafts replies is safe. One that also sends them, issues refunds, or cancels accounts needs a human in the loop before any of that reaches a customer.
Where support agents actually go wrong
Most AI support setups fail the same way: the model is good at drafting a plausible-sounding reply, but "plausible-sounding" and "correct" are different things. A customer says their order never arrived, and the agent — trusting the customer's framing without checking the shipment record — offers a full refund plus a discount code. A customer asks to close their account, and the agent, trying to be helpful, does it immediately instead of confirming they understand it's irreversible.
None of this requires the model to be malicious or even particularly wrong. It just needs to be confidently overconfident, which is the default mode for most LLMs. The fix isn't a better prompt telling it to "be careful with refunds" — that's advice the model can and will talk itself past under the right conversational pressure. The fix is a gate the agent's code cannot route around: draft the reply or the refund, submit it for approval, and only call the real API once a human has said yes.
Triage: what needs a human, what doesn't
Not every support action deserves an approval step — that would just turn your inbox into a bottleneck. A reasonable split:
| Action | Approve first? |
|---|---|
| Answering a "how do I reset my password" FAQ | No |
| Replying with account-specific details (order status, balance) | No, if read-only |
| Issuing a refund or credit | Yes |
| Cancelling or downgrading a subscription | Yes |
| Replying to an angry or legal-sounding message | Yes |
| Escalating to a human teammate | No — that's already the safe path |
The pattern below applies to any of the "yes" rows; refunds specifically are covered in more depth in approving refunds issued by an AI support agent.
Wiring the gate with the Python SDK
Here's a support agent that drafts a reply to a billing complaint and won't send it until approved:
import time
from impri import ImpriClient
client = ImpriClient(api_key=os.environ["IMPRI_API_KEY"])
def handle_billing_ticket(ticket):
draft = generate_reply(ticket) # your LLM call, not shown
action = client.actions.create(
kind="support.reply",
title=f"Reply to ticket #{ticket.id}: {ticket.subject}",
preview={"format": "markdown", "body": draft},
target_url=ticket.helpdesk_url,
expires_in=3600, # a support ticket goes stale fast
editable=["preview.body"],
)
while True:
result = client.actions.get(action.id)
if result.status != "pending":
break
time.sleep(5)
if result.status == "approved":
final_body = result.decision.final_preview["body"]
helpdesk.post_reply(ticket.id, final_body)
client.actions.report_result(action.id, status="executed")
else:
# rejected or expired — escalate to a human agent instead
helpdesk.assign_to_queue(ticket.id, queue="human-review")The important detail is editable=["preview.body"] — a support lead can fix the tone or the numbers before approving, and final_preview (never the original draft) is what actually gets sent. If the ticket sits unreviewed for an hour, it expires rather than going out stale, and the fallback path routes it to a real person instead of silently dropping it.
Give reviewers enough to decide fast
A support lead approving from their phone between other tickets doesn't have time to re-read the whole thread. Put the decision-relevant facts in the title and keep the preview body to what's actually being sent — not a dump of internal context. If the action depends on something the reviewer can't see in the preview (e.g., "this customer has churned twice before"), that belongs in your ticket system, not stuffed into the approval card.
For refunds and other actions with financial impact, set idempotent: false in the action payload so the approval card shows a warning against approving it twice — see the optional fields in the main integration guide.
Boundaries worth being explicit about
Impri is the approval gate — it stores the draft reply, notifies the reviewer, and holds the decision. It does not read the support ticket, judge whether the refund amount is reasonable, or understand your helpdesk's data model. That judgment stays with the human reviewer and with your agent's own logic for what to draft in the first place.
It's also only a real gate if helpdesk.post_reply and refund_api.issue are unreachable except through this code path. If your support agent also holds a raw API key it could call directly, wrap that client so the unapproved path doesn't exist in the code — see SDK integrations for the wrapper pattern, and approve emails before your AI agent sends them if replies go out over email specifically.
Next step: quickstart to get an API key and try this against a real ticket.