Home
Home/Developers/Audit logs/portal
Ed25519 signaturesSHA-256 hashes8 SDKs
Status
Sign inStart free
Home
Start here
Overview
Authentication
Errors
FAQ
API
Events
Canonical JSON and hashes
Documents
Anchoring a file
AI attestations
Attestation schema
Verifying attestations
Traces
Sealing a trace
Audit logs
Organizations
Streams
Portal
Public proof
Event schema
Exporting events
Integrations
Clerk
Auth0
Embeddable viewer
All endpoints
Reference
SDKs
Node.js
Python
Go
Java
Ruby
Rust
.NET
PHP
REST
Verification
API · Audit Logs

Audit portal

Mint a short-lived portal session for a customer, then read, verify and manage streams with that portal token.

Audit Logs overview
sha-256 · 3a352297…2592
sha-256 · bae251a9…8ffa
On this page
Portal sessionsPOST/v1/audit/portal_sessionsCreate a portal session
Portal, with a portal tokenPOST/v1/audit/portal/exchangeExchange a portal link tokenGET/v1/audit/portal/eventsList events through the portalGET/v1/audit/portal/events/{id}Get an event through the portalGET/v1/audit/portal/events/{id}/verifyVerify an event through the portalGET/v1/audit/portal/orgGet the portal's org and issuerGET/v1/audit/portal/streamsList streams through the portalPOST/v1/audit/portal/streamsCreate a stream through the portalDELETE/v1/audit/portal/streams/{id}Delete a stream through the portalPOST/v1/audit/portal/streams/{id}/testTest a stream through the portal
Portal sessions
POST/v1/audit/portal_sessions
API keyaudit:write

Create a portal session

Mints a one-time link for the hosted audit viewer scoped to one org and one intent, and returns the link, its raw token and both lifetimes.

Headers
Content-Typestring · required

Must be application/json.

Request body
organization_idstring · required

Your organization_id or the aorg_ id of the org the viewer may read; org_id is accepted as a legacy alias.

intentstring · required

What the link may do: audit_logs reads events, log_streams manages the org's streams. One of audit_logs, log_streams.

session_duration_secondsinteger · 60 to 86,400 · default 7200

How long the viewer session lasts once the link is opened.

link_duration_secondsinteger · 60 to 3,600 · default 300

How long the one-time link stays valid before it must be opened.

  • Mint the session server-side and hand the url or token to the end user; the token exchanges once for a portal JWT via POST /v1/audit/portal/exchange, then it is spent.
  • The url host is the dashboard base URL configured for the deployment.
  • The portal JWT is scoped to the org and intent in the session; it cannot read any other org.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const session = await client.audit.portalSessions.create({
  organizationId: "org_8472",
  intent: "audit_logs",
  sessionDurationSeconds: 3600,
});
console.log(session.url, session.link_expires_in, session.session_expires_in);
Response · 201
{
  "id": "aps_01J0Y5R7T9W1Y3A5C7E9G1J3KM",
  "intent": "audit_logs",
  "url": "https://app.invoance.com/portal?token=pl_3f8a9c1d2e4b5a6f7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
  "token": "pl_3f8a9c1d2e4b5a6f7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
  "link_expires_in": 300,
  "session_expires_in": 3600
}
Response fields
idstring

Session id, aps_ followed by a ULID.

intentstring

The intent you sent. One of audit_logs, log_streams.

urlstring

Hosted viewer URL carrying the token; send the end user here.

tokenstring

The raw one-time link token, for embedding the viewer yourself; only its SHA-256 is stored and it is never shown again.

link_expires_ininteger

Seconds until the unopened link expires.

session_expires_ininteger

Seconds the portal JWT lasts once the link is exchanged.

Errors
invalid_intent400

intent is neither audit_logs nor log_streams.

invalid_duration400

session_duration_seconds is outside 60 to 86,400.

invalid_link_duration400

link_duration_seconds is outside 60 to 3,600.

insufficient_scope403

The key does not have audit:write; audit:read alone is not enough.

