# Invoance developer docs # Developer overview Invoance is cryptographic proof infrastructure. You POST events, documents, AI attestations, and audit-log events to one API; the backend signs each with a per-tenant Ed25519 key, stores it in an append-only ledger, and exposes verification endpoints so anyone can confirm a record without trusting Invoance. ## Base URL `https://api.invoance.com/v1` ## What you can build - **Event Ledger** - record any business event (who did what, when) as an immutable, signed record. - **Documents** - anchor a file's SHA-256 hash (optionally storing the original, encrypted) and prove it later. - **AI Attestations** - bind a model's input and output into one signed record. - **Traces** - group related records into a sealed sequence with a composite hash. - **Audit Logs** - signed, independently verifiable activity logs you hand to your own customers, with a hosted viewer, SIEM streaming, export, and offline verification. ## Authentication Every endpoint takes an API key as a Bearer token: `Authorization: Bearer invoance_live_xxx`. Keys are organization-scoped and carry scopes (`read` / `write`, and `audit:read` / `audit:write` for Audit Logs). ## Quickstart ```bash pip install invoance # Python npm install invoance # Node ``` ```python from invoance import InvoanceClient async with InvoanceClient() as client: # reads INVOANCE_API_KEY result = await client.events.ingest( event_type="user.signed_in", payload={"user_id": "user_123"}, ) print(result.event_id) ``` ## Where to go next - Core cryptographic concepts: https://www.invoance.com/developers/concepts - API endpoints reference: https://www.invoance.com/developers/endpoints - Audit Logs and the invoance.audit/1 schema: https://www.invoance.com/developers/audit - SDKs and libraries: https://www.invoance.com/developers/sdks --- # Core cryptographic concepts Every Invoance record rests on four primitives. Understanding them is enough to verify anything Invoance produces, independently. ## Hashing (SHA-256) Each payload is fingerprinted with SHA-256: a fixed 256-bit digest where any change to the input changes the output completely. Invoance stores the hash (and, for documents, optionally the original bytes, encrypted). Recomputing the hash of the original data and comparing it is how you prove the data is the same bytes that were anchored. ## Canonicalization Before signing, the payload is converted to a deterministic canonical form (stable key order, normalized timestamps, scalars-only for audit metadata) so the exact bytes that were signed can be rebuilt later from the stored fields. Verification is reconstruction-based: it rebuilds the canonical bytes and checks them, so determinism is essential. This is also why audit metadata values are scalars only - floats serialize differently across machines and would break a byte-exact rebuild. ## Signing (Ed25519) The canonical bytes are signed with an Ed25519 private key that belongs to your tenant. The matching public key is published, so anyone can confirm a record was produced by your tenant's key and has not been altered. Each tenant's private seed is encrypted at rest; keys are per-tenant, never shared. ## Verification To verify a record: recompute the hash, rebuild the canonical signed bytes from the stored fields, and check the Ed25519 signature against the pinned tenant public key. If anything changed, the hash or signature no longer matches. This needs no trust in Invoance, and for audit events it can run fully offline in the SDK. ## Immutability and sequencing Audit events are append-only: the database blocks UPDATE and DELETE (deletions happen only through the retention path). Each event also gets a gap-free per-org sequence number, so a missing number signals a deletion. Together, signatures catch modification and the sequence catches removal. --- # Authentication The Invoance API uses organization-scoped API keys to authenticate requests. Keys are hashed before storage; the plaintext is never persisted after issuance. ## API keys All protected endpoints require a valid API key passed as a Bearer token. Keys are scoped to a single organization and grant access based on their assigned scope. ``` Authorization: Bearer invoance_live_XXXXXXXXXXXXXXXXXXXXXXXX # Alternative header (both accepted, only one required) X-API-Key: invoance_live_XXXXXXXXXXXXXXXXXXXXXXXX ``` ## Key format ``` invoance_live_XXXXXXXXXXXXXXXXXXXXXXXX ``` - **Opaque** - keys are randomly generated and contain no derivable information about the tenant or scope. - **Hashed at rest** - the plaintext key is shown once at creation. Invoance stores only a secure hash; it cannot be recovered. - **Non-transferable** - keys are bound to a single organization and cannot be used across tenants. - **Immediately revocable** - revoked keys are rejected on the next request with no propagation delay. ## Scopes Each API key is assigned one or more scopes at creation time. Requests using a key without the required scope are rejected with `403 Forbidden`. - `read` - required for all retrieval endpoints (fetch ledger entries, attestations, and event records). Example: `GET /v1/document/:id`, `GET /v1/ai/attestations/:id`, `GET /v1/events/:id`. - `write` - required for all ingestion endpoints (anchor documents, create attestations, record events). Example: `POST /v1/document/anchor`, `POST /v1/ai/attestations`, `POST /v1/events`. Audit Logs uses its own scopes, `audit:read` and `audit:write`, distinct from the ledger read/write scopes above. ## Enforcement - **HTTPS only** - all requests must be made over HTTPS. Plain HTTP is rejected immediately. - **Org-scoped** - keys are scoped to a single organization. Cross-tenant access is not possible. - **Immediate revocation** - revoked or inactive keys are rejected on the next request with no cache delay. - **IP allowlisting** - optional per-key IP allowlists can be configured from the dashboard. - **Rate limiting** - all API key requests are subject to per-plan rate limits. Exceeded limits return `429`. ## Unauthenticated endpoints Public verification endpoints do not require an API key. They are read-only, rate-limited, and designed for independent third-party verification of anchored records. ``` GET https://invoance.com/proof/event/{event_id} GET https://invoance.com/proof/document/{event_id} GET https://invoance.com/proof/ai/{attestation_id} GET https://invoance.com/.well-known/invoance-platform-key ``` ## Related - Endpoints: https://www.invoance.com/developers/endpoints - Errors: https://www.invoance.com/developers/errors --- # Create an API key API keys authenticate every request. Create and manage them in the dashboard. ## Create a key 1. Sign in at app.invoance.com and open Settings, then API & Access. 2. Click Create key, name it (for example "production"), and choose its scopes. 3. Copy the key immediately - it is shown once and stored only as a hash. ``` invoance_live_XXXXXXXXXXXXXXXXXXXXXXXX ``` ## Scopes Grant the least privilege a key needs: - `read` - retrieval endpoints (events, documents, attestations). - `write` - ingestion endpoints (anchor, attest, record). - `audit:read` - read, verify, and export Audit Logs. - `audit:write` - send audit events and mint portal links. A request with a key that lacks the required scope returns `403 insufficient_scope`. ## IP allowlisting Optionally pin a key to one or more IP ranges from the dashboard. Requests from other addresses are rejected. ## Use the key ```bash export INVOANCE_API_KEY="invoance_live_xxx" curl https://api.invoance.com/v1/events -H "Authorization: Bearer $INVOANCE_API_KEY" ``` The SDKs read `INVOANCE_API_KEY` from the environment automatically. ## Rotate and revoke Keys do not expire on their own. To rotate, create a new key, deploy it, then revoke the old one - revocation takes effect on the next request with no cache delay. Keep keys server-side; never ship them in client code. --- # API endpoints RESTful endpoints for anchoring, attesting, and retrieving immutable records. All writes are append-only; all reads are deterministic. Endpoints are grouped by resource; each group has its own reference page with request and response samples. ## Base URL `https://api.invoance.com/v1` ## Global contract - Transport: HTTPS only; HTTP is rejected. - Authentication: required on all read and write endpoints. - Canonicalization: the canonical payload is computed server-side before signing. - Idempotency: required on write endpoints via the `Idempotency-Key` header. ``` Authorization: Bearer invoance_live_xxx Idempotency-Key: 7f4c1c9d-5b8a-4d42-9e0b-xxxxxxxx Content-Type: application/json ``` ## Endpoint groups - Key Introspection (1 endpoint): https://www.invoance.com/developers/endpoints/me - Documents (5 endpoints): https://www.invoance.com/developers/endpoints/documents - AI Attestations (8 endpoints): https://www.invoance.com/developers/endpoints/ai-attestations - Event Ledger (4 endpoints): https://www.invoance.com/developers/endpoints/events - Traces (5 endpoints): https://www.invoance.com/developers/endpoints/traces - Audit Logs (20 endpoints): https://www.invoance.com/developers/endpoints/audit --- # Key Introspection endpoints - `GET /v1/me` - validate the caller's API key (any scope) and see who it belongs to: organization (name, domain, plan tier), tenant, the key's own metadata (prefix, last4, scopes, timestamps), and its effective rate limit. A 200 proves the key authenticates; the SDKs' validate() targets this endpoint. Never returns sibling keys, dashboard users, billing internals, or IP rules. Responses carry `Cache-Control: no-store`. --- # Documents endpoints - `POST /v1/document/anchor` - anchor a document hash (optionally store original bytes; optional trace_id). A duplicate hash returns 409. - `GET /v1/document` - list document events (paginated, filterable). - `GET /v1/document/:event_id` - retrieve a document event with signatures and metadata. - `GET /v1/document/:event_id/original` - download the original bytes, if stored. - `POST /v1/document/:event_id/verify` - compare a submitted hash against the anchored hash. --- # AI Attestations endpoints - `POST /v1/ai/attestations` - submit input/output; Invoance hashes, signs, and stores an immutable attestation. - `GET /v1/ai/attestations` - list attestations (paginated, filterable). - `GET /v1/ai/attestations/:attestation_id` - retrieve an attestation with proof metadata and signature. - `GET /v1/ai/attestations/:attestation_id/raw` - the exact canonical payload that was hashed and signed. - `POST /v1/ai/attestations/:attestation_id/verify` - compare a hash against input/output/payload hashes. - `GET /v1/proof/ai/:attestation_id` - public proof record (no API key required). - `POST /v1/proof/ai/:attestation_id/verify` - public hash + signature verification (no API key required). - `GET /v1/keys/:domain` - public tenant signing key for independent verification. --- # Event Ledger endpoints - `POST /v1/events` - record any business event (who/what/when), signed at ingestion. - `GET /v1/events` - list events (paginated, filterable). - `GET /v1/events/:event_id` - retrieve an immutable event with all hashes. - `POST /v1/events/:event_id/verify` - verify a hash or raw payload against the anchored event. --- # Traces endpoints - `POST /v1/traces` - create a trace (container for related items; optional metadata up to 16 KB). - `GET /v1/traces` - list traces (filter by status / date). - `GET /v1/traces/:trace_id` - retrieve a trace with linked items and seal metadata. - `POST /v1/traces/:trace_id/seal` - seal: compute a composite hash and sign the bundle (202; poll). - `GET /v1/traces/:trace_id/proof` - the full proof bundle for a sealed trace. --- # Audit Logs endpoints ## Orgs and lifecycle - `POST /v1/audit/orgs` - create an audit org (your end customer). - `PATCH /v1/audit/orgs/:id` - rename an org; pass `"name": null` to clear (display-only, never affects signed data). - `GET /v1/audit/orgs` - list orgs; archived orgs are excluded unless `?include_archived=true`. Org objects carry `archived_at`. - `GET /v1/audit/orgs/:id/integrity` - sequence-gap integrity scan. - `PUT /v1/audit/orgs/:id/retention` - set retention (clamped to your plan). - `POST /v1/audit/orgs/:id/archive` - freeze new activity (ingest/streams/portal/exports return 409 org_archived); history stays readable and verifiable; retention keeps running. Idempotent, reversible; archived orgs still count toward the plan org cap. - `POST /v1/audit/orgs/:id/unarchive` - resume everything. Idempotent. - `DELETE /v1/audit/orgs/:id` - hard delete, only when nothing signed would be destroyed (never-ingested org, or archived with retention fully purged); otherwise 409 org_not_deletable. Frees the plan-cap slot and the organization_id. ## Events - `POST /v1/audit/events` - send a signed audit event (Idempotency-Key required). - `GET /v1/audit/events` - keyset-paginated list with action / actor / target / date filters. - `GET /v1/audit/events/:id` - retrieve an event. - `GET /v1/audit/events/:id/verify` - pinned-key signature verification. ## Streams, portal, exports, integrations - `POST/GET /v1/audit/orgs/:id/streams`, `POST .../streams/:id/test`, `DELETE .../streams/:id` - SIEM webhook streams. - `POST /v1/audit/portal_sessions` - mint a one-time hosted-viewer link. - `POST /v1/audit/exports` and `GET /v1/audit/exports/:id` - async CSV/NDJSON export plus presigned download. - `POST /v1/integrations/webhooks/:token` - inbound receiver for zero-code provider ingest (Clerk, Auth0); signature-verified, no API key. ### Send an audit event ```bash curl -X POST https://api.invoance.com/v1/audit/events \ -H "Authorization: Bearer invoance_live_xxx" \ -H "Idempotency-Key: " \ -H "Content-Type: application/json" \ -d '{ "organization_id": "org_01J8F3KQ2R7VWX9YB4ND6MCZAH", "action": "user.signed_in", "occurred_at": "2026-06-24T12:00:00.000Z", "actor": { "type": "user", "id": "user_123" }, "targets": [] }' ``` See the Audit Logs guide at https://www.invoance.com/developers/audit for the full invoance.audit/1 schema. --- # API error reference Errors return a standard HTTP status and a JSON body with a machine-readable code, a human message, and, where relevant, the offending field. ```json { "error": "insufficient_scope", "message": "This key cannot write audit events." } ``` ## Status codes - `400 Bad Request` - malformed or invalid request. For Audit Logs this includes an envelope that is not valid invoance.audit/1: an unknown field, a missing required field, a wrong type, a duplicate JSON key, a float in metadata, or an occurred_at outside the allowed window. - `401 Unauthorized` - missing or invalid API key. - `403 Forbidden` - the key is valid but lacks the required scope (insufficient_scope), or is IP-restricted. - `404 Not Found` - the resource does not exist or belongs to another tenant. The same 404 covers both so ids cannot be probed across tenants. - `409 Conflict` - a duplicate, e.g. anchoring the same document hash twice, or creating an org with an existing organization_id. - `422 Unprocessable Entity` - syntactically valid but semantically wrong (missing field, invalid type, domain mismatch). - `429 Too Many Requests` - rate limit exceeded; honor the Retry-After header. - `5xx` - a server error; safe to retry idempotent requests with the same Idempotency-Key. ## Idempotency and retries Write endpoints take an `Idempotency-Key` header. Retrying with the same key returns the original result instead of creating a duplicate. Audit ingest requires the header; the SDKs derive one from the event content. ## Common audit errors - `invalid_organization_id`, `org_exists` - org creation. - `too_many_orgs` (403) - the plan's audit-org cap is reached; delete an unused org or upgrade. - `org_archived` (409) - the org is archived and accepts no new activity; unarchive to resume. - `org_not_deletable` (409) - the org has signed history retention hasn't purged, or recently accepted events may still be in the signing queue; archive it and it becomes deletable once all data ages out. - `insufficient_scope` - the key lacks audit:read or audit:write. - `invalid_intent`, `invalid_duration` - portal link parameters. - `payload_hash_mismatch`, `signature_invalid` - returned by verify when a stored event has been tampered with. --- # Audit Logs A signed, independently verifiable activity log for your product. Send events over the API and each one is signed with your tenant's Ed25519 key and given a gap-free sequence number, so tampering or deletion shows up as a broken signature or a missing sequence. Hand your end customers a hosted viewer, stream events to a SIEM, export a range, and verify any event without trusting our answer. Base URL: `https://api.invoance.com/v1` ## Before you begin The audit API uses its own scopes, distinct from the ledger scopes: a key needs `audit:write` to send events and `audit:read` to read, verify, and export. Grant them on an API key in Settings. ## Quick start ### 1. Create an org Each of your end customers is an *org*, addressed by your own `organization_id`. Create it once; events reference it by that id. ```bash curl -X POST https://api.invoance.com/v1/audit/orgs \ -H "Authorization: Bearer invoance_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "organization_id": "org_01J8F3KQ2R7VWX9YB4ND6MCZAH", "name": "Acme Production" }' ``` ### 2. Send an event Post an event with an `action`, an `actor`, and optional targets. An `Idempotency-Key` header is required; the SDKs generate one for you. ```bash curl -X POST https://api.invoance.com/v1/audit/events \ -H "Authorization: Bearer invoance_live_xxx" \ -H "Idempotency-Key: 7f4c1c9d-5b8a-4d42-9e0b-1f2a3b4c5d6e" \ -H "Content-Type: application/json" \ -d '{ "organization_id": "org_01J8F3KQ2R7VWX9YB4ND6MCZAH", "action": "user.signed_in", "occurred_at": "2026-06-24T12:00:00.000Z", "actor": { "type": "user", "id": "user_123", "name": "Ada Lovelace" }, "targets": [{ "type": "team", "id": "t_eng" }] }' ``` ### 3. Hand off a hosted viewer Mint a one-time link to a read-only, org-scoped viewer your customer can open with no Invoance account. Set `intent` to `audit_logs` for the event viewer or `log_streams` for the stream-config screen. ### 4. Verify an event `GET /v1/audit/events/{id}/verify` rebuilds the canonical bytes and checks the Ed25519 signature against your tenant's pinned key, returning `{ valid, reason, key_source }`. ## Event schema (invoance.audit/1) Every event uses the frozen `invoance.audit/1` envelope: a fixed set of fields, fixed types, within the limits below. An unknown top-level field, a missing required field, or a wrong type is rejected `400` and never stored or signed. The contents of `metadata` are free-form within the size limits. Schema id: `invoance.audit/1` · version: `1` (reserved; if you send a version it must equal 1). ### A complete event This is exactly what you POST to `/v1/audit/events`. ```json { "action": "user.signed_in", "occurred_at": "2026-06-24T12:00:00.000Z", "actor": { "type": "user", "id": "user_42", "name": "Ada Lovelace" }, "targets": [{ "type": "team", "id": "team_eng" }], "context": { "location": "203.0.113.10", "user_agent": "Chrome/124.0.0.0" }, "metadata": { "plan": "growth" } } ``` ### Fields | Field | Type | Required | Notes | |---|---|---|---| | `action` | string | Yes | Dotted resource.verb, e.g. user.signed_in. | | `occurred_at` | string (RFC3339 UTC) | Yes | When it happened, on your clock. Bounded to now - 5y to now + 24h. SDKs default to now. | | `actor` | object | Yes | type (user / api_key / system) and id required; name and metadata optional. | | `targets` | array of object | Yes | May be empty ([]) but the field must be present. Each: type, id, optional name and metadata. | | `context` | object | No | location = actor IP, user_agent = client string. | | `metadata` | object | No | Flat key to scalar map. Free-form contents. | | `version` | integer | No | Reserved. Must equal 1 if present. | Absent optional fields, or fields sent as `null`, are omitted from the signed bytes entirely. The server assigns `id` (`aevt_`), `org_id`, `ingested_at`, `seq`, and `schema_id`. ### Limits and rules - metadata (and actor / target metadata): up to 50 keys, key up to 40 chars, value up to 500 chars. - metadata values are scalars only: string, boolean, or int64. Floats and nested objects/arrays are rejected 400 (send decimals as strings). - The Idempotency-Key header is required on ingest; the SDKs derive one from the event content. - Ids: orgs are `aorg_`, events are `aevt_`. What gets signed: the canonical bytes of the present fields are signed with your tenant's Ed25519 key. The verify endpoint and the SDK offline verifier rebuild those exact bytes and check the signature against your pinned key. The signature attests `ingested_at` (the server clock), not the client-supplied `occurred_at`. ## Using the SDKs The Python and Node SDKs expose the same surface under `client.audit`. They default `occurred_at` to now, generate the idempotency key, and ship an offline verifier that needs no network call. See the Audit Logs SDK reference at https://www.invoance.com/developers/audit/sdk. ## Embed the viewer The hosted viewer also ships as a React component: `@invoance/audit-viewer` renders the same signed event table, filters, export, and in-browser Ed25519 tamper test inside your own product. Native DOM, no iframe; prefixed stylesheet themed by CSS variables. ```bash npm install @invoance/audit-viewer ``` Your backend mints a short-lived portal session for the signed-in customer. Server-side only: the API key must never reach the browser; only the one-time token does. ```ts // A route in YOUR backend (server-side only; never expose the API key) const session = await client.audit.portalSessions.create({ organization_id: currentCustomerOrgId, intent: "audit_logs", session_duration_seconds: 3600, }); return { token: session.token }; ``` ```tsx import { AuditLogViewer } from "@invoance/audit-viewer"; import "@invoance/audit-viewer/styles.css"; { const r = await fetch("/api/audit-portal-token", { method: "POST" }); return (await r.json()).token; }} /> ``` The component exchanges the token for an org-scoped session and re-invokes your callback on expiry. Sessions minted with `intent: "log_streams"` render the stream-destination screen instead. ## Exporting events `POST /v1/audit/exports` queues an async CSV or NDJSON export with optional filters (actions, actor_id, target_id, range_start, range_end). Poll `GET /v1/audit/exports/{id}` until the status is `ready`, then download the presigned URL. ## Endpoint reference - Orgs: `POST/GET /v1/audit/orgs`, `PATCH /v1/audit/orgs/{id}` (rename), `GET /v1/audit/orgs/{id}/integrity`, `PUT /v1/audit/orgs/{id}/retention`, `POST .../{id}/archive` and `.../{id}/unarchive`, `DELETE /v1/audit/orgs/{id}` (only when nothing signed would be destroyed) - Events: `POST /v1/audit/events`, `GET /v1/audit/events`, `GET /v1/audit/events/{id}`, `GET /v1/audit/events/{id}/verify` - Streams: `POST/GET /v1/audit/orgs/{id}/streams`, `POST .../{id}/test`, `DELETE .../{id}` - Portal and exports: `POST /v1/audit/portal_sessions`, `POST /v1/audit/exports`, `GET /v1/audit/exports/{id}` --- # Audit Logs SDK The audit namespace lives under `client.audit`. It defaults `occurred_at` to now, generates the required idempotency key for you, and ships an offline verifier that needs no network call. A key needs `audit:write` to send events and `audit:read` to read, verify, and export. ## Install ```bash pip install invoance # Python npm install invoance # Node ``` ## Methods ### Send an event ```python ev = await client.audit.events.ingest( organization_id="org_01J8F3KQ2R7VWX9YB4ND6MCZAH", action="user.signed_in", actor={"type": "user", "id": "user_123"}, ) print(ev["event_id"]) ``` ```ts const ev = await client.audit.events.ingest({ organizationId: "org_01J8F3KQ2R7VWX9YB4ND6MCZAH", action: "user.signed_in", actor: { type: "user", id: "user_123" }, }); console.log(ev.event_id); ``` ### Get an event ```python event = await client.audit.events.get("aevt_01J...") print(event["action"], event["seq"]) ``` ### List events Keyset-paginated, newest first, with action / actor / target / date filters. ```python page = await client.audit.events.list( organization_id="org_01J8F3KQ2R7VWX9YB4ND6MCZAH", actions="user.signed_in", limit=50 ) for e in page["events"]: print(e["id"], e["action"]) ``` ### Verify an event offline Reconstruct the canonical signed bytes and check the Ed25519 signature locally, with no trust in the server's answer. ```python from invoance import verify_audit_event event = await client.audit.events.get("aevt_01J...") result = verify_audit_event(event) # offline, no network call print(result.valid) # True ``` ```ts import { verifyAuditEvent } from "invoance"; const event = await client.audit.events.get("aevt_01J..."); const result = verifyAuditEvent(event); // offline, no network call console.log(result.valid); // true ``` ### Export events Queue an async CSV or NDJSON export, then poll for the download URL. ```python job = await client.audit.exports.create(organization_id="org_01J8F3KQ2R7VWX9YB4ND6MCZAH", format="csv") status = await client.audit.exports.get(job["id"]) # poll until "ready" print(status["status"], status.get("download_url")) ``` See the Audit Logs quick start at https://www.invoance.com/developers/audit for request/response shapes, the hosted-viewer hand-off, SIEM streaming, and the endpoint reference. --- # Audit Log Event Schema (invoance.audit/1) Every event you send to `POST /v1/audit/events` uses the frozen `invoance.audit/1` envelope: a fixed set of fields, fixed types, within the limits below. The envelope is strict and identical for every customer, so an unknown top-level field, a missing required field, or a wrong type is rejected `400` and never stored or signed. What is not constrained is the contents of `metadata`: you send whatever key/values you want within the size limits, with no pre-declaration. ## A complete event Exactly what you send, with every field populated: an actor with its own metadata, two targets (one with metadata), request context, and top-level metadata using all three allowed scalar types (string, integer, boolean). The server adds `id`, `org_id`, `seq`, `ingested_at` and `schema_id`, then signs it. ```json { "action": "team.member.invited", "occurred_at": "2026-06-24T12:00:00.000Z", "actor": { "type": "user", "id": "user_42", "name": "Ada Lovelace", "metadata": { "role": "admin", "mfa": true } }, "targets": [ { "type": "user", "id": "user_99", "name": "Charles Babbage", "metadata": { "invited_email": "charles@acme.com" } }, { "type": "team", "id": "team_eng", "name": "Engineering" } ], "context": { "location": "203.0.113.10", "user_agent": "Chrome/124.0.0.0" }, "metadata": { "plan": "growth", "seats": 25, "trial": false }, "version": 1 } ``` Schema id `invoance.audit/1`, version `1` (reserved; if you send a `version` it must equal 1). ## Fields | Field | Type | Required | Notes | | --- | --- | --- | --- | | action | string | Yes | Dot-segmented resource.verb, e.g. user.signed_in or team.member.invited. | | occurred_at | string (RFC3339 UTC) | Yes | When it happened, on your clock. Bounded to now - 5y to now + 24h. SDKs default this to now. | | actor | object | Yes | Who did it: type (user, api_key, system) and id required; name and metadata optional. | | targets | array of object | Yes | What it was done to. May be empty ([]) but the field must be present. Each: type, id, optional name and metadata. | | context | object | No | location = actor IP, user_agent = client string. | | metadata | object | No | Flat key to scalar map (see limits). Free-form contents. | | version | integer | No | Reserved. Must equal 1 if present. | Absent optional fields, or fields sent as null, are omitted from the signed bytes entirely, so the signature covers exactly the fields present. The server assigns id (aevt_), org_id, ingested_at, seq and schema_id; client-sent values for those are ignored. ## Limits and rules - metadata (and actor / target metadata): up to 50 keys, key up to 40 chars, value up to 500 chars. - metadata values are scalars only: string, boolean, or int64. Floats and nested objects/arrays are rejected 400 (send decimals as strings). - Idempotency-Key header is required on ingest; the SDKs derive one from the event content. - Ids: orgs are aorg_, events are aevt_. ## What gets signed The canonical bytes of the present fields are signed with your tenant's Ed25519 key. The `/verify` endpoint and the SDK offline verifier rebuild those exact bytes and check the signature against your pinned key. The signature attests `ingested_at` (our server clock), not the client-supplied `occurred_at`. See the Audit Logs quick start at https://www.invoance.com/developers/audit and how verification works at https://www.invoance.com/developers/verification. --- # Exporting Audit Log Events Exports are asynchronous. Create a job with the same filters you would pass to the list endpoint, poll until its status is `ready`, then download from the short-lived presigned URL. Choose `csv` for spreadsheets or `ndjson` for line-delimited JSON. A single export spans both hot and cold-storage rows, so an old range comes back whole. ## 1. Create the export ```bash curl -X POST https://api.invoance.com/v1/audit/exports \ -H "Authorization: Bearer invoance_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "organization_id": "org_01J8F3KQ2R7VWX9YB4ND6MCZAH", "format": "csv", "filters": { "actions": "user.signed_in", "range_start": "2026-06-01T00:00:00Z", "range_end": "2026-06-30T23:59:59Z" } }' ``` Response: `{ "id": "aexp_01J...", "status": "pending", "format": "csv" }` ## 2. Poll until ready ```bash curl https://api.invoance.com/v1/audit/exports/aexp_01J... \ -H "Authorization: Bearer invoance_live_xxx" ``` When ready, the response includes a short-lived presigned `download_url`: ```json { "id": "aexp_01J...", "status": "ready", "format": "csv", "row_count": 4192, "download_url": "https://...r2.cloudflarestorage.com/..." } ``` ## 3. Download the file ```bash curl -L "https://...r2.cloudflarestorage.com/..." -o audit-export.csv ``` ## With the Python SDK ```python import asyncio from invoance import InvoanceClient async with InvoanceClient() as client: job = await client.audit.exports.create( organization_id="org_01J8F3KQ2R7VWX9YB4ND6MCZAH", format="csv", filters={"actions": "user.signed_in"}, ) while True: status = await client.audit.exports.get(job["id"]) if status["status"] in ("ready", "failed"): break await asyncio.sleep(2) print(status["status"], status.get("download_url")) ``` ## Good to know - Filters mirror `GET /v1/audit/events`: actions, actor_id, target_id, and an occurred_at range. - The download_url is presigned and short-lived. Re-poll the job to mint a fresh one if it expires. - Exports are read-only and never billed; reads and verification are always free. See the full endpoint reference at https://www.invoance.com/developers/endpoints. --- # Embeddable audit log viewer (@invoance/audit-viewer) A React component that puts the audit log inside your own product (for example admin.yourapp.com/audit): the same signed event table, filters, CSV/JSON export, and in-browser Ed25519 tamper test as the hosted portal. Native DOM, no iframe; the compiled stylesheet is prefixed and themed by CSS variables, and verification runs in the end-user's browser, so the proof never depends on trusting the page that renders it. ## Before you begin - An API key with the audit scopes and at least one audit org receiving events. - React 18 or newer. No other runtime dependencies; Tailwind is not required. ## 1. Install ```bash npm install @invoance/audit-viewer ``` ## 2. Add a token endpoint to your backend The component authenticates with a short-lived, org-scoped portal session your backend mints for whichever of YOUR customers is signed in. Server-side only: the Invoance API key must never reach the browser; the one-time token is useless for anything except opening this one organization's viewer. ```ts // app/api/audit-portal-token/route.ts (Next.js example; any backend works) import { InvoanceClient } from "invoance"; const client = new InvoanceClient({ apiKey: process.env.INVOANCE_API_KEY }); export async function POST() { const organizationId = await currentCustomerOrgId(); // however your app resolves it const session = await client.audit.portalSessions.create({ organization_id: organizationId, intent: "audit_logs", session_duration_seconds: 3600, }); return Response.json({ token: session.token }); } ``` Every SDK exposes the same call; in Python it is `client.audit.portal_sessions.create(...)`. Keep `session_duration_seconds` short: the component re-mints through your endpoint automatically on expiry. ## 3. Render the component ```tsx "use client"; import { AuditLogViewer } from "@invoance/audit-viewer"; import "@invoance/audit-viewer/styles.css"; export default function AuditPage() { return ( { const r = await fetch("/api/audit-portal-token", { method: "POST" }); return (await r.json()).token; }} /> ); } ``` getPortalToken is a callback, not a static prop, so expiry never strands the visitor. The package ships a "use client" banner, so Next.js App Router pages can import it directly; in Vite or CRA mount it anywhere. ## 4. Verify it works Open the page as a signed-in customer: their organization's events render with your issuer branding. Click a row and press Verify signature (verdict: Authentic), then add a character to any field and verify again (verdict: Tampered), computed entirely in the browser. ## Props | Prop | Default | What it does | |---|---|---| | `getPortalToken` | - | Async callback returning a fresh one-time portal token; re-invoked on expiry | | `tokenFromUrl` | false | Read and strip a one-time ?token= from the page URL (hosted-page style) | | `persistSession` | false | Cache the session JWT in sessionStorage across refreshes | | `baseUrl` | https://api.invoance.com | API origin | | `pageSize` | 25 | Initial rows per page (25, 50, or 100) | | `autoRefresh` | true | Re-pull page 1 every 15 seconds while idle | | `theme` | - | CSS-variable overrides, e.g. { variables: { background: "240 6% 12%" } } | | `className` | - | Extra class(es) on the root element | | `onSessionExpired` | - | Called when the session cannot be renewed | ## Theming Nine CSS variables (HSL triples) drive every color: --inv-background, --inv-foreground, --inv-border, --inv-muted, --inv-popover, --inv-popover-foreground, --inv-success, --inv-danger, --inv-warning. Override on :root, any ancestor, or via the theme prop (bare names get the --inv- prefix automatically). ## Sessions and failure modes - The exchanged JWT is org-scoped and portal traffic is rate-limited per visitor IP; it never counts against your API budget. - On a 401 the component re-invokes getPortalToken once and retries; if that fails it shows an expiry message and calls onSessionExpired. - A rate-limited exchange retries automatically honoring Retry-After; a rate-limit rejection does not consume the one-time link. - Browsers without Web Crypto Ed25519 still get the hash check; the signature is reported as not verifiable in this browser, distinct from a failed verification. - Events return context/metadata verbatim; whoever can render the embed can read them. Your tenant id is never exposed. Source: https://github.com/Invoance/invoance-audit-viewer --- # Zero-code audit log integrations Point your auth provider's webhooks at Invoance and every organization in your app starts accumulating signed, independently verifiable audit events (sign-ins, membership changes, role updates) before your team writes a line of instrumentation. Clerk and Auth0 are supported today; each has its own setup guide and curated event map. ## Providers - [Clerk](https://www.invoance.com/developers/audit/integrations/clerk): Svix-signed webhooks for organizations, memberships, roles, and sessions, with orgs auto-created under their real names. - [Auth0](https://www.invoance.com/developers/audit/integrations/auth0): log-stream webhooks for sign-ins, signups, password and email changes. Deliveries arrive batched and are deduplicated by log id. ## How it works 1. **Create the endpoint.** Dashboard → Audit Logs → Integrations → Connect provider. You get a unique webhook URL (shown once, stored hashed) plus provider-specific setup steps. 2. **Every delivery is verified.** Clerk deliveries carry a Svix signature; Auth0 sends the Authorization token you configured. Nothing is trusted before it verifies. 3. **Events become signed records.** Verified events are translated into a clean audit vocabulary, Ed25519-signed, and sealed into each organization's gap-free sequence, identical to events sent via the API. 4. **Organizations appear on their own.** Provider events carry the end-customer organization; unknown orgs are auto-created (up to your plan's limit) with their real names, each one a portal link away from being shown to your customer. ## Delivery semantics The receiver is built for at-least-once delivery: providers retry on failure, idempotency absorbs the retries, and permanent conditions are acknowledged with a 200 so a single odd event can never make your provider disable the endpoint. These rules are shared by every provider. | Response | Meaning | |---|---| | 200 received | Event mapped, signed, and sealed into the org's gap-free sequence. Clerk gets {received, event_id}; Auth0 batches get {received, accepted, skipped}. | | 200 skipped | Permanent, per-event condition: unmapped type, org-less event under the skip policy, org over your plan cap, or a paused source. Counted per source with a reason, visible in the dashboard's skip log. Never billed. | | 401 | Signature (Clerk) or Authorization token (Auth0) did not verify. Not counted as received. | | 404 | Unknown or deleted endpoint token. | | 429 | Per-source rate limit (with Retry-After), or your monthly signed-event allotment is exhausted; the provider's own retry becomes the backoff. | | 5xx | Transient failure on our side. The provider retries; per-event idempotency keys (Svix message id / Auth0 log id) guarantee retries never create duplicates. | **Quota:** only events that are actually signed count against your allotment; skipped events are free, always. **Org routing:** each source either auto-creates organizations as their events arrive (up to your plan's org limit) or runs in allowlist mode, accepting only organizations you created yourself. Org-less events are skipped or routed to a catch-all org you designate. Everything is switchable per source, live, from the dashboard. --- # Clerk integration Point Clerk's webhooks at Invoance and every organization in your app starts accumulating signed, independently verifiable audit events (sign-ins, membership changes, role updates) before your team writes a line of instrumentation. Every delivery is verified against its Svix signature before anything is trusted. ## Set up 1. Create the integration in Dashboard → Audit Logs → Integrations (provider: Clerk) and copy the webhook URL. It is shown once and stored hashed. 2. In the Clerk Dashboard, open Configure → Webhooks → Add endpoint and paste the URL. 3. Subscribe to the events you want from the table below; start with the core set. 4. Copy the endpoint's signing secret (`whsec_…`) into the integration. If you ever rotate it in Clerk, use Roll secret on the source; the URL stays the same and nothing else changes. ## Event map Verified events are translated into a clean audit vocabulary, Ed25519-signed, and sealed into each organization's gap-free sequence, identical to events sent via the API. Provider events carry the end-customer organization; unknown orgs are auto-created (up to your plan's limit) with their real names. | Clerk event | Becomes | Organization | Tier | |---|---|---|---| | `organizationMembership.created` | `member.added` | embedded organization (auto-creates it, with name) | core | | `organizationMembership.deleted` | `member.removed` | embedded organization | core | | `organizationMembership.updated` | `role.changed` (role kept in metadata) | embedded organization | core | | `organization.created` | `organization.created` | the organization itself | core | | `user.created` | `user.created` | none → org-less policy | core | | `user.deleted` | `user.deleted` | none → org-less policy | core | | `session.revoked` | `session.revoked` | session's active organization (nullable) | core | | `session.created` | `user.signed_in` | session's active organization (nullable) | high-volume | | `session.removed` | `user.signed_out` | session's active organization (nullable) | high-volume | | `user.updated` | `user.updated` | none → org-less policy | high-volume | **High-volume tier:** `session.created` fires on every sign-in, `session.removed` on sign-outs *and* session expiry, and `user.updated` on any User-object change including backend metadata writes. They count against your monthly signed-event allotment, so subscribe deliberately. `session.ended` is intentionally not mapped: a single sign-out fires both it and `session.removed`, which would double-count. Response codes, retry behavior, quota rules, and org routing are shared by every provider and documented under delivery semantics on the [Integrations overview](https://www.invoance.com/developers/audit/integrations). --- # Auth0 integration Stream Auth0 tenant logs to Invoance and every organization in your app starts accumulating signed, independently verifiable audit events (sign-ins, signups, password and email changes) before your team writes a line of instrumentation. Every delivery is verified against the Authorization token you configured. ## Set up 1. Create the integration in Dashboard → Audit Logs → Integrations (provider: Auth0). Auth0 doesn't issue a secret; you invent one, so hit Generate and copy both the URL and the token. 2. In the Auth0 Dashboard, open Monitoring → Streams → Create Stream → Custom Webhook. 3. Payload URL: your webhook URL. Content Type: `application/json`. Content Format: JSON Array. Authorization Token: the generated value. Auth0 sends it on every delivery and Invoance verifies it matches. 4. Pick the stream's category filters; only the codes below become events. ## Log-code map Verified log entries are translated into a clean audit vocabulary, Ed25519-signed, and sealed into each organization's gap-free sequence, identical to events sent via the API. | Log code | Auth0 meaning | Becomes | |---|---|---| | `s` | Successful login | `user.signed_in` | | `slo` | User successfully logged out | `user.signed_out` | | `f` / `fp` / `fu` | Failed login (generic / wrong password / unknown user) | `user.sign_in_failed` (exact code kept in metadata) | | `ss` | Successful signup | `user.created` | | `sdu` | User successfully deleted | `user.deleted` | | `scp` | Successful password change | `user.password_changed` | | `sce` | Successful email change | `user.email_changed` | | `limit_wc` / `limit_mu` | IP blocked after repeated failed logins | `account.blocked` (code kept in metadata) | Deliveries arrive batched; each entry is processed independently and deduplicated by its Auth0 log id, so replays and partial retries never create duplicates. Events made in an Auth0 Organization context carry `org_id` / `org_name` and route (and auto-create) accordingly; events without one follow your org-less policy. Sign-ins and failed logins are high-volume, so filter your stream categories with your allotment in mind. Response codes, retry behavior, quota rules, and org routing are shared by every provider and documented under delivery semantics on the [Integrations overview](https://www.invoance.com/developers/audit/integrations). --- # AI Attestations Bind a model's input, output, and identity into one signed, independently verifiable record. One API call after your model responds: Invoance computes three SHA-256 hashes (input, output, whole payload), signs a canonical statement with your tenant's Ed25519 key, and stores it in an append-only ledger. Every attestation gets a public proof page anyone can verify with no Invoance account. Base URL: `https://api.invoance.com/v1` ## Before you begin Attestations use the standard ledger scopes: a key needs `write` to create attestations and `read` to retrieve and verify them. Pass the key as `Authorization: Bearer invoance_live_xxx` (or `X-API-Key`). ## Quick start ### 1. Create an attestation POST the model's exact input and output plus the model identity. `type` is one of `output`, `decision`, or `approval`. An `Idempotency-Key` header is optional but recommended for safe retries. ```bash curl -X POST https://api.invoance.com/v1/ai/attestations \ -H "Authorization: Bearer invoance_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "type": "output", "payload": { "input": "Summarize the attached contract.", "output": "The contract grants a 12-month license..." }, "context": { "model_provider": "openai", "model_name": "gpt-4o", "model_version": "2024-11-20" }, "subject": { "user_id": "user_123", "session_id": "sess_456" } }' ``` Response `201`: ```json { "attestation_id": "9d2f1c4e-...", "created_at": "2026-07-04T12:00:00Z", "input_hash": "a1b2...", "output_hash": "c3d4...", "payload_hash": "e5f6...", "status": "accepted" } ``` Submitting a byte-identical body again returns `200` with the original `attestation_id` and `status: "duplicate"` — content-level dedup on the payload hash, independent of the idempotency header. ### 2. Retrieve it `GET /v1/ai/attestations/{attestation_id}` returns the full record: the three hashes, the exact signed bytes (`signed_payload`), the Ed25519 `signature`, `public_key`, model context, and your organization details. Persistence is asynchronous — a `201` means durably queued, so a GET immediately after may briefly 404. `GET /v1/ai/attestations/{attestation_id}/raw` returns the byte-exact canonical JSON that was hashed — hashing those bytes reproduces `payload_hash`. ### 3. Verify a hash ```bash curl -X POST https://api.invoance.com/v1/ai/attestations/9d2f1c4e-.../verify \ -H "Authorization: Bearer invoance_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "content_hash": "<64-hex SHA-256>" }' ``` The response says whether the hash matches and which field it matched (`attestation_hash`, `input_hash`, `output_hash`, or `payload_hash`). ### 4. Share a public proof Every attestation has a public, no-auth proof endpoint — hand the link to an auditor or end user: ``` GET https://api.invoance.com/v1/proof/ai/{attestation_id} POST https://api.invoance.com/v1/proof/ai/{attestation_id}/verify ``` The public verify checks the submitted hash and the Ed25519 signature against your tenant's registered (pinned) key. See https://www.invoance.com/developers/ai-attestations/verification. ## Using the SDKs Both SDKs expose attestations under `client.attestations`. ```python result = await client.attestations.ingest( attestation_type="output", input="Summarize the attached contract.", output="The contract grants a 12-month license...", model_provider="openai", model_name="gpt-4o", model_version="2024-11-20", subject={"user_id": "user_123"}, ) print(result.attestation_id) ``` ```ts const result = await client.attestations.ingest({ type: "output", input: "Summarize the attached contract.", output: "The contract grants a 12-month license...", modelProvider: "openai", modelName: "gpt-4o", modelVersion: "2024-11-20", subject: { userId: "user_123" }, }); console.log(result.attestation_id); ``` Full method reference: https://www.invoance.com/developers/ai-attestations/sdk. ## Limits and quotas - Whole request body up to 1 MB; `subject` up to 8 KB and 20 custom keys. - Attestations count against a monthly plan quota (`429 quota_exceeded` when reached); the authenticated verify endpoint has its own verification quota. - Pass `trace_id` to attach an attestation to an open trace and seal it into a composite proof later. ## Endpoint reference - `POST /v1/ai/attestations` - create (write scope; optional Idempotency-Key). - `GET /v1/ai/attestations` - list (paginated, filter by type / model_provider / date). - `GET /v1/ai/attestations/{id}` - retrieve with signature and proof metadata. - `GET /v1/ai/attestations/{id}/raw` - byte-exact canonical JSON. - `POST /v1/ai/attestations/{id}/verify` - authenticated hash verify. - `GET /v1/proof/ai/{id}` and `POST /v1/proof/ai/{id}/verify` - public proof and verify, no API key. - `GET https://api.invoance.com/keys/{domain}` - public tenant signing-key discovery. Next: the request schema and hashes at https://www.invoance.com/developers/ai-attestations/schema. --- # AI Attestation Schema What you send to `POST /v1/ai/attestations`, the three hashes Invoance computes from it, and the canonical statement your tenant's Ed25519 key signs. ## A complete request ```json { "type": "output", "payload": { "input": "Summarize the attached contract.", "output": "The contract grants a 12-month license..." }, "context": { "model_provider": "openai", "model_name": "gpt-4o", "model_version": "2024-11-20" }, "subject": { "user_id": "user_123", "session_id": "sess_456", "workflow": "contract-review" }, "trace_id": "b3a1c9d0-...-optional" } ``` ## Fields | Field | Type | Required | Notes | |---|---|---|---| | `type` | string | Yes | One of output, decision, approval. Anything else is rejected 400 invalid_attestation_type. | | `payload.input` | string | Yes | The exact model input. Non-empty after trim (400 empty_payload). | | `payload.output` | string | Yes | The exact model output. Non-empty (400 empty_payload). | | `context.model_provider` | string | Yes | e.g. openai, anthropic. Non-empty (400 invalid_context). | | `context.model_name` | string | Yes | e.g. gpt-4o. Non-empty. | | `context.model_version` | string | Yes | e.g. 2024-11-20. Non-empty. | | `subject` | object | No | user_id and session_id (both optional strings) plus up to 20 custom key/value pairs; whole block up to 8 KB serialized. | | `trace_id` | string (UUID) | No | Attach to an existing trace. The trace must exist (404 trace_not_found) and be open (409 trace_not_open). | ## The three hashes Every attestation carries three hex SHA-256 digests, returned at creation and on every read: - `input_hash` - SHA-256 of the `payload.input` UTF-8 bytes. - `output_hash` - SHA-256 of the `payload.output` UTF-8 bytes. - `payload_hash` - SHA-256 of the canonical request bytes (the whole body). This is also the `attestation_hash` (the record's root hash) and the per-tenant dedup key: an identical body returns 200 with `status: "duplicate"` and the original id. The canonical bytes are the request re-serialized in a fixed field order — `type`, `payload` (input, output), `context` (model_provider, model_name, model_version), `subject` (user_id and session_id omitted when absent, custom keys in alphabetical order), `trace_id` — not the raw wire bytes. Those exact bytes are stored verbatim and served by `GET /v1/ai/attestations/{id}/raw`, so hashing the /raw response reproduces `payload_hash`. ## The canonical signed statement What gets signed is a server-built statement over the hashes, not the raw customer payload. Its JSON serialization, in this exact field order, is stored as `signed_payload` and signed with your tenant's Ed25519 key (standard Ed25519, no pre-hash): ```json { "v": 1, "attestation_id": "9d2f1c4e-...", "tenant_id": "f0e1d2c3-...", "attestation_type": "output", "input_hash": "a1b2...", "output_hash": "c3d4...", "payload_hash": "e5f6...", "model_provider": "openai", "model_name": "gpt-4o", "model_version": "2024-11-20", "created_at": "2026-07-04T12:00:00Z" } ``` The 64-byte `signature` and 32-byte `public_key` are returned (hex) by `GET /v1/ai/attestations/{id}` and the public proof endpoint, with `signature_alg: "ed25519"`. ## Limits and rules - Whole serialized body up to 1 MB (413 payload_too_large). - `subject`: up to 20 custom keys beyond user_id/session_id (400 subject_too_many_keys), up to 8 KB serialized (400 subject_too_large). - `content_hash` on the verify endpoints must be exactly 64 hex characters (SHA-256). - `Idempotency-Key` header is optional; replaying the same key with the same body returns the cached response for 24 h, a different body returns 409 idempotency_key_reuse_mismatch. - Retention: past your plan's retention window a record is sealed — GET, list, and /raw return 410 retention_expired, but verify and the public proof keep working. Upgrading unseals. Next: how verification works at https://www.invoance.com/developers/ai-attestations/verification. --- # AI Attestation Verification & Proof Every attestation can be checked at increasing levels of independence — from a server-side hash comparison to recomputing everything yourself against a published key. Nothing in the ladder requires trusting Invoance's answer. ## The verification ladder ### 1. Hash verify (authenticated) `POST /v1/ai/attestations/{id}/verify` with `{ "content_hash": "<64-hex>" }` compares your hash against `attestation_hash`, `input_hash`, `output_hash`, and `payload_hash`, returning `match_result` and `matched_field`. Counts against your monthly verification quota. ### 2. Verify by raw payload (SDK) `client.attestations.verify_payload()` (Python) / `verifyPayload()` (Node) serializes your copy of the payload, hashes it client-side, and calls verify — so you prove the bytes you hold are the bytes that were sealed. ### 3. Ed25519 signature verify (SDK) `client.attestations.verify_signature()` / `verifySignature()` fetches the record and verifies the Ed25519 signature over `signed_payload` locally (node:crypto on Node, PyNaCl on Python). Verification is math, not an API answer. ### 4. Public proof page (no account) `GET /v1/proof/ai/{attestation_id}` is public and unauthenticated (rate limited per IP). It returns the organization identity (name, issuer, verified domain) and the attestation: type, all three hashes, model context, subject ids, signature, and key. The `public_key` served here comes from the tenant's write-once registered key — the pinned key — never from the attestation row, so a tampered row cannot vouch for itself. ### 5. Public verify (hash + signature) ```bash curl -X POST https://api.invoance.com/v1/proof/ai/9d2f1c4e-.../verify \ -H "Content-Type: application/json" \ -d '{ "content_hash": "<64-hex SHA-256>" }' ``` No API key. Compares the hash against `payload_hash`, `input_hash`, and `output_hash` AND verifies the Ed25519 signature over `signed_payload` against the pinned tenant key, returning `match_result`, `signature_valid`, and `matched_field`. ### 6. Pinned key discovery Fetch a tenant's signing key straight from its verified domain — the root of trust for fully independent verification: ```bash curl https://api.invoance.com/keys/acme.com # { "domain": "acme.com", "public_key": "", # "algorithm": "ed25519", "key_id": "invoance:pk_..." } ``` Note the encoding: `/keys/{domain}` returns the key base64url-encoded; the proof endpoints return it hex-encoded. Same 32 bytes. ## Full third-party recipe 1. Fetch `GET /v1/ai/attestations/{id}/raw`, SHA-256 the bytes - must equal `payload_hash` (= `attestation_hash`). 2. SHA-256 the claimed input and output text - must equal `input_hash` and `output_hash`. 3. Verify the Ed25519 signature over `signed_payload` against the tenant's published key from `/keys/{domain}` - not the key embedded in the record. If all three hold, the exact input, output, model identity, and time are proven; forging any of them requires the tenant's private key. Sealed records (past retention) still verify: the verify endpoints and the public proof page keep working after GET/list return `410`. Related: the canonical statement format at https://www.invoance.com/developers/ai-attestations/schema and the SDK verify methods at https://www.invoance.com/developers/ai-attestations/sdk. --- # AI Attestations SDK Attestations live under `client.attestations` in both SDKs. Python methods are async with keyword-only arguments; Node takes a single params object. A key needs `write` scope to ingest and `read` for everything else. ## Install ```bash pip install invoance # Python npm install invoance # Node ``` ## Methods ### Create an attestation ```python result = await client.attestations.ingest( attestation_type="output", # "output" | "decision" | "approval" input="Summarize the attached contract.", output="The contract grants a 12-month license...", model_provider="openai", model_name="gpt-4o", model_version="2024-11-20", subject={"user_id": "user_123", "session_id": "sess_456"}, # optional idempotency_key="7f4c1c9d-...", # optional trace_id=None, # optional: attach to an open trace ) print(result.attestation_id, result.payload_hash) ``` ```ts const result = await client.attestations.ingest({ type: "output", input: "Summarize the attached contract.", output: "The contract grants a 12-month license...", modelProvider: "openai", modelName: "gpt-4o", modelVersion: "2024-11-20", subject: { userId: "user_123", sessionId: "sess_456" }, // optional idempotencyKey: "7f4c1c9d-...", // optional }); console.log(result.attestation_id, result.payload_hash); ``` ### List attestations ```python page = await client.attestations.list( page=1, limit=50, attestation_type="output", # optional filter model_provider="openai", # optional filter date_from="2026-06-01T00:00:00Z", date_to="2026-07-01T00:00:00Z", ) for a in page.attestations: print(a.attestation_id, a.attestation_type, a.created_at) ``` ```ts const page = await client.attestations.list({ page: 1, limit: 50, attestationType: "output", modelProvider: "openai", dateFrom: "2026-06-01T00:00:00Z", dateTo: "2026-07-01T00:00:00Z", }); ``` ### Get an attestation ```python att = await client.attestations.get("9d2f1c4e-...") print(att.attestation_hash, att.signature_alg, att.public_key) ``` ### Get the raw canonical payload Returns the byte-exact canonical JSON submitted at ingest — hashing it reproduces `payload_hash`. ```python raw = await client.attestations.get_raw("9d2f1c4e-...") ``` ```ts const raw = await client.attestations.getRaw("9d2f1c4e-..."); ``` ### Verify by hash ```python res = await client.attestations.verify("9d2f1c4e-...", content_hash="<64-hex>") print(res.match_result, res.matched_field) ``` ```ts const res = await client.attestations.verify("9d2f1c4e-...", { contentHash: "<64-hex>" }); ``` ### Verify by raw payload (SDK only) Serializes and hashes your copy of the payload client-side, then verifies — key order must match the canonical field order (type, payload, context, subject). ```python res = await client.attestations.verify_payload("9d2f1c4e-...", payload=raw) ``` ```ts const res = await client.attestations.verifyPayload("9d2f1c4e-...", raw); ``` ### Verify the Ed25519 signature (SDK only) Fetches the record and checks the signature over `signed_payload` locally — node:crypto on Node, PyNaCl on Python (install pynacl). ```python res = await client.attestations.verify_signature("9d2f1c4e-...") print(res.valid, res.reason) ``` ```ts const res = await client.attestations.verifySignature("9d2f1c4e-..."); console.log(res.valid, res.reason); ``` Note: `verify_signature` trusts the public key embedded in the fetched record. For a pinned-key check with zero trust in the server, fetch your published key from `GET https://api.invoance.com/keys/{your-domain}` and verify `signed_payload` against it — see https://www.invoance.com/developers/ai-attestations/verification. See the quick start at https://www.invoance.com/developers/ai-attestations for endpoints, limits, and the public proof flow. --- # SDKs and libraries Official Python and Node SDKs with typed methods for every endpoint, client-side hashing, and Ed25519 verification, plus cURL for any language. ## Install ```bash pip install invoance # Python npm install invoance # Node ``` ## Initialize Set `INVOANCE_API_KEY` in the environment and the client picks it up. ```python from invoance import InvoanceClient async with InvoanceClient() as client: result = await client.events.ingest( event_type="user.signed_in", payload={"user_id": "user_123"}, ) print(result.event_id) ``` ```ts import { InvoanceClient } from "invoance"; const client = new InvoanceClient(); // or { apiKey: "invoance_live_xxx" } ``` ## Surfaces - `client.events` - ingest / list / get / verify ledger events. - `client.documents` - anchor a file or hash, retrieve, download original, verify. - `client.ai` - create and verify AI attestations. - `client.traces` - create, seal, and fetch trace proofs. - `client.audit` - the Audit Logs product (ingest, list, get, verify offline, exports, orgs, portal sessions). See https://www.invoance.com/developers/audit/sdk. ## Documents ```python result = await client.documents.anchor_file( file="./invoice.pdf", document_ref="Invoice #1042", ) print(result.event_id) ``` Pass `skip_original=True` (Python) or `skipOriginal: true` (Node) to anchor the hash only. ## Offline verification The audit SDK ships an offline verifier - it reconstructs the canonical bytes and checks the Ed25519 signature locally, no network call: ```python from invoance import verify_audit_event result = verify_audit_event(event) print(result.valid) ``` ## cURL Every endpoint works with plain cURL; all examples use `https://api.invoance.com` and a Bearer token. --- # Traces API A trace groups related events, documents, and AI attestations into one auditable sequence, then seals it into a single verifiable proof with a composite hash. ## Lifecycle 1. **Create** a trace (status `open`) with a label and optional metadata (up to 16 KB). 2. **Attach** items by passing `trace_id` when you anchor an event, document, or attestation. 3. **Seal** the trace: Invoance computes a composite hash over all linked items, signs the bundle, and transitions it to `sealed` (returns 202; poll the trace). 4. **Prove**: fetch the proof bundle - every item with its hashes and signatures, plus the composite hash. A sealed trace is immutable. ## Create a trace ```bash curl -X POST https://api.invoance.com/v1/traces \ -H "Authorization: Bearer invoance_live_xxx" \ -H "Idempotency-Key: " \ -H "Content-Type: application/json" \ -d '{ "label": "Monthly compliance review, March 2026" }' ``` ## Seal and prove ```bash curl -X POST https://api.invoance.com/v1/traces/trc_01HX.../seal \ -H "Authorization: Bearer invoance_live_xxx" curl https://api.invoance.com/v1/traces/trc_01HX.../proof \ -H "Authorization: Bearer invoance_live_xxx" ``` The composite hash lets anyone confirm the whole set of items existed, in order, at seal time, and was not altered. --- # Verification API Verification independently confirms that a signed record is authentic and unaltered. It needs no trust in Invoance, and for audit events it can run fully offline. ## How it works 1. Recompute the SHA-256 hash of the original payload, or rebuild the canonical bytes from the stored fields. 2. Check that the recomputed hash matches the anchored hash. 3. Verify the Ed25519 signature against the tenant's published public key. If the data changed, the hash no longer matches; if the signature was forged or the signer differs, the signature check fails. ## Public verification Public, read-only verification endpoints need no API key: ``` GET https://invoance.com/proof/event/{event_id} GET https://invoance.com/proof/document/{event_id} GET https://invoance.com/proof/ai/{attestation_id} GET https://invoance.com/.well-known/invoance-platform-key ``` ## API verification Scoped endpoints recompute and verify server-side and return whether a record is valid and why: - `POST /v1/document/:id/verify`, `POST /v1/events/:id/verify`, `POST /v1/ai/attestations/:id/verify` - `GET /v1/audit/events/:id/verify` - rebuilds the canonical bytes and checks the signature against the tenant's pinned key, returning `{ valid, reason, key_source }`. ## Offline verification (Audit Logs) The Python and Node SDKs verify an audit event locally with no network call - they reconstruct the invoance.audit/1 canonical bytes and check Ed25519 against the event's (or a pinned) public key: ```python from invoance import verify_audit_event result = verify_audit_event(event) # offline print(result.valid) ``` Pin the tenant's published key for a full tamper guarantee. --- # API FAQ Common questions about the Invoance API: authentication, anchoring, verification, and rate limits. ## Getting started **What is the Invoance API?** A REST API that lets you hash, sign, and timestamp any business event, document, or AI output. Every record is written to an append-only ledger and can be independently verified by anyone, with no Invoance account required. **How do I create an API key?** Sign in at app.invoance.com, go to Profile then API Keys, and click Create Key. Name it (for example "production") and copy the key immediately - it is only shown once. Set it as INVOANCE_API_KEY in your environment, or pass it when initializing the SDK client. **What do I need to start?** An API key and one of the SDKs. Install with `pip install invoance` (Python) or `npm install invoance` (Node.js), set your key, and anchor your first event in three lines. **Which SDKs are available?** Official SDKs for Python and Node.js. Every endpoint is also documented with raw cURL examples for any language. **Is there a free tier or sandbox?** Yes. Every new organization gets a free allocation for testing. All anchored records on the free tier are production-grade; there is no separate sandbox environment. ## Authentication and security **How does authentication work?** Bearer token authentication. Include your API key in the Authorization header: `Authorization: Bearer invoance_live_xxx`. Keys are scoped to your organization and can be rotated at any time. **Can I have multiple API keys?** Yes - production, staging, or per-service keys, each independently revocable. **Is my data encrypted?** All traffic is encrypted in transit via TLS. Payloads are hashed with SHA-256 and signed with Ed25519 before being written to the ledger. The original payload content is not stored, only the cryptographic proof. ## Events and anchoring **What is an anchored event?** Any data you send that gets hashed (SHA-256), signed (Ed25519), timestamped, and written to the immutable ledger. Once anchored, it cannot be altered or deleted. **What can I anchor?** Business events (approvals, state changes, audit actions), documents (any file type), and AI outputs (model, prompt, and response bound into one signed record). **Does the API support idempotency?** Yes. Pass an Idempotency-Key header with a unique value per request; a duplicate key within 10 minutes returns the original response. **Is there a size limit on payloads?** Event payloads are limited to 1 MB of JSON. Document anchoring supports files up to 50 MB; the SDK streams and hashes them without loading the whole file into memory. ## Verification and proof **How does verification work?** Anyone can verify a record by recomputing the SHA-256 hash of the original payload and validating the Ed25519 signature against the organization's public key. No Invoance account is needed. **Where do I find my public key?** At https://api.invoance.com/keys/{your-domain} and in your dashboard under Settings. **What are traces?** Traces group related events, documents, and AI attestations into a single verifiable process proof, bundled with a composite hash and a sealed timestamp. ## Rate limits and errors **What are the rate limits?** Up to 100 requests per second per organization on paid plans (10 rps on the free tier). Exceeding the limit returns 429 Too Many Requests with a Retry-After header. **What does a 422 mean?** 422 Unprocessable Entity means the request was syntactically valid but semantically wrong: a missing required field, an invalid event type, or a URL that does not match your organization's domain. **Where is the full error list?** See the Errors page. Every error response includes a machine-readable code, a human-readable message, and a pointer to the offending field where applicable. ---