Fill API: submit answers to a form with a fill token

Updated 2026-10-30

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:

http
POST /v1/public/forms/{slug}/fill-tokens

It returns 201:

json
{
  "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.

bash
curl https://aiformapi.com/v1/fill/fil_8Xk2mQ9vTz4LbN7cRp1WdYs5HgJf3aEu
json
{
  "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.

bash
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 number is a number, a checkbox is true or false, a multi_choice is a list of strings, a date is YYYY-MM-DD.
  • A single_choice value must match one option exactly.
  • Leave out optional questions the person skipped. An empty string, null or 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.

json
{
  "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.

json
{
  "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.

json
{"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
HTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 10
RateLimit-Remaining: 0
RateLimit-Reset: 12
json
{"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

CodeMeaning
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_largeThe body is over 64 KB.
not_found (404)No such token, or the token is not in the right format.
rate_limiter_unavailableWe could not check the limit and refuse to guess. Try again later.

See Errors for the full list.

Complete examples

JavaScript

js
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

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.