not_found404

No audit org with that id or organization_id belongs to the tenant.

org_archived409

The org is archived; unarchive it first.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

A database query failed.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

An Authorization header was sent without the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

Portal, with a portal token
POST/v1/audit/portal/exchange
Public

Exchange a portal link token

Spends a one-time portal link token and returns the short-lived, org-scoped JWT the viewer uses on the portal routes.

Headers
Content-Typestring · required

Must be application/json.

Request body
tokenstring · required

The raw token from create portal session (the token field, or the token query parameter of url).

  • Single use and expiry are enforced in one atomic update: the session is marked consumed only if it was unconsumed and unexpired, so a replayed link fails with 401.
  • The portal routes allow cross-origin browser calls from any origin, so an embedded viewer can call this from your own domain; the API-key routes do not.
  • A body without a token key is rejected by the framework with a plain-text 4xx before the handler runs.
  • None of the SDKs expose this call; it is meant for the hosted viewer and the @invoance/audit-viewer embed.
Node.js
// No API key: the exchange is public and the link token is the credential.
const linkToken = process.env.PORTAL_LINK_TOKEN;

const res = await fetch("https://api.invoance.com/v1/audit/portal/exchange", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ token: linkToken }),
});
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const session = await res.json();
console.log(session.intent, session.expires_in);
// session.token is the Bearer token for the /v1/audit/portal/* routes.
Response · 200
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhcHNfMDFKMFk1UjdUOVcxWTNBNUM3RTlHMUozS00iLCJhdWQiOiJhdWRpdF9wb3J0YWwiLCJvcmdfaWQiOiJhb3JnXzAxSjBYVzlLM1JRNVQ3VjhZMkM0RTZHOEhNIiwiaW50ZW50IjoiYXVkaXRfbG9ncyIsImV4cCI6MTc5MDA2ODA0N30.q7Yw3d5Z1kH0m2Rr8sVv4cXx6bNn9pLl1jGg0fTt2uE",
  "token_type": "Bearer",
  "intent": "audit_logs",
  "expires_in": 3600
}
Response fields
tokenstring

The portal JWT (audience audit_portal) carrying the session id, org id, intent and expiry; send it as Authorization: Bearer on the portal routes.

token_typestring

Always Bearer. One of Bearer.

intentstring

The intent the session was minted with. One of audit_logs, log_streams.

expires_ininteger

Seconds until the JWT expires, equal to the session's session_duration_seconds.

Errors
invalid_or_used_link401

No session matches the token, the link expired before it was opened, or it was already exchanged.

rate_limited429

The caller's IP exceeded 10 exchanges per second (or 600 per minute); the body carries retry_after.

db_error500

The session lookup failed.

token_sign_failed500

The JWT could not be signed.

GET/v1/audit/portal/events
Portal token

List events through the portal

Returns the session's org events newest first with the same filters and cursor as the API list.

Headers
Authorizationstring · required

Bearer followed by the portal JWT from the exchange.

Query parameters
actionsstring

Comma-separated list of action strings; only events whose action is in the list are returned.

actor_idstring

Only events whose actor.id equals this value.

target_idstring

Only events with a target whose id equals this value.

occurred_aftertimestamp (ISO 8601)

Inclusive lower bound on occurred_at, RFC 3339 (no range_start alias on this route).

occurred_beforetimestamp (ISO 8601)

Inclusive upper bound on occurred_at, RFC 3339 (no range_end alias on this route).

limitinteger · clamped to 1 to 100 · default 50

Page size.

cursorstring

Opaque next_cursor from the previous page.

  • The org comes from the JWT; there is no organization_id parameter and the token cannot read any other org.
  • The output is byte-for-byte the same event JSON the API list returns.
  • A token minted before intents existed (empty intent) is treated as audit_logs.
Node.js
// Portal reads use the short-lived JWT from POST /v1/audit/portal/exchange,
// not an API key. PORTAL_TOKEN holds that JWT.
const portalToken = process.env.PORTAL_TOKEN;

