Your API's Newest Users Are Agents: Designing for Non-Human Clients A developer demonstrated a pattern for designing APIs that LLM-based agents can call reliably, arguing that human-oriented APIs fail machine clients through vague errors, non-idempotent writes, and bloated responses. The example implements a small Flask ticket API with machine-readable error codes, an Idempotency-Key header, and a published tool schema, paired with a plan-act-observe agent loop that has a hard step limit and success predicate. Your API's newest users are agents. They don't read docs, they don't browse dashboards, and they don't file support tickets. They parse OpenAPI specs, call endpoints in loops, and expect deterministic, machine-readable responses. If your API was designed for humans clicking buttons, it's already failing this new class of client. In this post, I'll walk through a concrete example: building a small "tool API" that an LLM-based agent can call. We'll cover the problem, a solution, and a runnable Python implementation. No hand-waving about "agentic workflows" — just code you can run and a loop with explicit termination conditions. Human-facing APIs optimize for discoverability and forgiveness. Agents optimize for determinism and low token cost. Three specific failure modes show up when agents hit a human-designed API: {"error": "bad request"} forces the agent to guess. The agent will retry, hallucinate a fix, or give up. Here's a minimal example of the kind of handler that causes these problems: python bad handler.py from flask import Flask, request, jsonify app = Flask name @app.route "/create ticket", methods= "POST" def create ticket bad : data = request.get json silent=True or {} if "title" not in data: return jsonify {"error": "bad request"} , 400 ... create ticket, no idempotency, returns everything return jsonify {"ticket": {"id": 1, "title": data "title" , "history": ... }} An agent calling this has no way to distinguish "missing field" from "malformed JSON" from "server error," and no safe retry path. Design the API for a client that is literal, stateless between calls, and token-budgeted. Concretely: code string the agent can branch on. Idempotency-Key header; store the result keyed by it. We'll build a small in-memory ticket API with the properties above, then write an agent loop that uses it. The agent loop is deliberately simple: it's a plan-act-observe loop with a hard step limit and a success predicate. No framework required. python agent api.py import json import uuid from dataclasses import dataclass, field, asdict from typing import Optional from flask import Flask, request, jsonify app = Flask name @dataclass class Ticket: id: str title: str status: str = "open" tickets: dict str, Ticket = {} idempotency store: dict str, dict = {} TOOL SCHEMA = { "name": "create ticket", "description": "Create a support ticket. Idempotent on Idempotency-Key header.", "parameters": { "type": "object", "properties": { "title": {"type": "string", "minLength": 1, "maxLength": 200}, }, "required": "title" , "additionalProperties": False, }, } @app.route "/tools", methods= "GET" def list tools : return jsonify {"tools": TOOL SCHEMA } @app.route "/create ticket", methods= "POST" def create ticket : key = request.headers.get "Idempotency-Key" if not key: return jsonify {"code": "missing idempotency key", "message": "Provide Idempotency-Key header."} , 400 if key in idempotency store: return jsonify idempotency store key , 200 data = request.get json silent=True if not isinstance data, dict : return jsonify {"code": "invalid json", "message": "Body must be a JSON object."} , 400 title = data.get "title" if not isinstance title, str or not title.strip : return jsonify {"code": "invalid title", "message": "'title' must be a non-empty string."} , 422 ticket = Ticket id=str uuid.uuid4 , title=title.strip tickets ticket.id = ticket payload = {"ticket": asdict ticket } idempotency store key = payload return jsonify payload , 201 @app.route "/tickets/