EAS Audit Lite — API documentation

Hash-chained tamper-evident audit log API. All requests authenticate with a Bearer token. Base URL: https://audit.eliteagenticsolutions.com

Authentication

Every endpoint requires a Bearer token in the Authorization header. You receive your API key via email after Stripe checkout.

Authorization: Bearer empire_audit_xxxxxxxxxxxxxxxxxxxxxx

Treat the key like a password. We hash it server-side and only show it once.

Write an event

POST/audit/v1/events

Append an event to the tenant's hash chain. Returns the event's position, hash, and previous-hash link.

Request body:

FieldTypeRequiredNotes
event_typestringyese.g. user.login, config.change, order.placed
payloadobjectyesArbitrary JSON. Stored as-is and hashed.
ts_msintegernoUnix milliseconds. Defaults to now.
curl -X POST 'https://audit.eliteagenticsolutions.com/audit/v1/events' \
  -H "Authorization: Bearer empire_audit_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "user.login",
    "payload": {"user_id":"u123","ip":"1.2.3.4","ua":"chrome/130"}
  }'

Response:

{
  "event_id": 42,
  "chain_position": 42,
  "entry_hash": "a3f4ce6b...",
  "prev_hash": "b89c1d2e...",
  "payload_hash": "7d12af90...",
  "ts_ms": 1730000000123,
  "tenant_id": "cus_..."
}

Query events

GET/audit/v1/events

List events, newest first. Pagination via cursor (last seen chain_position).

Query paramNotes
fromLower bound on ts_ms (inclusive)
toUpper bound on ts_ms (inclusive)
event_typeExact match filter
limitMax 1000 (default 100)
cursorSkip rows with chain_position <= cursor
curl 'https://audit.eliteagenticsolutions.com/audit/v1/events?event_type=user.login&limit=50' \
  -H "Authorization: Bearer empire_audit_xxx"

Get a single event

GET/audit/v1/events/{event_id}

Returns one event with on-the-fly hash re-verification. Response includes integrity_ok boolean.

curl 'https://audit.eliteagenticsolutions.com/audit/v1/events/42' \
  -H "Authorization: Bearer empire_audit_xxx"

Verify the chain

GET/audit/v1/chain/verify

Walk every event from genesis and check hash continuity. Returns OK or pinpoints the break.

curl 'https://audit.eliteagenticsolutions.com/audit/v1/chain/verify' \
  -H "Authorization: Bearer empire_audit_xxx"

# Success:
{"status": "OK", "checked": 12345, "tenant_id":"cus_xx", "head_hash": "..."}

# Break:
{"status": "BREAK", "break_at_position": 1234, "reason":"entry_hash_mismatch", "checked": 1233}

Export the chain

POST/audit/v1/export?fmt=json|csv

Dump the full chain (or a time-bounded slice) for compliance review.

curl -X POST 'https://audit.eliteagenticsolutions.com/audit/v1/export?fmt=csv&from=1700000000000' \
  -H "Authorization: Bearer empire_audit_xxx" \
  -o audit-export.csv

Check your usage

GET/audit/v1/usage

Current month's event count, tier limits, storage usage.

{
  "tier": "starter",
  "events_this_month": 12345,
  "events_cap_monthly": 100000,
  "events_stored_total": 250000,
  "events_stored_cap": 1000000,
  "rate_per_min": 60,
  "features": ["api","query","verify","export"]
}

Slack alert integration (Growth + Scale)

Send real-time alerts to a Slack channel when audit events of selected types are written. Configure once, then alerts fire fire-and-forget on every matching event-write.

POST/audit/v1/integrations/slack

Body:

FieldTypeNotes
webhook_urlstringSlack incoming-webhook URL (e.g. https://hooks.slack.com/services/T0/B0/XXXXX)
alert_event_typesarray<string>Which event_type values trigger alerts. Use ["*"] to alert on every event. Events tagged payload.alert=true also fire regardless of this filter.

Returns 200 on success. Returns 400 if the test POST to Slack fails. Returns 402 for Starter tier.

curl -X POST 'https://audit.eliteagenticsolutions.com/audit/v1/integrations/slack' \
  -H "Authorization: Bearer empire_audit_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXXXXXX",
    "alert_event_types": ["chain.tampered", "user.deleted", "config.change"]
  }'

GET/audit/v1/integrations/slack

Returns current config (webhook URL is redacted). Returns 402 for Starter.

DELETE/audit/v1/integrations/slack

Removes the webhook config. Alerts stop firing immediately.

Alert message format:

{
  "text": ":bell: EAS Audit Alert: chain.tampered",
  "blocks": [
    {
      "type": "section",
      "text": {"type": "mrkdwn", "text": "*Event*: `chain.tampered`\n*Customer*: you@example.com\n*Time*: 2026-05-10T01:54:15Z\n*Chain Position*: #42"}
    },
    {
      "type": "section",
      "text": {"type": "mrkdwn", "text": "*Payload*:\n```{...}```"}
    }
  ]
}

Outbound webhooks (Scale)

Real-time replication: every audit event is POSTed to your endpoint with an HMAC-SHA256 signature in X-Audit-Signature. Retries with exponential backoff (1, 5, 15, 60, 300 seconds). Webhook auto-disables after 10 consecutive delivery failures and emails the tenant on disable.

POST/audit/v1/webhooks

Body:

FieldTypeNotes
target_urlstringYour endpoint, e.g. https://my-app.com/audit-events
event_type_filterarray<string>Which events to forward. ["*"] = all events.

Returns 201 with webhook_id + secret. The secret is shown ONCE — save it. Returns 402 for non-Scale tiers.

