Fill API: submit answers to a form with a fill token
A fill token lets an agent read one form and send one set of answers. No API key is needed. Use GET /v1/fill/{token} to read the form. Use POST /v1/fill/{token} to submit. A token works for 60 minutes, one successful submit and 5 attempts.
Get a token
People get a token from the form page. The page calls this public endpoint:
POST /v1/public/forms/{slug}/fill-tokensIt returns 201:
{
"token": "fil_8Xk2mQ9vTz4LbN7cRp1WdYs5HgJf3aEu",
"fill_url": "https://aiformapi.com/v1/fill/fil_8Xk2mQ9vTz4LbN7cRp1WdYs5HgJf3aEu",
"prompt": "You are helping me fill in a form. ...",
"expires_at": "2026-10-30T10:14:00Z",
"form_id": "frm_4k9QeV2mTc8Xa1"
}A fill token starts with fil_. If the owner turned agent fill off, this call returns 403 forbidden with details.reason set to agent_fill_disabled.
When spam protection is on, the request carries a turnstile_token. Tokens are meant for a person who asked their agent to help, so the agent receives the token inside the prompt. See Agent fill.
GET /v1/fill/{token}
Some agents can only read a URL. This call returns the form and instructions as JSON. A token allows 30 reads per hour.
curl https://aiformapi.com/v1/fill/fil_8Xk2mQ9vTz4LbN7cRp1WdYs5HgJf3aEu{
"form": {
"id": "frm_4k9QeV2mTc8Xa1",
"title": "Friday dinner RSVP",
"description": "Tell us if you can come. It takes 20 seconds.",
"fields": [
{"id": "full_name", "label": "Your name", "type": "short_text", "required": true},
{"id": "attending", "label": "Can you come?", "type": "single_choice", "required": true,
"options": ["Yes", "No", "Maybe"]},
{"id": "guests", "label": "How many people are you bringing?", "type": "number", "min": 0, "max": 5}
]
},
"expires_at": "2026-10-30T10:14:00Z",
"attempts_left": 5,
"instructions": [
"Ask the person for any answer you do not know, one short question at a time. Never invent personal details.",
"Before you submit, show all answers and ask \"Send it?\". Submit only after they say yes.",
"Then POST {\"answers\": {...}} to https://aiformapi.com/v1/fill/{token}. 201 = done (tell the person the receipt). 422 = fix the listed fields. 429 = wait Retry-After seconds."
],
"submit": {
"method": "POST",
"url": "https://aiformapi.com/v1/fill/fil_8Xk2mQ9vTz4LbN7cRp1WdYs5HgJf3aEu",
"content_type": "application/json",
"body_example": {"answers": {"full_name": "", "attending": "", "guests": null}}
}
}Reading the form does not use up the token.
POST /v1/fill/{token}
Send the answers as an object. Each key is a field id.
curl -X POST https://aiformapi.com/v1/fill/fil_8Xk2mQ9vTz4LbN7cRp1WdYs5HgJf3aEu \
-H "Content-Type: application/json" \
-d '{"answers": {"full_name": "Ada Lovelace", "attending": "Yes", "guests": 1}}'Rules for answers:
- Use the field id as the key.
- Send the right JSON type. A
numberis a number, acheckboxistrueorfalse, amulti_choiceis a list of strings, adateisYYYY-MM-DD. - A
single_choicevalue must match one option exactly. - Leave out optional questions the person skipped. An empty string,
nullor an empty list also counts as skipped. - Include every required field. A required checkbox must be
true. - Send only ids that exist in the form. An unknown id is reported as a problem.
Field types are in Forms and fields. The body can be 64 KB at most.
Responses
201 Created
The answers are stored. Tell the person the receipt.
{
"response_id": "rsp_9Zt3mWq7Lx2KbN",
"receipt": "AF-7K3Q-9XMD",
"submitted_at": "2026-10-30T09:14:02Z"
}The response has the source agent_api.
422 invalid_answers
Some answers do not pass. Nothing is stored. Fix the listed fields and send again. You have 5 attempts in total.
{
"error": {
"code": "invalid_answers",
"message": "Some answers need a fix.",
"details": {
"fields": [
{"id": "guests", "problem": "must be at most 5"},
{"id": "attending", "problem": "must be one of: Yes, No, Maybe"}
],
"attempts_left": 4
}
}
}410 token_expired or token_used
The token is older than 60 minutes or has used its 5 attempts (token_expired), or it already stored a response (token_used). Ask the person to press the card on the form page and copy a new prompt.
{"error": {"code": "token_expired", "message": "This fill code has expired. Ask for a new one."}}429 rate_limited
You sent too many requests. Wait the number of seconds in the Retry-After header, then try again.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 10
RateLimit-Remaining: 0
RateLimit-Reset: 12{"error": {"code": "rate_limited", "message": "Too many requests. Try again in 12 seconds."}}The limit is 10 POST requests per minute for each IP address.
Other errors
| Code | Meaning |
|---|---|
form_closed (409) | The owner closed the form, or it reached its closing time or response cap. Nothing can be sent. |
quota_exceeded (403) | The form owner reached the monthly response limit. |
payload_too_large | The body is over 64 KB. |
not_found (404) | No such token, or the token is not in the right format. |
rate_limiter_unavailable | We could not check the limit and refuse to guess. Try again later. |
See Errors for the full list.
Complete examples
JavaScript
const TOKEN = process.env.FILL_TOKEN; // from the prompt
const ENDPOINT = `https://aiformapi.com/v1/fill/${TOKEN}`;
async function submit(answers) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ answers }),
});
if (res.status === 201) return res.json(); // { response_id, receipt, submitted_at }
const body = await res.json();
const code = body.error?.code;
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") || 5);
await new Promise((r) => setTimeout(r, wait * 1000));
return submit(answers);
}
if (res.status === 422) {
// Show body.error.details.fields to the person, fix the answers, then call submit() again.
// A token allows 5 attempts.
throw new Error("Fix these fields: " + JSON.stringify(body.error.details.fields));
}
if (res.status === 410) throw new Error(`${code}: ask for a new fill code`);
throw new Error(`${res.status} ${code}`);
}
const receipt = await submit({ full_name: "Ada Lovelace", attending: "Yes", guests: 1 });
console.log("Done. Receipt:", receipt.receipt);Python
import os
import time
import requests
TOKEN = os.environ["FILL_TOKEN"] # from the prompt
ENDPOINT = f"https://aiformapi.com/v1/fill/{TOKEN}"
def submit(answers):
while True:
r = requests.post(ENDPOINT, json={"answers": answers}, timeout=30)
if r.status_code == 201:
return r.json() # response_id, receipt, submitted_at
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
error = r.json().get("error", {})
if r.status_code == 422:
# Show these to the person, fix the answers, call submit() again (5 attempts in total).
raise ValueError(f"Fix these fields: {error['details']['fields']}")
if r.status_code == 410:
raise RuntimeError(f"{error.get('code')}: ask for a new fill code")
raise RuntimeError(f"{r.status_code} {error.get('code')}")
receipt = submit({"full_name": "Ada Lovelace", "attending": "Yes", "guests": 1})
print("Done. Receipt:", receipt["receipt"])Use the fill MCP instead
If the agent has the fill connector, it does not need HTTP. The tools get_fill_form and submit_fill apply the same validation and the same token rules. See MCP.