Audit portal
Mint a short-lived portal session for a customer, then read, verify and manage streams with that portal token.
/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/v1/audit/portal_sessionsCreate 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.
Content-TypeMust be application/json.
organization_idYour organization_id or the aorg_ id of the org the viewer may read; org_id is accepted as a legacy alias.
intentWhat the link may do: audit_logs reads events, log_streams manages the org's streams. One of audit_logs, log_streams.
session_duration_secondsHow long the viewer session lasts once the link is opened.
link_duration_secondsHow 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.
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);
{
"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
}
idSession id, aps_ followed by a ULID.
intentThe intent you sent. One of audit_logs, log_streams.
urlHosted viewer URL carrying the token; send the end user here.
tokenThe raw one-time link token, for embedding the viewer yourself; only its SHA-256 is stored and it is never shown again.
link_expires_inSeconds until the unopened link expires.
session_expires_inSeconds the portal JWT lasts once the link is exchanged.
invalid_intentintent is neither audit_logs nor log_streams.
invalid_durationsession_duration_seconds is outside 60 to 86,400.
invalid_link_durationlink_duration_seconds is outside 60 to 3,600.
insufficient_scopeThe key does not have audit:write; audit:read alone is not enough.
not_foundNo audit org with that id or organization_id belongs to the tenant.
org_archivedThe org is archived; unarchive it first.
rate_limitedThe tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.
db_errorA database query failed.
missing_api_keyNeither an Authorization header nor an X-API-Key header was sent.
invalid_authorization_schemeAn Authorization header was sent without the Bearer scheme.
invalid_api_key_formatThe key does not start with invoance_live_.
invalid_api_keyThe key does not match any API key.
api_key_revokedThe key has been revoked.
ip_not_allowedThe key has an IP allowlist and the caller's address is not on it.
api_key_lookup_failedThe key could not be looked up in the database.
/v1/audit/portal/exchangeExchange 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.
Content-TypeMust be application/json.
tokenThe 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.
// 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.
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhcHNfMDFKMFk1UjdUOVcxWTNBNUM3RTlHMUozS00iLCJhdWQiOiJhdWRpdF9wb3J0YWwiLCJvcmdfaWQiOiJhb3JnXzAxSjBYVzlLM1JRNVQ3VjhZMkM0RTZHOEhNIiwiaW50ZW50IjoiYXVkaXRfbG9ncyIsImV4cCI6MTc5MDA2ODA0N30.q7Yw3d5Z1kH0m2Rr8sVv4cXx6bNn9pLl1jGg0fTt2uE",
"token_type": "Bearer",
"intent": "audit_logs",
"expires_in": 3600
}
tokenThe portal JWT (audience audit_portal) carrying the session id, org id, intent and expiry; send it as Authorization: Bearer on the portal routes.
token_typeAlways Bearer. One of Bearer.
intentThe intent the session was minted with. One of audit_logs, log_streams.
expires_inSeconds until the JWT expires, equal to the session's session_duration_seconds.
invalid_or_used_linkNo session matches the token, the link expired before it was opened, or it was already exchanged.
rate_limitedThe caller's IP exceeded 10 exchanges per second (or 600 per minute); the body carries retry_after.
db_errorThe session lookup failed.
token_sign_failedThe JWT could not be signed.
/v1/audit/portal/eventsList events through the portal
Returns the session's org events newest first with the same filters and cursor as the API list.
AuthorizationBearer followed by the portal JWT from the exchange.
actionsComma-separated list of action strings; only events whose action is in the list are returned.
actor_idOnly events whose actor.id equals this value.
target_idOnly events with a target whose id equals this value.
occurred_afterInclusive lower bound on occurred_at, RFC 3339 (no range_start alias on this route).
occurred_beforeInclusive upper bound on occurred_at, RFC 3339 (no range_end alias on this route).
limitPage size.
cursorOpaque 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.
// 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);
{
"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
}
eventsThe page of events, ordered by occurred_at descending then id descending.
events[].idEvent id, aevt_ followed by a ULID; minted at ingest before the event is queued.
events[].org_idThe aorg_ id of the audit org the event belongs to (not your organization_id).
events[].seqPosition in the org's log, assigned by the signer in commit order; per org it starts at 1 and has no gaps.
events[].schema_idAlways invoance.audit/1; it is inside the signed bytes as a domain tag. One of invoance.audit/1.
events[].occurred_atWhen the event happened in your system, normalized to UTC with exactly three fractional digits and a Z suffix.
events[].ingested_atServer time when the event was accepted, in the same canonical form; this value is signed.
events[].actionThe action string as sent, for example user.signed_in.
events[].actorThe actor object as sent: type and id, plus name and metadata when given.
events[].targetsThe targets array as sent; empty when the event had none.
events[].contextThe context object as sent (location, user_agent), or null when omitted.
events[].metadataThe flat metadata object as sent, or null when omitted.
events[].payload_hashSHA-256 of the canonical signed bytes, 64 hex characters.
events[].signatureEd25519 signature over the canonical bytes, 128 hex characters.
events[].signing_public_keyThe 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_cursorOpaque cursor for the next page, or null on the last page.
invalid_timestampoccurred_after or occurred_before is not RFC 3339.
invalid_cursorcursor is not a cursor this endpoint issued.
missing_portal_tokenNo Authorization: Bearer header was sent.
invalid_portal_tokenThe token is not a portal JWT, has expired, or has the wrong audience.
wrong_intentThe portal link was minted with intent log_streams, which cannot read events.
rate_limitedThe 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_errorA database query failed.
/v1/audit/portal/events/{id}Get an event through the portal
Returns one event of the session's org.
AuthorizationBearer followed by the portal JWT from the exchange.
idThe aevt_ id of the event.
- An event of a different org returns the same 404 as an unknown id.
// 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);
{
"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"
}
idEvent id, aevt_ followed by a ULID; minted at ingest before the event is queued.
org_idThe aorg_ id of the audit org the event belongs to (not your organization_id).
seqPosition in the org's log, assigned by the signer in commit order; per org it starts at 1 and has no gaps.
schema_idAlways invoance.audit/1; it is inside the signed bytes as a domain tag. One of invoance.audit/1.
occurred_atWhen the event happened in your system, normalized to UTC with exactly three fractional digits and a Z suffix.
ingested_atServer time when the event was accepted, in the same canonical form; this value is signed.
actionThe action string as sent, for example user.signed_in.
actorThe actor object as sent: type and id, plus name and metadata when given.
targetsThe targets array as sent; empty when the event had none.
contextThe context object as sent (location, user_agent), or null when omitted.
metadataThe flat metadata object as sent, or null when omitted.
payload_hashSHA-256 of the canonical signed bytes, 64 hex characters.
signatureEd25519 signature over the canonical bytes, 128 hex characters.
signing_public_keyThe tenant's Ed25519 public key as recorded with the row, 64 hex characters; shown for display, verification uses the key registered in tenant_keys.
missing_portal_tokenNo Authorization: Bearer header was sent.
invalid_portal_tokenThe token is not a portal JWT, has expired, or has the wrong audience.
wrong_intentThe portal link was minted with intent log_streams, which cannot read events.
not_foundNo event with that id belongs to the session's org.
rate_limitedThe 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_errorA database query failed.
/v1/audit/portal/events/{id}/verifyVerify an event through the portal
Runs the same pinned-key verification as the API verify for one event of the session's org.
AuthorizationBearer followed by the portal JWT from the exchange.
idThe aevt_ id of the event.
- The key is read from tenant_keys for the event's tenant, exactly as on the API route.
// 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);
{
"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"
}
event_idThe id from the path.
validTrue 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.
reasonWhy the check failed, or null when valid. One of canonicalization_failed, payload_hash_mismatch, wrong_domain, signature_invalid.
schemaAlways invoance.audit/1. One of invoance.audit/1.
payload_hashSHA-256 of the recomputed canonical bytes; empty when canonicalization failed.
signed_dataThe canonical signed object parsed back to JSON (keys sorted, nulls removed, schema_id included); null when canonicalization failed.
key_sourceAlways tenant_keys: the key used is the tenant's registered key, never the one stored on the row. One of tenant_keys.
missing_portal_tokenNo Authorization: Bearer header was sent.
invalid_portal_tokenThe token is not a portal JWT, has expired, or has the wrong audience.
wrong_intentThe portal link was minted with intent log_streams, which cannot read events.
not_foundNo event with that id belongs to the session's org.
rate_limitedThe 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_errorA database query failed, or the event's tenant has no registered key.
/v1/audit/portal/orgGet the portal's org and issuer
Returns the branding of the tenant that issued the link and the org the session may read.
AuthorizationBearer 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.
// 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);
{
"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"
}
issuerThe Invoance tenant whose product the viewer is embedded in.
issuer.nameThe tenant's issuer_name, or null when not set.
issuer.logo_urlThe tenant's logo URL, or null.
issuer.domain_verifiedTrue once the tenant's primary domain passed DNS verification.
orgThe end-customer org the session is scoped to.
org.nameThe org's display name, falling back to its organization_id.
org.organization_idThe tenant's own id for the org.
org.archived_atWhen the org was archived, or null.
intentThe session's intent. One of audit_logs, log_streams.
missing_portal_tokenNo Authorization: Bearer header was sent.
invalid_portal_tokenThe token is not a portal JWT, has expired, or has the wrong audience.
not_foundThe org in the token no longer exists.
rate_limitedThe 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_errorA database query failed.
/v1/audit/portal/streamsList streams through the portal
Returns up to 100 streams of the session's org, newest first, without secrets.
AuthorizationBearer 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.
// 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);
}
{
"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"
}
]
}
streamsThe streams, ordered by created_at descending.
streams[].idStream id, astr_ followed by a ULID.
streams[].typeDestination type; only webhook can be created. One of webhook.
streams[].endpointThe https URL deliveries are posted to, as validated at create time.
streams[].stateactive 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_seqThe highest seq delivered so far; the next delivery starts at cursor_seq + 1.
streams[].failure_streakConsecutive failed deliveries; reset to 0 on success and used to compute the backoff.
streams[].last_errorMessage from the last failed delivery, or null.
streams[].last_delivery_atWhen the last successful delivery was recorded, or null.
streams[].created_atWhen the stream was created.
missing_portal_tokenNo Authorization: Bearer header was sent.
invalid_portal_tokenThe token is not a portal JWT, has expired, or has the wrong audience.
wrong_intentThe portal link was not minted with intent log_streams.
rate_limitedThe 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_errorA database query failed.
/v1/audit/portal/streamsCreate 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.
AuthorizationBearer followed by a portal JWT minted with intent log_streams.
Content-TypeMust be application/json.
typeDestination type; only webhook is accepted. One of webhook.
urlAbsolute 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.
// 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);
{
"id": "astr_01J0Y3N5P7R9T1V3X5Z7B9D1FG",
"type": "webhook",
"endpoint": "https://siem.example.com/hooks/invoance",
"state": "active",
"cursor_seq": 41,
"signing_secret": "whsec_4c1d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d"
}
idStream id, astr_ followed by a ULID.
typeDestination type; only webhook can be created. One of webhook.
endpointThe https URL deliveries are posted to, as validated at create time.
stateactive 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_seqThe org's last_seq at creation; nothing older is replayed.
signing_secretwhsec_ followed by 64 hex characters, returned only in this response.
unsupported_stream_typetype is not webhook.
invalid_urlurl is not an absolute URL.
not_httpsurl does not use https.
no_hosturl has no host.
resolution_failedThe host did not resolve in DNS.
forbidden_destinationThe host resolves to a private, loopback, link-local or cloud metadata address.
too_many_streamsThe org already has as many streams as the tenant's plan allows.
missing_portal_tokenNo Authorization: Bearer header was sent.
invalid_portal_tokenThe token is not a portal JWT, has expired, or has the wrong audience.
wrong_intentThe portal link was not minted with intent log_streams.
not_foundThe org in the token no longer exists.
org_archivedThe org is archived; unarchive it first.
rate_limitedThe 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_errorA database query failed.
/v1/audit/portal/streams/{id}Delete a stream through the portal
Removes one of the session's org streams and returns the deleted id.
AuthorizationBearer followed by a portal JWT minted with intent log_streams.
idThe astr_ id of the stream.
- Deleting is allowed on an archived org.
// 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);
{
"deleted": true,
"id": "astr_01J0Y3N5P7R9T1V3X5Z7B9D1FG"
}
deletedAlways true on success.
idThe astr_ id that was deleted.
missing_portal_tokenNo Authorization: Bearer header was sent.
invalid_portal_tokenThe token is not a portal JWT, has expired, or has the wrong audience.
wrong_intentThe portal link was not minted with intent log_streams.
not_foundNo stream with that id belongs to the session's org.
rate_limitedThe 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_errorA database query failed.
/v1/audit/portal/streams/{id}/testTest a stream through the portal
Posts one synthetic stream.test event to the stream's destination and returns what it answered.
AuthorizationBearer followed by a portal JWT minted with intent log_streams.
idThe astr_ id of the stream.
- Same synthetic event, signing and 15 second wait as the API test route; the cursor and state are untouched.
// 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);
{
"delivered": true,
"http_status": 200,
"error": null,
"retryable": false
}
deliveredTrue when the destination answered with a 2xx.
http_statusThe destination's status code, or null when no response arrived.
errorWhy the delivery failed, or null on success.
retryableTrue when the dispatcher would retry this failure; false on success or a permanent failure.
unsupported_stream_typeThe stream is not a webhook stream.
no_endpointThe stream has no endpoint stored.
missing_portal_tokenNo Authorization: Bearer header was sent.
invalid_portal_tokenThe token is not a portal JWT, has expired, or has the wrong audience.
wrong_intentThe portal link was not minted with intent log_streams.
not_foundThe org in the token no longer exists, or no stream with that id belongs to it.
org_archivedThe org is archived; unarchive it first.
rate_limitedThe 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_errorA database query failed or the stream's stored secret could not be decrypted.
/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/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