Skip to content

Agent Quickstart

This guide walks an AI agent from zero to a successful submission. The flow is:

text
register agent  →  operator grants scopes  →  exchange secret for token  →  validate  →  self-correct  →  submit

Read the Overview for the envelope and auth model. All examples use the standard envelope.

The Auth Model in One Paragraph

An agent is a registered identity with a secret. An agent cannot do anything until an operator (a human, via the app UI or a session) grants it scopes on a specific form. The agent then exchanges its secret for a 7-day session token and acts within its granted scopes. Grants are read live, so revoking a grant takes effect immediately.

1. Register an Agent

Registering an agent returns the agent's secret once. Store it securely; it cannot be retrieved again.

bash
curl -X POST https://api.kinoforms.com/papi/v1/identity/agents \
  -H "x-api-key: $KINOFORMS_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "name": "intake-bot" }'
json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "clientId": "ag_f47ac10b58cc4372a5670e02b2c3d479",
    "secret": "ags_live_3b91f2a8c7d4e6b1...  (shown ONCE)",
    "secretPrefix": "ags_live_3b",
    "name": "intake-bot",
    "status": "active",
    "workspaceId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
  },
  "apiVersion": "v1"
}

If you lose the secret, rotate it (you cannot read it back):

bash
curl -X POST https://api.kinoforms.com/papi/v1/identity/agents/550e8400-e29b-41d4-a716-446655440000/rotate-secret \
  -H "x-api-key: $KINOFORMS_API_KEY"

Other identity endpoints:

  • GET /papi/v1/identity/agents — list agents.
  • GET /papi/v1/identity/agents/{id} — fetch one agent.
  • PATCH /papi/v1/identity/agents/{id} — rename or disable ({ "status": "disabled" }; set "status": "active" to re-enable).
  • DELETE /papi/v1/identity/agents/{id} — revoke the agent entirely (terminal; status becomes revoked, grants and sessions stop authorizing immediately).

2. Operator Grants Scopes

The agent is useless until an operator grants it scopes on a form. A grant binds an agent to one form with a set of scopes. Grant creation is authenticated with a Better Auth session cookie (the app UI's auth) — not an API key, not x-respondent-token.

bash
curl -X POST https://api.kinoforms.com/papi/v1/identity/agents/550e8400-e29b-41d4-a716-446655440000/grants \
  -H "cookie: $OPERATOR_SESSION_COOKIE" \
  -H "content-type: application/json" \
  -d '{
    "formId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "scopes": ["agent:read", "agent:validate", "agent:submit"],
    "respondentId": "6ba7b812-9dad-11d1-80b4-00c04fd430c8"
  }'

Grant fields:

  • formId (required) — the form the grant applies to.
  • scopes (required) — a subset of agent:read, agent:validate, agent:submit.
  • respondentId (optional) — bind the grant to a specific respondent. If the form requires it, submissions without the assigned respondent are rejected (respondent_not_assigned).

agent:submit requires a workspace admin/editor session

agent:submit grants can only be created by a UI/app session held by a workspace admin or editor on the form's workspace — never by an API key. An API-key caller that tries to grant agent:submit is rejected with submit_not_grantable_by_api_key; binding a respondent via an API key is rejected with respondent_binding_requires_session. agent:read and agent:validate may be granted by an API key. This invariant is enforced in identity.rs:527-553 (grant-creation authorization check).

Manage grants:

  • GET /papi/v1/identity/agents/{id}/grants — list an agent's grants.
  • DELETE /papi/v1/identity/agents/{id}/grants/{grant_id} — revoke a grant. Takes effect immediately on the next agent call.

3. Exchange the Secret for a Token

The agent trades its secret for a 7-day session token:

bash
curl -X POST https://api.kinoforms.com/papi/v1/auth/token \
  -H "content-type: application/json" \
  -d '{ "clientSecret": "'"$AGENT_SECRET"'" }'
json
{
  "data": { "token": "rt_live_8f2c...", "expiresAt": "2026-06-29T12:00:00Z" },
  "apiVersion": "v1"
}

Cache the token and reuse it until expiresAt. Send it as x-respondent-token on validate/submit.

4. Validate (and Self-Correct)

Validate is a dry run that always returns 200. Use it to iterate. It needs the agent:validate scope.

bash
curl -X POST https://api.kinoforms.com/papi/v1/validate/$ACCESS_KEY \
  -H "x-respondent-token: $SESSION_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "data": { "email": "not-an-email" } }'
json
{
  "data": {
    "valid": false,
    "errors": [
      { "field": "email", "code": "format", "message": "Value is not a valid email address.",
        "help": "https://docs.kinoforms.com/developer/api/validation#format" }
    ],
    "warnings": [],
    "missingRequired": ["name"]
  },
  "apiVersion": "v1"
}