const url = new URL("https://api.invoance.com/v1/audit/portal/events");
url.searchParams.set("actions", "user.signed_in");
url.searchParams.set("limit", "50");

const res = await fetch(url, { headers: { Authorization: "Bearer " + portalToken } });
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const page = await res.json();
for (const event of page.events) {
  console.log(event.seq, event.action, event.actor.id);
}
console.log(page.next_cursor);
Response · 200
{
  "events": [
    {
      "id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
      "org_id": "aorg_01J0XW9K3RQ5T7V8Y2C4E6G8HM",
      "seq": 42,
      "schema_id": "invoance.audit/1",
      "occurred_at": "2026-09-22T08:14:07.000Z",
      "ingested_at": "2026-09-22T08:14:07.312Z",
      "action": "user.signed_in",
      "actor": {
        "type": "user",
        "id": "u_4821",
        "name": "Ada Lovelace"
      },
      "targets": [
        {
          "type": "workspace",
          "id": "ws_17"
        }
      ],
      "context": {
        "location": "203.0.113.10",
        "user_agent": "Mozilla/5.0"
      },
      "metadata": {
        "method": "sso",
        "mfa": true
      },
      "payload_hash": "df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3",
      "signature": "c6a2bf3bc3895915ead5f0b99eaf84534d4e8b9aa68bef8b0508cfd473f06e694d76d2bc330b83cfa613e49e7d2490d0413285017acfb78746bcc1524d05160d",
      "signing_public_key": "bee215a9d2a0170176a88733056d8a0ae5372b0da8238796bef4a0b75d4c0974"
    }
  ],
  "next_cursor": null
}
Response fields
eventsobject[]

The page of events, ordered by occurred_at descending then id descending.

events[].idstring

Event id, aevt_ followed by a ULID; minted at ingest before the event is queued.

events[].org_idstring

The aorg_ id of the audit org the event belongs to (not your organization_id).

events[].seqinteger

Position in the org's log, assigned by the signer in commit order; per org it starts at 1 and has no gaps.

events[].schema_idstring

Always invoance.audit/1; it is inside the signed bytes as a domain tag. One of invoance.audit/1.

events[].occurred_attimestamp (ISO 8601)

When the event happened in your system, normalized to UTC with exactly three fractional digits and a Z suffix.

events[].ingested_attimestamp (ISO 8601)

Server time when the event was accepted, in the same canonical form; this value is signed.

events[].actionstring

The action string as sent, for example user.signed_in.

events[].actorobject

The actor object as sent: type and id, plus name and metadata when given.

events[].targetsobject[]

The targets array as sent; empty when the event had none.

events[].contextobject

The context object as sent (location, user_agent), or null when omitted.

events[].metadataobject

The flat metadata object as sent, or null when omitted.

events[].payload_hashhex

SHA-256 of the canonical signed bytes, 64 hex characters.

events[].signaturehex

Ed25519 signature over the canonical bytes, 128 hex characters.

events[].signing_public_keyhex

The tenant's Ed25519 public key as recorded with the row, 64 hex characters; shown for display, verification uses the key registered in tenant_keys.

next_cursorstring

Opaque cursor for the next page, or null on the last page.

Errors
invalid_timestamp400

occurred_after or occurred_before is not RFC 3339.

invalid_cursor400

cursor is not a cursor this endpoint issued.

missing_portal_token401

No Authorization: Bearer header was sent.

invalid_portal_token401

The token is not a portal JWT, has expired, or has the wrong audience.

wrong_intent403

The portal link was minted with intent log_streams, which cannot read events.

rate_limited429

The caller's IP exceeded 60 requests per second (or 3,600 per minute) on portal reads; the body carries retry_after and the Retry-After header is set.

db_error500

A database query failed.

GET/v1/audit/portal/events/{id}
Portal token

Get an event through the portal

Returns one event of the session's org.

Headers
Authorizationstring · required

Bearer followed by the portal JWT from the exchange.

Path parameters
idstring · required

