> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usefini.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Preparing your APIs for an AI agent

> How to design the read and write endpoints an AI support agent calls: least-privilege credentials, idempotent writes, confirmation before irreversible actions, small explicit responses, clear errors, latency budgets, sandboxes and audit logs.

export const ChecklistMeter = ({title = "Checklist", items = [], results = {}, disclaimer}) => {
  const FV = {
    lime: "#C3EE5E",
    ink: "#131415",
    line: "rgba(127,127,127,0.28)",
    soft: "rgba(127,127,127,0.07)",
    softer: "rgba(127,127,127,0.04)",
    muted: "rgba(127,127,127,0.95)",
    pass: "#C3EE5E",
    warn: "#FFB020",
    fail: "#FF4D4D",
    radius: 14
  };
  const fvCard = {
    border: `1px solid ${FV.line}`,
    borderRadius: FV.radius,
    padding: 18,
    margin: "20px 0",
    background: FV.softer
  };
  const fvChip = active => ({
    border: `1px solid ${active ? FV.lime : FV.line}`,
    background: active ? FV.lime : "transparent",
    color: active ? FV.ink : "inherit",
    borderRadius: 999,
    padding: "6px 12px",
    fontSize: 13,
    fontWeight: 600,
    cursor: "pointer",
    lineHeight: 1.2
  });
  const fvBtn = primary => ({
    border: `1px solid ${primary ? FV.lime : FV.line}`,
    background: primary ? FV.lime : "transparent",
    color: primary ? FV.ink : "inherit",
    borderRadius: 10,
    padding: "7px 14px",
    fontSize: 13,
    fontWeight: 600,
    cursor: "pointer"
  });
  const fvLabel = {
    fontSize: 11,
    fontWeight: 700,
    letterSpacing: "0.08em",
    textTransform: "uppercase",
    opacity: 0.6,
    marginBottom: 8
  };
  const [on, setOn] = useState(() => items.map(() => false));
  const n = on.filter(Boolean).length;
  const reqMissing = items.some((it, i) => it.required && !on[i]);
  const pct = items.length ? Math.round(n / items.length * 100) : 0;
  const msg = n === items.length ? results.complete : reqMissing && results.missingRequired ? results.missingRequired : results.partial;
  return <div style={fvCard}>
      <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "baseline"
  }}>
        <div style={fvLabel}>{title}</div>
        <div style={{
    fontSize: 13,
    fontWeight: 700
  }}>{n} / {items.length}</div>
      </div>
      <div style={{
    height: 8,
    borderRadius: 999,
    background: FV.soft,
    overflow: "hidden",
    marginBottom: 12
  }}>
        <div style={{
    width: `${pct}%`,
    height: "100%",
    background: FV.lime,
    transition: "width .3s"
  }} />
      </div>
      {items.map((it, i) => <label key={i} style={{
    display: "flex",
    gap: 10,
    alignItems: "flex-start",
    padding: "8px 4px",
    borderTop: i ? `1px solid ${FV.line}` : "none",
    cursor: "pointer"
  }}>
          <input type="checkbox" checked={on[i]} onChange={() => setOn(o => o.map((v, j) => j === i ? !v : v))} style={{
    marginTop: 3,
    accentColor: FV.lime
  }} />
          <span style={{
    fontSize: 14,
    lineHeight: 1.5
  }}>
            {it.label}{it.required && <span style={{
    fontSize: 11,
    fontWeight: 700,
    marginLeft: 6,
    opacity: 0.6
  }}>REQUIRED</span>}
            {it.detail && <span style={{
    display: "block",
    fontSize: 12.5,
    opacity: 0.65
  }}>{it.detail}</span>}
          </span>
        </label>)}
      {msg && <div style={{
    marginTop: 12,
    fontSize: 13.5,
    padding: "10px 12px",
    borderRadius: 10,
    background: n === items.length ? "rgba(195,238,94,0.16)" : FV.soft
  }}>{msg}</div>}
      {disclaimer && <div style={{
    fontSize: 12,
    opacity: 0.6,
    marginTop: 8
  }}>{disclaimer}</div>}
    </div>;
};