The self-correction loop:

  1. Read errors — fix each field; follow the help link for the exact rule.
  2. Read missingRequired — supply each missing key.
  3. Re-validate. Repeat until valid is true and missingRequired is empty.

Validate is throttled generously precisely so an agent can iterate fast. See Parsing validation responses for the per-field codes and self-correction recipes.

5. Submit

Once validate is clean, submit for real. This needs the agent:submit scope (operator-granted).

bash
curl -X POST https://api.kinoforms.com/papi/v1/submit/$ACCESS_KEY \
  -H "x-respondent-token: $SESSION_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "data": { "email": "[email protected]", "name": "Ada" } }'

A clean submit returns 200 with the created submission. Submit is rate-limited to 1 per 5 seconds per agent (see Rate Limits), so validate first rather than retrying submit in a tight loop.

For long or resumable flows, use partial save-and-continue (?partial=true) — see the Submitter guide.

Signed Requests (RFC 9421)

An agent can self-authenticate by signing its HTTP requests — no bearer token needed. This is gated by the FEATURE_AGENT_SIGNING flag (off by default).

How It Works

The agent signs outgoing requests with an Ed25519 private key following RFC 9421 (HTTP Message Signatures). The covered components are exactly:

  • @method — the HTTP method (POST, GET, etc.)
  • @authority — the host (e.g. api.kinoforms.com)
  • @path — the request path (e.g. /papi/v1/submit/...)

Parameters: created (Unix timestamp), keyid (the registered key identifier), alg="ed25519", nonce (a unique base64url random, 16 bytes).

The resulting Signature and Signature-Input headers are attached to the request. If both a signature AND a bearer token are present, the signature takes precedence. A present-but-invalid signature is a uniform 401 (never a silent fall-through to bearer).

Registering a Key

Generate an Ed25519 keypair. The public key must be in SPKI PEM format (with -----BEGIN/END PUBLIC KEY----- wrappers). Register it:

bash
curl -X POST https://api.kinoforms.com/papi/v1/identity/agents/550e8400-e29b-41d4-a716-446655440000/keys \
  -H "cookie: $OPERATOR_SESSION_COOKIE" \
  -H "content-type: application/json" \
  -d '{
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----"
  }'

The response includes the derived keyId"key_" + sha256(publicKeyPem)[:16]. This is the value you put in the keyid signature parameter. The private key NEVER leaves the agent.

Signing a Request

Build the signature base (4 lines, \n-joined, no trailing newline):

text
"@method": POST
"@authority": api.kinoforms.com
"@path": /papi/v1/submit/abc123
"@signature-params": ("@method" "@authority" "@path");created=1719000000;keyid="key_a1b2c3d4e5f67890";alg="ed25519";nonce="base64url-16-bytes"

Sign the base with Ed25519, then set headers:

http
Signature-Input: sig1=("@method" "@authority" "@path");created=1719000000;keyid="key_a1b2c3d4e5f67890";alg="ed25519";nonce="base64url-16-bytes"
Signature: sig1=:<base64-encoded-signature>:

Each nonce must be unique (replays are rejected). The created timestamp must be within ±300 seconds of server time.

Reference Implementation

See experiments/agent-credential-mcp/ — a zero-dependency MCP server that manages credentials and signs requests without exposing the private key to the agent's context window.

Failure Codes Worth Handling

  • agent_inactive — the agent was disabled or revoked.
  • no_grant — no grant exists for this agent + form.
  • missing_scope — the grant lacks the scope this call needs (e.g. agent:submit).
  • respondent_not_assigned — the form is bound to a respondent the agent is not assigned to.
  • tenant_mismatch / workspace_mismatch / binding_required — the form is outside the agent's binding.
  • submit_not_grantable_by_api_key — an API key tried to grant agent:submit.
  • rate_limited — back off and retry.

All of these are documented in the Validation reference.

Rate Limits

TierScopeLimitEnv var (default)
Submitper agent1 / 5s (hardcoded)
Submitper IP (pre-throttle)30 / minRATE_LIMIT_PAPI_SUBMIT_IP_PER_MINUTE (30)
Validateper agent30 / minRATE_LIMIT_PAPI_VALIDATE_PER_MINUTE (30)
Validateper IP (pre-throttle)60 / minRATE_LIMIT_PAPI_VALIDATE_IP_PER_MINUTE (60)
Auth mintper IP30 / minRATE_LIMIT_PAPI_AUTH_PER_MINUTE (30)

When a request is throttled the API returns 429 with rate_limited. Retry with exponential backoff (honor any Retry-After).

KinoForms documentation