The aevt_ id of the event.

  • An event of a different org returns the same 404 as an unknown id.
Node.js
// Portal reads use the short-lived JWT from POST /v1/audit/portal/exchange,
// not an API key. PORTAL_TOKEN holds that JWT.
const portalToken = process.env.PORTAL_TOKEN;

const res = await fetch("https://api.invoance.com/v1/audit/portal/events/aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK", {
  headers: { Authorization: "Bearer " + portalToken },
});
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const event = await res.json();
console.log(event.seq, event.action, event.payload_hash);
Response · 200
{
  "id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
  "org_id": "aorg_01J0XW9K3RQ5T7V8Y2C4E6G8HM",
  "seq": 42,
  "schema_id": "invoance.audit/1",
  "occurred_at": "2026-09-22T08:14:07.000Z",
  "ingested_at": "2026-09-22T08:14:07.312Z",
  "action": "user.signed_in",
  "actor": {
    "type": "user",
    "id": "u_4821",
    "name": "Ada Lovelace"
  },
  "targets": [
    {
      "type": "workspace",
      "id": "ws_17"
    }
  ],
  "context": {
    "location": "203.0.113.10",
    "user_agent": "Mozilla/5.0"
  },
  "metadata": {
    "method": "sso",
    "mfa": true
  },
  "payload_hash": "df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3",
  "signature": "c6a2bf3bc3895915ead5f0b99eaf84534d4e8b9aa68bef8b0508cfd473f06e694d76d2bc330b83cfa613e49e7d2490d0413285017acfb78746bcc1524d05160d",
  "signing_public_key": "bee215a9d2a0170176a88733056d8a0ae5372b0da8238796bef4a0b75d4c0974"
}
Response fields
idstring

Event id, aevt_ followed by a ULID; minted at ingest before the event is queued.

org_idstring

The aorg_ id of the audit org the event belongs to (not your organization_id).

seqinteger

Position in the org's log, assigned by the signer in commit order; per org it starts at 1 and has no gaps.

schema_idstring

Always invoance.audit/1; it is inside the signed bytes as a domain tag. One of invoance.audit/1.

occurred_attimestamp (ISO 8601)

When the event happened in your system, normalized to UTC with exactly three fractional digits and a Z suffix.

ingested_attimestamp (ISO 8601)

Server time when the event was accepted, in the same canonical form; this value is signed.

actionstring

The action string as sent, for example user.signed_in.

actorobject

The actor object as sent: type and id, plus name and metadata when given.

targetsobject[]

The targets array as sent; empty when the event had none.

contextobject

The context object as sent (location, user_agent), or null when omitted.

metadataobject

The flat metadata object as sent, or null when omitted.

payload_hashhex

SHA-256 of the canonical signed bytes, 64 hex characters.

signaturehex

Ed25519 signature over the canonical bytes, 128 hex characters.

signing_public_keyhex

The tenant's Ed25519 public key as recorded with the row, 64 hex characters; shown for display, verification uses the key registered in tenant_keys.

Errors
missing_portal_token401

No Authorization: Bearer header was sent.

invalid_portal_token401

The token is not a portal JWT, has expired, or has the wrong audience.

wrong_intent403

The portal link was minted with intent log_streams, which cannot read events.

not_found404

No event with that id belongs to the session's org.

rate_limited429

The caller's IP exceeded 60 requests per second (or 3,600 per minute) on portal reads; the body carries retry_after and the Retry-After header is set.

db_error500

A database query failed.

GET/v1/audit/portal/events/{id}/verify
Portal token

Verify an event through the portal

Runs the same pinned-key verification as the API verify for one event of the session's org.

Headers
Authorizationstring · required

Bearer followed by the portal JWT from the exchange.

Path parameters
idstring · required

The aevt_ id of the event.

  • The key is read from tenant_keys for the event's tenant, exactly as on the API route.
Node.js
// Portal reads use the short-lived JWT from POST /v1/audit/portal/exchange,
// not an API key. PORTAL_TOKEN holds that JWT.
const portalToken = process.env.PORTAL_TOKEN;

