Septor

Septor is the platform's immutable audit trail service. It captures every important action as a hash-chained event, making tampering detectable and providing compliance-ready proof of who did what, when, and why.

Overview

Septor sits behind the Janus gateway at /v1/events. Every event is linked to the previous event for the same entity via SHA-256 hash, forming an append-only chain. If any event is modified, the chain breaks and the verify endpoint reports exactly where.

Architecture

Your Service
    |
    v
  Septor  -->  Validate (Zod)  -->  Hash chain  -->  MongoDB
    |
    v
Seven Elements captured automatically:
  WHO   - actor (user email, service name)
  WHAT  - event type
  WHEN  - server timestamp
  WHERE - IP address, user agent
  HOW   - HTTP method and endpoint
  WHY   - reason (optional)
  HASH  - SHA-256 linking to previous event

URLs

EnvironmentURL
Productionhttps://septor.insureco.io
Production (platform)https://septor.tawa.insureco.io
Sandboxhttps://septor.sandbox.tawa.insureco.io
In-Cluster (prod)http://septor.septor-prod.svc.cluster.local:8473
In-Cluster (sandbox)http://septor.septor-sandbox.svc.cluster.local:8473

Quick Start

Add Septor to your service in three steps:

1. Declare the dependency

Add Septor to your service's catalog-info.yaml so the builder injects the connection URL automatically:

# catalog-info.yaml
spec:
  internalDependencies:
    - service: septor
      port: 8473
After deploy: The builder injects SEPTOR_URL as an environment variable pointing to the in-cluster service address.

2. Install the SDK

npm install septor

3. Emit your first event

import { Septor } from 'septor'

const septor = new Septor({
  apiUrl: process.env.SEPTOR_URL || 'http://localhost:8473',
  namespace: 'your-service-name',
})

await septor.emit('entity.created', {
  entityId: 'abc-123',
  data: { key: 'value' },
  metadata: {
    who: '[email protected]',
    why: 'New entity created by user',
  },
})

POST /v1/events/emit

Emit a new event to the audit trail. The event is automatically hash-chained to the previous event for the same entity.

Request body

FieldTypeRequiredDescription
namespacestringYesYour service name (e.g. flood-rater, bind-desk)
eventTypestringYesSemantic event name (e.g. rating.created)
entityIdstringYesThe entity being audited (quoteId, policyId, etc.)
dataobjectYesEvent payload (any JSON)
metadata.whostringYesActor — user email, service name, or API key
metadata.whystringNoReason for the action (recommended for compliance)

Example

curl -X POST https://septor.insureco.io/v1/events/emit \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "flood-rater",
    "eventType": "rating.created",
    "entityId": "quote_123",
    "data": {
      "premium": 850,
      "buildingValue": 300000,
      "zone": "AE"
    },
    "metadata": {
      "who": "[email protected]",
      "why": "Initial flood quote generated"
    }
  }'

Response (201)

{
  "success": true,
  "data": {
    "eventId": "evt_5d5fb86239fde3f5e53e7fbb",
    "eventHash": "0e43d16c8185046b4e5c1fe...",
    "chainIndex": 0,
    "previousHash": null,
    "createdAt": "2026-02-16T17:06:00.080Z"
  }
}
Chain index: Starts at 0 for the first event on an entity and increments with each subsequent event. The previousHash links to the prior event's hash, forming the chain.

GET /v1/events/query

Query events by namespace, entity, type, and date range.

Query parameters

ParamTypeRequiredDescription
namespacestringYesFilter by namespace
entityIdstringNoFilter by entity ID
eventTypestringNoFilter by type (supports * wildcard)
fromstringNoStart date (ISO 8601)
tostringNoEnd date (ISO 8601)
limitstringNoMax results (default: 50)

Example

GET /v1/events/query?namespace=flood-rater&entityId=quote_123&eventType=rating.*

Response (200)

{
  "success": true,
  "data": {
    "events": [
      {
        "eventId": "evt_5d5fb862...",
        "namespace": "flood-rater",
        "eventType": "rating.created",
        "entityId": "quote_123",
        "data": { "premium": 850 },
        "eventHash": "0e43d16c...",
        "previousHash": null,
        "chainIndex": 0,
        "metadata": {
          "who": "[email protected]",
          "what": "rating.created",
          "when": { "server": "2026-02-16T17:06:00Z" },
          "where": { "ipAddress": "10.244.2.167" },
          "how": { "method": "POST", "endpoint": "/v1/events/emit" },
          "why": "Initial flood quote generated"
        },
        "createdAt": "2026-02-16T17:06:00.080Z"
      }
    ],
    "total": 1,
    "returned": 1
  }
}

GET /v1/events/verify/:entityId

Verify the hash chain integrity for an entity. Recalculates all hashes and confirms no events have been tampered with.

Example

