tollgate
Guides

Add Tollgate to a customer support agent in 15 minutes

A real example — Stripe refund, Slack approval, YAML policy with amount threshold. Copy-paste ready.

You have a customer support agent that can issue refunds via Stripe. It works. But you would never run it unsupervised in production — one bad prompt and it refunds everything.

This guide adds Tollgate in 15 minutes. Small refunds go through automatically. Large refunds pause and ask a human in Slack. Everything is logged.


What you'll build

Scenarioamount = $30 → auto-allow
customer message— "My order arrived broken"
agent— processes request
tg.check("issue_refund", …)— amount: 30.00
policy: amount ≤ $50— rule matched
allow instantly— no human needed
Stripe refund executes— $30 returned
step 0 / 6fast path

Prerequisites

  • A Tollgate account — sign up free
  • Python 3.10+
  • A Stripe test account (or swap in your own refund logic)
  • Slack workspace with a #approvals channel

Step 1 — Create an agent (2 min)

  1. Go to app.usetollgate.com/agents
  2. Click New Agent → name it support-bot
  3. Copy the API key — it won't be shown again
export TOLLGATE_API_KEY="tg_live_..."

Step 2 — Write the policy (3 min)

Go to your agent → Edit Policy. Paste this:

version: 1

rules:
  # Refunds under $50 — auto-approve, no friction
  - action: issue_refund
    when:
      amount: { lte: 50 }
    decide: allow

  # Refunds $50–$500 — require a human to approve in Slack
  - action: issue_refund
    when:
      amount: { lte: 500 }
    decide: require_approval
    approvers:
      - "#approvals"

  # Refunds over $500 — always block, handle manually
  - action: issue_refund
    when:
      amount: { gt: 500 }
    decide: deny
    reason: "Refunds over $500 require manual processing"

# Everything else the agent does is allowed by default
default: allow

Click Save Policy. This is now live — no deploy needed.

This is how Tollgate evaluates each incoming action against your rules:

INCOMING
issue_refund({ amount: 20.00 })
1
when: amount ≤ 25→allow
2
when: amount ≤ 250→require_approval
3
when: amount > 250→deny

Step 3 — Connect Slack (3 min)

Go to Settings → Slack → Connect Slack. Authorize your workspace.

When Tollgate sends an approval request, it will post to #approvals with two buttons — Approve and Reject. The agent blocks and waits for the click.

Here's the full lifecycle from agent call to resumed execution:

1
Agent calls a guarded tool
tg.guard() sends request to Tollgate API
2
Policy: require_approval matched
Rule fires — action is held, not executed
3
Slack message posted
Action details + Approve/Reject buttons sent to #channel
4
SDK polls for decision
GET /v1/check/{id} every 2–3 seconds
5
Team member clicks Approve
One click in Slack — no dashboard needed
6
Slack sends webhook to Tollgate
POST /slack/interactive → decision recorded
7
Action status updated
approved → allowed, rejected → denied
8
SDK poll returns
Agent proceeds with the action — or stops cleanly
step 1 / 8

Step 4 — Install the SDK (1 min)

pip install tollgate-sdk

Step 5 — Wrap your refund function (5 min)

Here's a complete example. Your agent already has a issue_refund function — wrap it with tg.guard():

import os
import stripe
from tollgate import Tollgate, ActionDenied, ActionPending

stripe.api_key = os.environ["STRIPE_SECRET_KEY"]

tg = Tollgate(api_key=os.environ["TOLLGATE_API_KEY"])


@tg.guard("issue_refund")
def issue_refund(amount: float, customer_id: str, reason: str = "") -> dict:
    """Issue a refund via Stripe. Only runs if Tollgate allows it."""
    intent = stripe.PaymentIntent.create(
        amount=int(amount * 100),  # Stripe uses cents
        currency="usd",
        customer=customer_id,
    )
    refund = stripe.Refund.create(payment_intent=intent.id)
    return {"status": "refunded", "refund_id": refund.id, "amount": amount}


# Your agent calls this like any normal function
def handle_customer_request(message: str, customer_id: str):
    # ... your agent logic here ...
    # When it decides to issue a refund:
    try:
        result = issue_refund(
            amount=75.00,
            customer_id=customer_id,
            reason="Item arrived damaged"
        )
        return f"Refund of ${result['amount']} processed successfully."

    except ActionDenied as e:
        return f"Refund blocked: {e.reason}"

    except ActionPending:
        return "Refund is pending human approval. The customer will be updated shortly."

That's it. The @tg.guard decorator intercepts the call, checks the policy, and either:

  • Returns immediately (allowed)
  • Raises ActionDenied (denied)
  • Blocks until a human approves or rejects in Slack (pending), then either returns or raises ActionDenied

What it looks like in Slack

When a $75 refund is requested, your #approvals channel gets:

approvals2 members
T
TollgateToday at 14:03

🔔 Approval required — support-bot

Actionissue_refund
Amount$75.00
Customercus_abc123
ReasonItem arrived damaged
Expires5 minutes

The agent is frozen at issue_refund(...) waiting. Click Approve — the Stripe refund executes and the customer gets their money. Click Reject — ActionDenied is raised and you handle it in your code.


What it looks like in the audit log

Every call to tg.guard() creates a record in your audit log:

ActionAmountDecisionDecided byTime
issue_refund$30allowedpolicy14:02:01
issue_refund$75approved[email protected]14:03:47
issue_refund$750deniedpolicy14:05:12
cancel_subscription—approved[email protected]14:11:03
Live · 0 of 4 events

Using async? Use AsyncTollgate

from tollgate import AsyncTollgate, ActionDenied

tg = AsyncTollgate(api_key=os.environ["TOLLGATE_API_KEY"])


@tg.aguard("issue_refund")
async def issue_refund(amount: float, customer_id: str) -> dict:
    refund = await stripe.Refund.create_async(...)
    return {"status": "refunded", "amount": amount}

Using LangChain or OpenAI function calling?

Wrap the tool function the same way — Tollgate doesn't care what calls the function, only what the function does:

from langchain.tools import tool

@tool
@tg.guard("issue_refund")
def issue_refund(amount: float, customer_id: str) -> str:
    """Issue a refund to a customer."""
    # ...

For LangGraph with interrupt(), Tollgate replaces the need for it entirely — you get the pause, the Slack notification, and the resume in one decorator instead of building it yourself.


Troubleshooting

Agent blocks forever and never gets a Slack message → Check that Slack is connected in Settings → Slack and that the channel name in your policy matches exactly (including the #).

ActionDenied raised immediately even for small amounts → Check your policy YAML indentation — YAML is whitespace-sensitive. Re-save the policy in the dashboard.

I want to test without real Stripe calls → Replace the Stripe call with print(f"Would refund ${amount}") — Tollgate doesn't know or care what's inside the guarded function.


Next steps

On this page