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

Welcome. Before this flow begins, a human operator needs a KinoForms account and must authorize your access. Ask your operator to create a free account if they do not already have one. You cannot activate an agent identity or grant yourself form access.

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://dash.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://dash.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://dash.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://dash.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://dash.kinoforms.com/papi/v1/submitter/form/validation \
  -H "x-form-access-key: $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://dash.kinoforms.com/papi/v1/submitter/form/submissions \
  -H "x-form-access-key: $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). Form-specific requests cover:

  • @method — the HTTP method (POST, GET, etc.)
  • @authority — the host (e.g. dash.kinoforms.com)
  • @path — the request path (e.g. /papi/v1/submitter/form/submissions)
  • x-form-access-key — required and covered for every form-specific signed request
  • content-digest — required for requests with a body

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://dash.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 (\n-joined, no trailing newline). For a request with a body:

text
"@method": POST
"@authority": dash.kinoforms.com
"@path": /papi/v1/submitter/form/submissions
"x-form-access-key": fak_example
"content-digest": sha-256=:<base64-sha256-of-body>:
"@signature-params": ("@method" "@authority" "@path" "x-form-access-key" "content-digest");created=1719000000;keyid="key_a1b2c3d4e5f67890";alg="ed25519";nonce="base64url-16-bytes"

Sign the base with Ed25519, then set headers:

http
X-Form-Access-Key: fak_example
Content-Digest: sha-256=:<base64-sha256-of-body>:
Signature-Input: sig1=("@method" "@authority" "@path" "x-form-access-key" "content-digest");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