const res = await fetch("https://api.invoance.com/v1/audit/portal/events/aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK/verify", {
  headers: { Authorization: "Bearer " + portalToken },
});
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const result = await res.json();
console.log(result.valid, result.reason, result.key_source);
Response · 200
{
  "event_id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
  "valid": true,
  "reason": null,
  "schema": "invoance.audit/1",
  "payload_hash": "df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3",
  "signed_data": {
    "action": "user.signed_in",
    "actor": {
      "id": "u_4821",
      "name": "Ada Lovelace",
      "type": "user"
    },
    "context": {
      "location": "203.0.113.10",
      "user_agent": "Mozilla/5.0"
    },
    "event_id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
    "ingested_at": "2026-09-22T08:14:07.312Z",
    "metadata": {
      "method": "sso",
      "mfa": true
    },
    "occurred_at": "2026-09-22T08:14:07.000Z",
    "org_id": "aorg_01J0XW9K3RQ5T7V8Y2C4E6G8HM",
    "schema_id": "invoance.audit/1",
    "seq": 42,
    "targets": [
      {
        "id": "ws_17",
        "type": "workspace"
      }
    ]
  },
  "key_source": "tenant_keys"
}
Response fields
event_idstring

The id from the path.

validboolean

True when the recomputed hash matches the stored payload_hash, the bytes carry the audit schema_id, and the signature verifies under the tenant's registered key.

reasonstring

Why the check failed, or null when valid. One of canonicalization_failed, payload_hash_mismatch, wrong_domain, signature_invalid.

schemastring

Always invoance.audit/1. One of invoance.audit/1.

payload_hashhex

SHA-256 of the recomputed canonical bytes; empty when canonicalization failed.

signed_dataobject

The canonical signed object parsed back to JSON (keys sorted, nulls removed, schema_id included); null when canonicalization failed.

key_sourcestring

Always tenant_keys: the key used is the tenant's registered key, never the one stored on the row. One of tenant_keys.

Errors
missing_portal_token401

No Authorization: Bearer header was sent.

invalid_portal_token401

The token is not a portal JWT, has expired, or has the wrong audience.

wrong_intent403

The portal link was minted with intent log_streams, which cannot read events.

not_found404

No event with that id belongs to the session's org.

rate_limited429

The caller's IP exceeded 60 requests per second (or 3,600 per minute) on portal reads; the body carries retry_after and the Retry-After header is set.

db_error500

A database query failed, or the event's tenant has no registered key.

GET/v1/audit/portal/org
Portal token

Get the portal's org and issuer

Returns the branding of the tenant that issued the link and the org the session may read.

Headers
Authorizationstring · required

Bearer followed by the portal JWT from the exchange.

  • This route works with either intent; use archived_at to explain to the viewer why stream creation is rejected.
Node.js
// Portal reads use the short-lived JWT from POST /v1/audit/portal/exchange,
// not an API key. PORTAL_TOKEN holds that JWT.
const portalToken = process.env.PORTAL_TOKEN;

const res = await fetch("https://api.invoance.com/v1/audit/portal/org", {
  headers: { Authorization: "Bearer " + portalToken },
});
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const info = await res.json();
console.log(info.issuer.name, info.org.name, info.intent);
Response · 200
{
  "issuer": {
    "name": "Northwind Legal Ltd",
    "logo_url": "https://cdn.example.com/northwind/logo.svg",
    "domain_verified": true
  },
  "org": {
    "name": "Acme Robotics",
    "organization_id": "org_8472",
    "archived_at": null
  },
  "intent": "audit_logs"
}
Response fields
issuerobject

The Invoance tenant whose product the viewer is embedded in.

issuer.namestring

The tenant's issuer_name, or null when not set.

issuer.logo_urlstring

The tenant's logo URL, or null.

issuer.domain_verifiedboolean

True once the tenant's primary domain passed DNS verification.

orgobject

The end-customer org the session is scoped to.

org.namestring

