Agent Ingestion API Guide

How an agent runtime submits work receipts and checks their status. Canonical routes live under /api/v1; pre-v1 unversioned aliases still respond but return Deprecation and Sunset headers — do not build against them.

Machine-readable contract: GET /api/v1/openapi.json (generated from the route schemas). Interactive docs are served at /api/v1/docs only when the deployment sets ENABLE_API_DOCS=true. JSON Schemas: /api/v1/schemas/work-receipt.v0.3.json, .../work-receipt.v0.2.json, .../receipt-event.v0.1.json, .../receipt-event.v0.2.json.

Authentication

Agent endpoints use scoped bearer credentials:

Authorization: Bearer agk_<48 hex chars>

Credentials are bound to exactly one agent and carry scopes: receipts:create, receipts:read, profile:read. They can never attest work, name reviewers, or carry scores — submission schemas reject unknown fields outright rather than stripping them.

Browser sessions (HttpOnly cookies + x-csrf-token header) are a separate mechanism for the human portals and the operator-side credential-management endpoints below; agent keys are never accepted as browser logins.

Issuing a credential

Operator-side, cookie session + CSRF required:

POST /api/v1/operator/agents/:agentId/credentials
{ "scopes": ["receipts:create", "receipts:read"], "expires_in_days": 90 }

Response 201:

{
  "credential": { "key_id": "ak_...", "agent_id": "agt_...", "prefix": "agk_1a2b3c4d", "scopes": ["receipts:create", "receipts:read"], "expires_at": "2026-10-10T00:00:00.000Z" },
  "secret": "agk_..."
}

secret is shown exactly once and stored only as a hash. Omitting scopes grants all three. Manage credentials with GET /api/v1/operator/credentials (includes last_used_at audit) and POST /api/v1/operator/credentials/:keyId/revoke.

Submit a receipt

POST /api/v1/agent/receipts
Authorization: Bearer agk_...
Idempotency-Key: invoice-batch-2026-07-01
Content-Type: application/json
{
  "source_type": "api",
  "company_id": "org_001",
  "engagement_id": "eng-2026-07",
  "task": { "type": "invoice_processing", "category": "Finance", "summary": "Processed July invoice batch." },
  "started_at": "2026-07-01T09:00:00.000Z",
  "completed_at": "2026-07-01T09:05:00.000Z",
  "runtime": { "integration": "custom", "model_family": "claude", "workflow_id": "wf-42", "execution_id": "ex-4211" },
  "tools_used": ["erp_api", "ocr"],
  "outcome_claims": { "metrics": { "invoices_processed": 42, "success_rate": 0.98 }, "claims": ["Batch reconciled"], "estimated_hours_saved": 2 },
  "evidence": [
    { "type": "external_private_reference", "confidentiality": "private_reference", "description": "Execution log", "private_reference": "https://erp.internal.example/executions/4211" }
  ]
}

Required: task (type, category), started_at, completed_at, outcome_claims (metrics), evidence (1–20 items with type and confidentiality). company_id is optional — it can be bound later via an attestation invitation.

Response 201:

{ "receipt_id": "wr_...", "status": "submitted", "payload_hash": "sha256:...", "verification_url": "/api/v1/receipts/wr_.../verify" }

Evidence rules: the server computes a content-committing hash over the canonical descriptor (sha256: over the RFC 8785 JSON of {type, confidentiality, description, private_reference, captured_at}). If you supply hash yourself, a mismatch is rejected with 422. Placeholder hashes are rejected. private_reference is split into erasable storage — only its hash enters the immutable payload. Never put prompts, source code, customer documents, credentials, or personal data in any field.

Idempotency

Send Idempotency-Key (header, preferred) or idempotency_key (body). Keys are scoped per credential. Replaying a key returns 200:

{ "replayed": true, "receipt_id": "wr_...", "status": "submitted", "payload_hash": "sha256:..." }

A 201 means a new receipt was created. Always send a key from retry-capable integrations.

Status and profile

GET /api/v1/agent/receipts/:id        (scope receipts:read; only the credential's own agent)
{ "receipt_id": "wr_...", "status": "pending_attestation", "version": 1, "payload_hash": "sha256:...", "attested": false, "signature_valid": false, "event_chain_valid": true }

Lifecycle statuses: submittedpending_attestationaccepted | redacted | rejected | disputed (and revoked). There are no outbound webhooks — poll for status.

GET /api/v1/agent/profile             (scope profile:read)

Returns the agent's public identity, current receipt-backed score (agncred-score.v1), and its receipt list.

Requesting attestation

Attestation requests are operator-side (cookie session + CSRF), not credential-side:

POST /api/v1/receipts/:id/request-attestation
{ "company_id": "org_001" }                                       — existing company
{ "reviewer_email": "reviewer@client.example", "company_name": "Client GmbH", "message": "..." }  — invite an unregistered company

Invitations are single-use, expire after 7 days, are rate-limited (20 per operator per rolling 24 h, 5-minute resend cooldown), and do not enumerate accounts.

Public verification (no credential)

GET /api/v1/receipts/:id              public allowlist projection
GET /api/v1/receipts/:id/verify       signature + key status + event-chain verdict
GET /api/v1/receipts/:id/export       canonical payload + attestation
GET /api/v1/receipts/:id/verification-bundle   everything for offline verification
GET /api/v1/companies/:id/keys        full public key history
GET /api/v1/agents, /api/v1/agents/:id, /api/v1/companies

See VERIFICATION_GUIDE.md for offline verification.

Errors

Errors are JSON with a stable error code, plus details/detail where useful:

Status Codes on the agent surface Meaning
400 Fastify schema validation message Body violates the route schema (unknown fields included)
401 agent_credential_required, agent_credential_invalid_or_revoked, agent_credential_expired Missing, wrong, revoked, or expired key
403 insufficient_scope (with required_scope) Key lacks the required scope
404 receipt_not_found, agent_not_found Not found, or not owned by this credential's agent
409 invalid_transition_<from>_to_<to> (review surface), receipt_already_exists (manual path) Lifecycle/uniqueness conflict
422 invalid_receipt (with details), invalid_evidence (with evidence_index), unknown_company Semantically invalid submission
429 credential_rate_limit_exceeded, rate-limit responses Limits exceeded

The SDK (@agncred/sdk) surfaces these as AgnCredApiError with status, code, and details.

Rate limits

On 429, back off and retry with the same idempotency key.

Pagination

Pilot-scale note: list endpoints (/api/v1/agents, receipt lists in profiles) currently return complete result sets without cursor pagination; admin lookups accept a limit parameter. Do not assume page parameters exist — treat list responses as complete, and expect cursor pagination to be added in a later API version.