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: submitted → pending_attestation → accepted | 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
- Global: 300 requests/min per IP.
- Agent routes: 240/min per IP, plus 120/min per credential (enforced after authentication).
- Auth/token-minting routes: 10/min per IP; session exchange 20/min; invitation routes 10–30/min.
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.