The org's display name, falling back to its organization_id.

org.organization_idstring

The tenant's own id for the org.

org.archived_attimestamp (ISO 8601)

When the org was archived, or null.

intentstring

The session's intent. One of audit_logs, log_streams.

Errors
missing_portal_token401

No Authorization: Bearer header was sent.

invalid_portal_token401

The token is not a portal JWT, has expired, or has the wrong audience.

not_found404

The org in the token no longer exists.

rate_limited429

The caller's IP exceeded 60 requests per second (or 3,600 per minute) on portal reads; the body carries retry_after and the Retry-After header is set.

db_error500

A database query failed.

GET/v1/audit/portal/streams
Portal token

List streams through the portal

Returns up to 100 streams of the session's org, newest first, without secrets.

Headers
Authorizationstring · required

Bearer followed by a portal JWT minted with intent log_streams.

  • A token with intent audit_logs gets 403 wrong_intent here; the two intents are one-directional.
Node.js
// Portal reads use the short-lived JWT from POST /v1/audit/portal/exchange,
// not an API key. PORTAL_TOKEN holds that JWT.
const portalToken = process.env.PORTAL_TOKEN;

const res = await fetch("https://api.invoance.com/v1/audit/portal/streams", {
  headers: { Authorization: "Bearer " + portalToken },
});
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const { streams } = await res.json();
for (const stream of streams) {
  console.log(stream.id, stream.state, stream.endpoint);
}
Response · 200
{
  "streams": [
    {
      "id": "astr_01J0Y3N5P7R9T1V3X5Z7B9D1FG",
      "type": "webhook",
      "endpoint": "https://siem.example.com/hooks/invoance",
      "state": "active",
      "cursor_seq": 42,
      "failure_streak": 0,
      "last_error": null,
      "last_delivery_at": "2026-09-22T08:14:09.104233+00:00",
      "created_at": "2026-09-22T08:10:00.418212+00:00"
    }
  ]
}
Response fields
streamsobject[]

The streams, ordered by created_at descending.

streams[].idstring

Stream id, astr_ followed by a ULID.

streams[].typestring

Destination type; only webhook can be created. One of webhook.

streams[].endpointstring

The https URL deliveries are posted to, as validated at create time.

streams[].statestring

active while deliveries succeed, error while a transient failure is being retried with backoff, invalid once a permanent failure stopped the stream. One of active, error, invalid.

streams[].cursor_seqinteger

The highest seq delivered so far; the next delivery starts at cursor_seq + 1.

streams[].failure_streakinteger

Consecutive failed deliveries; reset to 0 on success and used to compute the backoff.

streams[].last_errorstring

Message from the last failed delivery, or null.

streams[].last_delivery_attimestamp (ISO 8601)

When the last successful delivery was recorded, or null.

streams[].created_attimestamp (ISO 8601)

When the stream was created.

Errors
missing_portal_token401

No Authorization: Bearer header was sent.

invalid_portal_token401

The token is not a portal JWT, has expired, or has the wrong audience.

wrong_intent403

The portal link was not minted with intent log_streams.

rate_limited429

The caller's IP exceeded 60 requests per second (or 3,600 per minute) on portal reads; the body carries retry_after and the Retry-After header is set.

db_error500

A database query failed.

POST/v1/audit/portal/streams
Portal token

Create a stream through the portal

Lets the end customer register a webhook destination for their own org, with the same checks and one-time signing secret as the API create.

Headers
Authorizationstring · required

Bearer followed by a portal JWT minted with intent log_streams.

Content-Typestring · required

Must be application/json.

Request body
typestring · required

Destination type; only webhook is accepted. One of webhook.

urlstring · required

Absolute https URL that will receive POST deliveries; the host must resolve to a public address.

  • Delivery format, signing and retry behaviour are the same as for streams created through the API; see create a webhook stream.
  • The plan cap counts streams per org regardless of which surface created them.
Node.js
// Portal reads use the short-lived JWT from POST /v1/audit/portal/exchange,
// not an API key. PORTAL_TOKEN holds that JWT.
const portalToken = process.env.PORTAL_TOKEN;

