Dashboard Get started

Each event can be signed with your organization's Ed25519 private key at capture time. TokenID stores the signature alongside the event and periodically computes a Merkle root over each window — giving you per-event and per-batch proof that nothing has been altered. Useful for compliance evidence, dispute resolution, and auditor handoff.

How it works

  • The SDK signs each event with your org's Ed25519 private key before it leaves the process, attaching the signature, a per-event nonce, and a signed_at timestamp.
  • The backend stores those fields on the row. Any later read can be re-verified against the public key registered for your org.
  • Periodically a Merkle root is computed over every event in a time window and (optionally) pushed to your webhook. The root, window bounds, and row count are stored immutably.

Register a signing key

POST/api/v1/signing-keys

Idempotent — re-registering the same key returns the existing record. The fastest path is the CLI helper, which generates a keypair and posts the public half:

tokenid keygen --register

Or manually:

curl -X POST https://token.audit.id/api/v1/signing-keys \
  -H "Authorization: Bearer td_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{"public_key": "MCowBQYDK2VwAyEA…", "algorithm": "ed25519", "label": "prod-2026-05"}'
Field Type Notes
public_key string 64-char hex OR base64 of 32 raw Ed25519 bytes
algorithm string Must be "ed25519" (only supported value)
label string Optional, max 128 chars

The response carries the signing_key_id — the SDK passes this back on every signed event so the backend knows which key to verify against.

Private keys never leave your environment. TokenID only ever sees the public half.

Verify a single event

GET/api/v1/events/{event_id}/verify

Re-runs the Ed25519 verification against the canonical event fields and the stored signature.

{
  "event_id": 482910,
  "has_signature": true,
  "verified": true,
  "key_fingerprint": "9f3c1ab2…",
  "message": "Signature valid."
}

If the event was ingested before signing was enabled, has_signature is false. If the row was tampered with after ingest, verified returns false with "Signature INVALID — event data may have been tampered.".


Digest webhooks

Subscribe an HTTPS endpoint to receive each Merkle digest as it's signed.

```bash curl -X POST https://token.audit.id/api/v1/org/{org_id}/digest-webhook \ -H "Authorization: Bearer td_live_xxxx" \ -H "Content-Type: application/json" \ -d '{"delivery_url": "https://yourapp.example/tokenid/digest"}' ``` `delivery_url` must use HTTPS. The stored URL is encrypted at rest.
```bash curl -X DELETE https://token.audit.id/api/v1/org/{org_id}/digest-webhook/{wh_id} \ -H "Authorization: Bearer td_live_xxxx" ``` Returns `204`. Soft-deactivate — the row is kept for audit, just flipped to inactive.

Digest history

Two endpoints expose stored digests:

Endpoint Use case
GET /api/v1/org/{org_id}/digests Simple list, most recent first, up to limit=500
GET /api/v1/org/{org_id}/digest-history Paginated, supports since / until, returns total

Each digest row carries:

Field Description
window_start / window_end Time bounds of the events covered
merkle_root Hex root over canonical leaves of every event in the window
row_count Number of events folded into the root
tokenid_signature TokenID's own signature over the digest (countersigned)
delivered_at When the digest was pushed to your webhook (null if pending)

Verify a batch

POST/api/v1/org/{org_id}/digest/verify

The auditor's primary tool. Given a digest_id and a list of event_ids claimed to have been in that window, the endpoint recomputes the Merkle root from every event in the window and checks (1) that the recomputed root matches what was stored, and (2) that every requested event was in the window.

curl -X POST https://token.audit.id/api/v1/org/{org_id}/digest/verify \
  -H "Authorization: Bearer td_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "digest_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "event_ids": [482910, 482911, 482912]
  }'
{
  "digest_verified": true,
  "events_included": true,
  "digest_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "window_start": "2026-05-20T00:00:00Z",
  "window_end": "2026-05-20T01:00:00Z",
  "stored_root": "f4c2…",
  "computed_root": "f4c2…",
  "window_event_count": 1842,
  "requested_events_found": 3,
  "events_requested": 3,
  "message": "Digest integrity verified and all requested events confirmed in window."
}
✓ If `digest_verified=true` and `events_included=true`, the digest hasn't been tampered with AND every event you cited was inside that window. This is the proof you hand an auditor.
If a row in the window was subsequently erased — for example via [GDPR subject erasure](/guides/retention) — `digest_verified` will return `false`. The original digest row is also marked `invalidated_at` with reason `gdpr_subject_erasure`, which is the expected and auditable behavior.

See Auditor export for the JSONL companion endpoint that hands an auditor every event in a date range, ready to be verified against the digests above.