To let an AI support agent act in your systems, give it a small set of purpose-built endpoints instead of your internal API: separate read and write endpoints, a dedicated least-privilege credential, an idempotency key on every write, and a confirmation step before anything irreversible. Responses should be small and stable, with explicit fields and a human-readable reason on every failure, and every call should be fast, rate-limited, testable in a sandbox and written to an audit log.

The agent is a new kind of client: it calls your APIs mid-conversation, on behalf of one customer, with inputs taken from natural language. Design for those three facts.

## The shape of the integration

Keep a layer between the agent and your core systems. That layer owns the policy (which customer, which action, which limits), the credential and the logging, so the agent never needs broad access.

```mermaid theme={null}
---
title: Agent, action layer and your systems
---
flowchart LR
    CUST(["Customer"]) --> AGENT["AI agent"]
    AGENT --> LAYER
    subgraph LAYER ["Action layer"]
        direction TB
        POL["Rules and limits"]
        CRED["Scoped credential"]
        LOG["Audit log"]
    end
    LAYER --> READS["Read endpoints"]
    LAYER --> WRITES["Write endpoints"]
    READS --> SYS[("Your systems")]
    WRITES --> SYS
    WRITES -.->|"Above limit"| TEAM(["Your team"])

    classDef source fill:#F7F7F7,color:#131415,stroke:#E8E8E8
    classDef agent fill:#131415,color:#FFFFFF,stroke:#131415,stroke-width:3px
    classDef surface fill:#FFFFFF,color:#131415,stroke:#131415
    classDef human fill:#C3EE5E,color:#131415,stroke:#131415,stroke-width:2px

    class CUST,SYS source
    class AGENT agent
    class POL,CRED,LOG,READS,WRITES surface
    class TEAM human
```

## Start from tasks, not from your API

List the customer tasks you want the agent to complete, then derive the endpoints. This keeps the surface small and easy to justify in a security review.

| Task | Reads | Writes | Reversible? | Limits |
| - | - | - | - | - |
| Where is my order? | Order status, tracking | None | Not applicable | Own orders only |
| Change delivery address | Order status | Update address | Yes, until shipped | Before dispatch only |
| Replace a lost card | Account status, active cards | Freeze card, order card | Freeze yes, order no | Fraud checks pass |
| Refund an order | Order, refund eligibility | Issue refund | No | Up to your auto-approve limit |

Build one endpoint per task step rather than exposing a generic "update customer" call. A narrow endpoint is easier to secure, test and explain.

## Separate reads from writes

Reads give the agent context: plan, account state, recent orders. They should be safe to call often, side-effect free, and use `GET`. Writes change state: cancel, refund, update. They should be few, explicit, and use `POST` or `PUT`. Keep them on separate credentials or scopes so a lookup integration can never move money.

## Scoping and least privilege

* **Give the agent its own service identity.** Never reuse an employee account or an admin key. You want to revoke it, rate-limit it and audit it on its own.
* **Scope per endpoint.** A credential for order lookups should not reach the refunds endpoint.
* **Bind every call to the customer in the conversation.** Enforce on the server that the record requested belongs to the verified customer. Don't rely on the agent to pass the right ID: inputs come from conversation text, and a customer can type someone else's email.
* **Enforce limits on the server.** Refund caps, allowed plan changes and eligibility rules belong in your API, so they hold even if the agent's configuration is wrong.
* **Use separate credentials per environment** (sandbox, staging, production).

## Authentication patterns

| Pattern | When it fits | Watch for |
| - | - | - |
| API key in a header | Server-to-server calls with a small number of endpoints | Store it in a secrets manager, rotate on a schedule, never share it over email or chat |
| OAuth client credentials | You already run an identity provider for service clients | Token lifetime and refresh; keep scopes narrow |
| Mutual TLS or an IP allowlist | Extra assurance on top of a key or token | Not a replacement for per-request auth |

Whichever you choose, validate the credential on every request and reject anything else with `401 Unauthorized`.

## Idempotency keys for writes

Networks time out and calls get retried. Without protection, a retry after a slow response can issue a second refund or a second replacement card. An idempotency key fixes this: the caller sends a unique key with each write, and your server stores the result for that key. A repeated request with the same key returns the stored result instead of acting again.

```http theme={null}
POST /refunds HTTP/1.1
Content-Type: application/json
Idempotency-Key: conv_8812_refund_order_5521

{ "order_id": "5521", "amount_cents": 2400 }
```