const res = await fetch("https://api.invoance.com/v1/audit/portal/streams", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + portalToken,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ type: "webhook", url: "https://siem.example.com/hooks/invoance" }),
});
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const stream = await res.json();
// Store signing_secret now; it is not returned again.
console.log(stream.id, stream.signing_secret);
Response · 201
{
  "id": "astr_01J0Y3N5P7R9T1V3X5Z7B9D1FG",
  "type": "webhook",
  "endpoint": "https://siem.example.com/hooks/invoance",
  "state": "active",
  "cursor_seq": 41,
  "signing_secret": "whsec_4c1d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d"
}
Response fields
idstring

Stream id, astr_ followed by a ULID.

typestring

Destination type; only webhook can be created. One of webhook.

endpointstring

The https URL deliveries are posted to, as validated at create time.

statestring

active while deliveries succeed, error while a transient failure is being retried with backoff, invalid once a permanent failure stopped the stream. One of active, error, invalid.

cursor_seqinteger

The org's last_seq at creation; nothing older is replayed.

signing_secretstring

whsec_ followed by 64 hex characters, returned only in this response.

Errors
unsupported_stream_type400

type is not webhook.

invalid_url400

url is not an absolute URL.

not_https400

url does not use https.

no_host400

url has no host.

resolution_failed400

The host did not resolve in DNS.

forbidden_destination400

The host resolves to a private, loopback, link-local or cloud metadata address.

too_many_streams400

The org already has as many streams as the tenant's plan allows.

missing_portal_token401

No Authorization: Bearer header was sent.

invalid_portal_token401

The token is not a portal JWT, has expired, or has the wrong audience.

wrong_intent403

The portal link was not minted with intent log_streams.

not_found404

The org in the token no longer exists.

org_archived409

The org is archived; unarchive it first.

rate_limited429

The caller's IP exceeded 60 requests per second (or 3,600 per minute) on portal reads; the body carries retry_after and the Retry-After header is set.

db_error500

A database query failed.

DELETE/v1/audit/portal/streams/{id}
Portal token

Delete a stream through the portal

Removes one of the session's org streams and returns the deleted id.

Headers
Authorizationstring · required

Bearer followed by a portal JWT minted with intent log_streams.

Path parameters
idstring · required

The astr_ id of the stream.

  • Deleting is allowed on an archived org.
Node.js
// Portal reads use the short-lived JWT from POST /v1/audit/portal/exchange,
// not an API key. PORTAL_TOKEN holds that JWT.
const portalToken = process.env.PORTAL_TOKEN;

const res = await fetch("https://api.invoance.com/v1/audit/portal/streams/astr_01J0Y3N5P7R9T1V3X5Z7B9D1FG", {
  method: "DELETE",
  headers: { Authorization: "Bearer " + portalToken },
});
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const result = await res.json();
console.log(result.deleted, result.id);
Response · 200
{
  "deleted": true,
  "id": "astr_01J0Y3N5P7R9T1V3X5Z7B9D1FG"
}
Response fields
deletedboolean

Always true on success.

idstring

The astr_ id that was deleted.

Errors
missing_portal_token401

No Authorization: Bearer header was sent.

invalid_portal_token401

The token is not a portal JWT, has expired, or has the wrong audience.

wrong_intent403

The portal link was not minted with intent log_streams.

not_found404

No stream with that id belongs to the session's org.

rate_limited429

The caller's IP exceeded 60 requests per second (or 3,600 per minute) on portal reads; the body carries retry_after and the Retry-After header is set.

db_error500

A database query failed.

POST/v1/audit/portal/streams/{id}/test
Portal token

Test a stream through the portal

Posts one synthetic stream.test event to the stream's destination and returns what it answered.

Headers
Authorizationstring · required

Bearer followed by a portal JWT minted with intent log_streams.

Path parameters
idstring · required

