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 schemaVerifying attestations
Traces
Sealing a trace
Audit logs
OrganizationsStreamsPortalPublic proofEvent schemaExporting eventsIntegrationsEmbeddable viewer
All endpoints
Reference
SDKs
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

  • Why Invoance
  • Pricing
  • Security
  • Compliance teams
  • Finance teams
  • Partners
  • Resources
  • Help center
  • Contact
© 2026 Invoance
PrivacyLegal noticeLegal FAQGitHubLinkedInX