GET /v1/events/verify/quote_123?namespace=flood-rater

Response (200)

{
  "success": true,
  "data": {
    "entityId": "quote_123",
    "valid": true,
    "totalEvents": 3,
    "firstEvent": "2026-02-16T17:06:00.080Z",
    "lastEvent": "2026-02-16T18:30:00.000Z"
  }
}
Tamper detection: If valid is false, the response includes a brokenAt field indicating the chain index where tampering was detected. This should trigger an immediate investigation.

GET /v1/events/chain/:entityId

Get the full event chain for an entity, including hashes for visualization and debugging.

Response (200)

{
  "success": true,
  "data": {
    "entityId": "quote_123",
    "chain": [
      {
        "chainIndex": 0,
        "eventType": "rating.created",
        "eventHash": "0e43d16c...",
        "previousHash": null,
        "createdAt": "2026-02-16T17:06:00Z"
      },
      {
        "chainIndex": 1,
        "eventType": "rating.updated",
        "eventHash": "a7b3f921...",
        "previousHash": "0e43d16c...",
        "createdAt": "2026-02-16T18:30:00Z"
      }
    ],
    "valid": true,
    "totalEvents": 2
  }
}

Seven Elements

Every event automatically captures seven metadata elements for compliance:

ElementFieldSource
Whometadata.whoProvided by caller
Whatmetadata.whatDerived from eventType
Whenmetadata.when.serverServer timestamp (automatic)
Wheremetadata.where.ipAddressRequest IP (automatic)
Howmetadata.how.methodHTTP method + endpoint (automatic)
Whymetadata.whyProvided by caller (optional)
HasheventHashSHA-256 of event contents (automatic)

Hash Chain

Each event's hash includes the previous event's hash, creating an immutable chain. Modifying any event breaks the chain for all subsequent events.

hash = SHA-256(JSON.stringify({
  eventType,
  entityId,
  data,
  previousHash || '',
  timestamp,
  who,
}))
Event 0              Event 1              Event 2
+-----------+        +-----------+        +-----------+
| hash: abc |<-------| prev: abc |<-------| prev: def |
| prev: null|        | hash: def |        | hash: 789 |
+-----------+        +-----------+        +-----------+

If Event 1 is tampered with, its hash changes,
breaking the link from Event 2 onward.

Event Naming Conventions

Use consistent <entity>.<action> patterns across services:

ServiceEvent Types
flood-raterrating.created, rating.updated, quote.submitted
bind-deskpolicy.bound, policy.cancelled, endorsement.created
policypaypayment.processed, payment.refunded, invoice.generated
iec-extractdocument.uploaded, document.extracted, field.corrected

Common actions: created, updated, deleted, approved, rejected, submitted, bound, cancelled, exported, viewed

SDK

The septor npm package provides a typed client for all endpoints.

Install

npm install septor

Setup

import { Septor } from 'septor'

const septor = new Septor({
  apiUrl: process.env.SEPTOR_URL || 'http://localhost:8473',
  namespace: 'flood-rater',
})

Emit events

const result = await septor.emit('rating.created', {
  entityId: 'quote_123',
  data: { premium: 850, zone: 'AE' },
  metadata: {
    who: '[email protected]',
    why: 'New flood insurance quote',
  },
})
// result.data.eventId, result.data.eventHash, result.data.chainIndex

Query events

const history = await septor.query({
  entityId: 'quote_123',
  eventType: 'rating.*',   // wildcard support
  limit: '100',
})
// history.data.events[], history.data.total

Verify chain

const result = await septor.verify('quote_123')

if (result.data.valid) {
  // Chain intact - safe for compliance
} else {
  // Chain broken at result.data.brokenAt
}

Get full chain

const chain = await septor.getChain('quote_123')
// chain.data.chain[], chain.data.valid, chain.data.totalEvents

CLI

The septor CLI is useful for debugging and manual operations:

# Install globally
npm install -g septor

# Emit a test event
septor emit rating.created \
  -n flood-rater \
  -e quote_123 \
  -d '{"premium": 850}' \
  -w [email protected] \
  --url https://septor.insureco.io

# Query events
septor query -n flood-rater -e quote_123

# Verify chain integrity
septor verify quote_123 -n flood-rater

Error Handling

All errors follow a consistent format:

{
  "success": false,
  "error": "Validation error",
  "details": [
    {
      "path": ["namespace"],
      "message": "Invalid input: expected string, received undefined"
    }
  ]
}
StatusMeaning
201Event emitted successfully
200Query / verify / chain succeeded
400Validation error (missing or invalid fields)
404Route not found
500Internal server error
Best practice: Septor failures should never block your service's primary operations. Wrap emit calls in a try/catch and log failures rather than crashing.
// Fire-and-forget pattern
septor.emit('entity.created', payload).catch((err) =>
  console.error('Audit event failed:', err.message)
)

Related