Derive the key from something stable, such as the conversation and the intent, so that the same request in the same conversation always maps to the same key. Keep keys long enough to cover your retry window, and return the same response body on a replay.

## Confirmation before irreversible actions

Classify every write as reversible (address change before dispatch), reversible at a cost (cancellation with a fee) or irreversible (refund, data deletion, transfer). For the last two:

1. **Read before you write.** Check eligibility and current state with a read endpoint first.
2. **Confirm with the customer** using a plain summary of exactly what will happen: "I'll cancel your Pro plan today. Your final charge is on June 15. Should I go ahead?"
3. **Re-check preconditions on the server** at write time, since state can change during the conversation.
4. **Hand off above a threshold.** Above your limit, the endpoint should refuse with a clear reason, and the agent should route the case to a person.

A stricter variant: a preview endpoint returns what would happen plus a token, and the commit endpoint accepts only that token, so the confirmed action and the executed action are the same.

## Response shape

The fields you return become what the agent can reason about and say. Keep responses small, flat and predictable.

* **Return only what the task needs.** Every extra field is data the agent might repeat to the customer.
* **Use explicit fields.** `"account_closed": true` is clearer than inferring state from a status code buried in a nested object.
* **Keep names and types stable.** The same field, same case style, same type, in every response. Return `null` rather than omitting a field.
* **Prefer low-cardinality enums** for status fields, with any display label in a separate field.
* **Include a human-readable reason**, written so it could be shown to a customer.

```json theme={null}
{
  "success": false,
  "error_code": "OUTSIDE_REFUND_WINDOW",
  "message": "This order was delivered 45 days ago. Refunds are available for 30 days.",
  "eligible_for_store_credit": true
}
```

## Error design

Separate business outcomes from protocol failures. "Not eligible for a refund" is a valid answer: return `200 OK` with `success: false`, a stable `error_code` and a message. Reserve `4xx` and `5xx` for bad input, missing auth and server errors. The code lets the agent branch (offer store credit, hand off); the message lets it explain. Never return stack traces in error bodies.

| Situation | Status | Body |
| - | - | - |
| Action completed | `200` | `success: true`, plus result fields such as a confirmation ID |
| Business rule refused it | `200` | `success: false`, `error_code`, customer-readable `message` |
| Missing or malformed input | `400` | Which field is wrong |
| Bad credential | `401` | Generic message |
| Record not found for this customer | `404` | `error_code` such as `USER_NOT_FOUND` |
| Rate limit reached | `429` | A `Retry-After` header |
| Unexpected failure | `500` | Generic message; details go in your logs |

## Latency budgets

The customer is waiting while the agent calls you, often across several calls in one turn. Set a budget per endpoint, keep reads faster than writes, and time out rather than hang. Avoid endpoints that fan out to many downstream services. For work that genuinely takes long (a document check, a bank transfer), accept the request quickly, return a reference and status, and let the agent tell the customer when to expect the result.

## Rate limits

Agent traffic follows conversation volume, so it spikes during outages and campaigns. Give the agent's credential its own limit so it can't starve other clients, return `429` with `Retry-After`, and share the limit with whoever configures the agent.

## Sandbox and test environments

Provide a sandbox that implements the same contract as production, with test customers that cover your real cases: active, closed, high-risk, several cards, no orders. Add a way to force failures (timeouts, `500`s, business refusals) so you can test how the agent recovers. Never point a new write integration at production until it has passed a representative set of test conversations in the sandbox.

## Audit logging

Log every agent call so any action can be traced back to a conversation.

| Field | Why |
| - | - |
| Timestamp and endpoint | What happened, when |
| Credential or service identity | Which client made the call |
| Conversation or interaction ID | Links the call to the transcript |
| Customer ID | Whose record was read or changed |
| Idempotency key | Ties retries to the original request |
| Outcome and error code | What the customer was told |

Redact secrets and sensitive values such as full card numbers, and keep logs for as long as your own retention policy requires.

## Readiness checklist