{
  "webhook_id": "8be51edc-e796-47f4-bc5a-304886d87830",
  "secret": "whsec_audit_xxxxxxxxxxxx",
  "target_url": "https://my-app.com/audit-events",
  "event_type_filter": ["*"],
  "enabled": true,
  "created_at": "2026-05-10T01:54:50Z"
}

GET/audit/v1/webhooks

List your configured webhooks. Shows enabled/disabled state, last delivery, consecutive failure count.

DELETE/audit/v1/webhooks/{id}

Remove a webhook.

POST/audit/v1/webhooks/{id}/test

Queue a synthetic test delivery to verify your endpoint. Inspect result via /deliveries.

GET/audit/v1/webhooks/{id}/deliveries

Returns most-recent delivery attempts with HTTP status, response excerpt, and timestamps. Useful for debugging.

Delivery payload

Body is canonical JSON (sorted keys, no whitespace). Includes entry_hash so you can verify chain position on your side.

POST https://my-app.com/audit-events
Content-Type: application/json
X-Audit-Signature: 99b5170a37a5a23f453a9bab9b415736efdaaf45a7dfd6791521dc221adab148
User-Agent: EAS-Audit-Lite-Webhook/1.0

{"chain_position":1,"entry_hash":"093a7b...","event_id":6,"event_type":"order.placed","payload":{"amount":4999,"order_id":"o1"},"tenant_id":"al_xxx","ts_ms":1778464494469}

Verify the HMAC signature

Python:

import hmac, hashlib

SECRET = "whsec_audit_xxxxxxxxxxxx"  # stored from creation response

def verify(body_bytes: bytes, signature_header: str) -> bool:
    expected = hmac.new(SECRET.encode(), body_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)

# In FastAPI / Flask:
sig = request.headers.get("X-Audit-Signature", "")
body = request.body()  # raw bytes
if not verify(body, sig):
    return Response(status=401)

Node.js:

const crypto = require("crypto");
const SECRET = "whsec_audit_xxxxxxxxxxxx";

function verify(bodyBuffer, signatureHeader) {
  const expected = crypto.createHmac("sha256", SECRET)
    .update(bodyBuffer)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signatureHeader, "hex"),
  );
}

// Express:
app.post("/audit-events", express.raw({type: "application/json"}), (req, res) => {
  const sig = req.headers["x-audit-signature"];
  if (!verify(req.body, sig)) return res.status(401).end();
  // safe to parse and use
  const event = JSON.parse(req.body.toString());
  // ...
});

Custom retention (Scale)

Choose how long EAS Audit Lite stores your events. A daily cron deletes events older than your retention_days setting. Pruned events cannot be recovered — export first if you need them long-term.

TierRetention
Starter30 days (fixed)
Growth90 days (fixed)
ScaleConfigurable: 30 / 90 / 180 / 365 / forever (36500 days)

PATCH/audit/v1/account/retention

curl -X PATCH 'https://audit.eliteagenticsolutions.com/audit/v1/account/retention' \
  -H "Authorization: Bearer empire_audit_xxx" \
  -H "Content-Type: application/json" \
  -d '{"retention_days": 365}'

Returns 400 if the value is not in your tier's allowed list, 402 if you're not on Scale.

GET/audit/v1/account/retention

Returns your current config + the allowed options for your tier.

{
  "tenant_id": "al_xxx",
  "tier": "scale",
  "retention_days_configured": 180,
  "retention_days_effective": 180,
  "options_available_for_tier": [30, 90, 180, 365, 36500]
}

Query the age of stored events

Use the existing /v1/events?from=<ts_ms> endpoint with the timestamp from NOW - retention_days. Compute on your side:

# Python
import time
from_ms = int((time.time() - 180*86400) * 1000)
url = f"https://audit.eliteagenticsolutions.com/audit/v1/events?from={from_ms}"

Chain integrity after pruning

When old events are deleted, the lowest remaining event is re-anchored to your tenant's GENESIS hash and its entry_hash is recomputed. The chain stays self-consistent for /v1/chain/verify. Cryptographic links to deleted events are broken — export the chain regularly if you need historical proof.

Error codes

CodeMeaning
400Malformed request body or value not allowed
401Missing / invalid Bearer token
402Tier upgrade required (Growth or Scale feature)
403Admin endpoint accessed without admin token
404Event ID / webhook not found in your tenant
429Rate limit or monthly quota exceeded

Code samples

Python

import requests
r = requests.post(
    "https://audit.eliteagenticsolutions.com/audit/v1/events",
    headers={"Authorization": "Bearer empire_audit_xxx"},
    json={"event_type": "user.login", "payload": {"user_id": "u123"}},
)
print(r.json())  # {"event_id": ..., "chain_position": ..., ...}

Node.js

const res = await fetch("https://audit.eliteagenticsolutions.com/audit/v1/events", {
  method: "POST",
  headers: {
    "Authorization": "Bearer empire_audit_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({event_type: "user.login", payload: {user_id: "u123"}}),
});
console.log(await res.json());

Go

body, _ := json.Marshal(map[string]interface{}{
  "event_type": "user.login",
  "payload": map[string]string{"user_id": "u123"},
})
req, _ := http.NewRequest("POST", "https://audit.eliteagenticsolutions.com/audit/v1/events", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer empire_audit_xxx")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

Hash chain concepts

Every event you write computes:

payload_hash = sha256(canonical_json(payload))
entry_hash   = sha256(tenant_id | prev_hash | event_type | ts_ms | payload_hash)

The very first event uses prev_hash = sha256("GENESIS::" + tenant_id). Every subsequent event's prev_hash is the previous event's entry_hash.

To tamper undetectably, an attacker would need to recompute every hash after their edit — but they cannot rewrite hashes they have already exported, anchored, or shown to auditors. Tampering breaks loud.

Canonical JSON: keys sorted, no whitespace. This guarantees the same payload always hashes to the same value regardless of key order.