The astr_ id of the stream.

  • Same synthetic event, signing and 15 second wait as the API test route; the cursor and state are untouched.
Node.js
// Portal reads use the short-lived JWT from POST /v1/audit/portal/exchange,
// not an API key. PORTAL_TOKEN holds that JWT.
const portalToken = process.env.PORTAL_TOKEN;

const res = await fetch("https://api.invoance.com/v1/audit/portal/streams/astr_01J0Y3N5P7R9T1V3X5Z7B9D1FG/test", {
  method: "POST",
  headers: { Authorization: "Bearer " + portalToken },
});
if (!res.ok) {
  throw new Error(res.status + " " + (await res.text()));
}
const result = await res.json();
console.log(result.delivered, result.http_status, result.error);
Response · 200
{
  "delivered": true,
  "http_status": 200,
  "error": null,
  "retryable": false
}
Response fields
deliveredboolean

True when the destination answered with a 2xx.

http_statusinteger

The destination's status code, or null when no response arrived.

errorstring

Why the delivery failed, or null on success.

retryableboolean

True when the dispatcher would retry this failure; false on success or a permanent failure.

Errors
unsupported_stream_type400

The stream is not a webhook stream.

no_endpoint400

The stream has no endpoint stored.

missing_portal_token401

No Authorization: Bearer header was sent.

invalid_portal_token401

The token is not a portal JWT, has expired, or has the wrong audience.

wrong_intent403

The portal link was not minted with intent log_streams.

not_found404

The org in the token no longer exists, or no stream with that id belongs to it.

org_archived409

The org is archived; unarchive it first.

rate_limited429

The caller's IP exceeded 60 requests per second (or 3,600 per minute) on portal reads; the body carries retry_after and the Retry-After header is set.

db_error500

A database query failed or the stream's stored secret could not be decrypted.

Other audit logs endpointsOverview
EventsPOST/v1/audit/eventsIngest an audit eventGET/v1/audit/eventsList audit eventsGET/v1/audit/events/{id}Get an audit eventGET/v1/audit/events/{id}/verifyVerify an audit event
OrganizationsPOST/v1/audit/orgsCreate an audit orgGET/v1/audit/orgsList audit orgsPATCH/v1/audit/orgs/{id}Rename an audit orgDELETE/v1/audit/orgs/{id}Delete an audit orgPOST/v1/audit/orgs/{id}/archiveArchive an audit orgPOST/v1/audit/orgs/{id}/unarchiveUnarchive an audit orgGET/v1/audit/orgs/{id}/integrityCheck an org's sequence integrityPUT/v1/audit/orgs/{id}/retentionSet an org's retention
StreamsPOST/v1/audit/orgs/{id}/streamsCreate a webhook streamGET/v1/audit/orgs/{id}/streamsList an org's streamsDELETE/v1/audit/orgs/{id}/streams/{stream_id}Delete a streamPOST/v1/audit/orgs/{id}/streams/{stream_id}/testSend a test delivery
ExportsPOST/v1/audit/exportsCreate an exportGET/v1/audit/exports/{id}Get an export
Public proofGET/v1/proof/audit/{event_id}Get the public proof of an audit eventPOST/v1/proof/audit/{event_id}/verifyVerify a copy of an audit event

Proof infrastructure. Records are hashed, signed with your organization's Ed25519 key, and stored append-only, so anyone can check them later.

Products

  • Audit Logs
  • Event Ledger
  • AI Attestation
  • Document Anchoring
  • Traces

Developers

  • Documentation
  • API reference
  • SDKs
  • How it works
  • How traces seal
  • System status

Verify

  • Audit Log
  • Event
  • AI Attestation
  • Document
  • Trace

Company

  • Company overview
  • What is Invoance
  • Pricing
  • Security
  • Compliance teams
  • Finance teams
  • Partners
  • Brand assets
  • Resources
  • Help center
  • Contact
© 2025 – 2026 Invoance, Inc. All rights reserved.© 2026 Invoance, Inc. All rights reserved.
PrivacyLegal noticeLegal FAQ