<ChecklistMeter
  title="Is your API ready for an AI agent?"
  items={[
{ label: "Endpoints map to specific customer tasks", detail: "No generic update calls", required: true },
{ label: "Reads and writes use separate credentials or scopes", required: true },
{ label: "The agent has its own service identity", detail: "Revocable and rate-limited on its own", required: true },
{ label: "Server checks the record belongs to the verified customer", required: true },
{ label: "Limits and eligibility are enforced server-side", required: true },
{ label: "Every write accepts an idempotency key", required: true },
{ label: "Irreversible actions have a confirmation step and a handoff threshold", required: true },
{ label: "Responses are flat, stable, and include a readable reason" },
{ label: "Business refusals return success false with an error code" },
{ label: "Latency budget and timeouts set per endpoint" },
{ label: "Dedicated rate limit with 429 and Retry-After" },
{ label: "Sandbox with realistic test customers and forced failures", required: true },
{ label: "Every call is logged with the conversation ID" }
]}
  results={{
complete: "Ready to connect. Run your test conversations against the sandbox before switching to production.",
partial: "The essentials are in place. Close the remaining items before you scale traffic.",
missingRequired: "Not ready for write actions yet. Finish the required items first; read-only lookups may be fine to start."
}}
/>

## Common mistakes

* **Exposing the internal admin API** with one broad key.
* **Trusting the customer ID** the agent passes without checking it on the server.
* **Retries without idempotency**, which turns a timeout into a duplicate refund.
* **Business refusals as `500`s**, so the agent can't tell "not eligible" from "broken".
* **Nested, changing response shapes** that break the agent's field mappings after a release.
* **Testing only the happy path** in the sandbox.

## Doing this in Fini

In Fini (usefini.com), your endpoints plug in like this:

* **The contract Fini expects:** HTTPS only, JSON in and out, an `x-api-key` header on every request, flat responses with stable fields, and a target of under two seconds for reads and under five seconds for writes. Write responses return `success` and `message`, plus `error_code` on failure. See the [API contract](/en/api-reference/api-contract), which also lists what to send your Fini contact, including rate limits and expected latency.
* **Reads and writes:** read endpoints become [Attributes](/en/api-reference/attributes), which run on every message; write endpoints become [Actions](/en/api-reference/actions), which run only when a **Tool** node in a published Rulebook rule invokes them. Test an Action with sample inputs using the **Play** button before saving.
* **Regression tests and live calls:** [Test Suite](/en/testing/test-suite) uses recorded or mock Action responses and never calls your endpoints, so it checks the agent's decisions (which Action, when, and how it handles a **Success** or **Failure** response) but not your API itself. Check the live endpoints end to end against your sandbox, then in a controlled [pilot](/en/playbooks/running-a-pilot).
* **Idempotency on MCP tools:** if a write tool on a connected MCP server declares an `idempotency_key` input, Fini generates and injects one per conversation and intent. See [MCP Connections](/en/api-reference/connect-mcp).
* **Keys for calling Fini:** if your systems also call Fini's public APIs, use workspace keys from **Deploy → API Keys** with only the `read` or `write` scope they need. See [API Keys](/en/deploy/api-keys).
* **A worked example:** the [card replacement walkthrough](/en/walkthroughs/card-replacement) chains a lookup attribute, two write Actions, a Form for the shipping address and a Reply Behavior fraud gate, built in a sandbox first.

## Related

<CardGroup cols={2}>
  <Card title="API contract" icon="code" href="/en/api-reference/api-contract">
    Endpoint requirements, sample requests and responses, and the handoff checklist.
  </Card>

  <Card title="Actions" icon="wand-magic-sparkles" href="/en/api-reference/actions">
    Typed API calls the agent runs from a Rulebook Tool node.
  </Card>

  <Card title="Card replacement walkthrough" icon="credit-card" href="/en/walkthroughs/card-replacement">
    Two chained write Actions with a fraud gate, end to end.
  </Card>

  <Card title="Designing an escalation policy" icon="signs-post" href="/en/playbooks/escalation-policy">
    Which conversations, including high-value actions, must reach a person.
  </Card>
</CardGroup>


## Related topics

- [Designing an escalation policy for AI support](/en/playbooks/escalation-policy.md)
- [Preparing your knowledge base for an AI agent](/en/playbooks/knowledge-base-prep.md)
- [Running multilingual AI support well](/en/playbooks/multilingual-support.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.