Witness API Reference

Base URL: https://api.solenoid.systems

All authenticated endpoints require a Bearer token. See Authentication. See Rate Limits.

POST /v1/witness/sys/init

Initialize a witness chain with an Ed25519 keypair. Required before first notarization.

Auth: Required | Cost: 1 credit

No request body required.

curl -X POST https://api.solenoid.systems/v1/witness/sys/init \
  -H "Authorization: Bearer sm_your_api_key_here"

Response (201 Created):

{
  "success": true,
  "publicKey": {
    "kty": "OKP",
    "crv": "Ed25519",
    "x": "d75Q3Cl07Ue4KjbAQfYN8X2..."
  },
  "createdAt": 1672531200,
  "chain_id": "wc_9tQvXm2LpR4sK7nB1cD8fG3hJ5wY6zA0"
}

chain_id is the chain’s public identifier. Publish it alongside your receipts: it is what a verifier needs to fetch your public key, and it is the only thing that addresses your chain without an API key. It is derived from your account and project, so it is stable, and it reveals neither.

Already initialized (200 OK):

{
  "success": false,
  "chain_id": "wc_9tQvXm2LpR4sK7nB1cD8fG3hJ5wY6zA0"
}

The chain id is returned whether or not this call created the chain, so calling init again is a safe way to recover it.

Initialization is one-time per account. If you’ve already initialized, the endpoint returns success: false with 200 status.

POST /v1/witness/config

Configure S3-compatible storage for receipt delivery.

Auth: Required | Cost: Free

FieldTypeRequiredDescription
endpointstringYesS3 endpoint URL (HTTPS required)
bucketstringYesBucket name
regionstringYesAWS region
accessKeyIdstringYesAccess key ID
secretAccessKeystringYesSecret access key
curl -X POST https://api.solenoid.systems/v1/witness/config \
  -H "Authorization: Bearer sm_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://s3.us-west-2.amazonaws.com",
    "bucket": "my-audit-logs",
    "region": "us-west-2",
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  }'

Response:

{
  "configured": true
}

Receipts are delivered to s3://{bucket}/witness/receipts/{sequence_id}.json. If S3 is unavailable, receipts are buffered in purgatory (R2).

SSRF validation ensures endpoint is a public HTTPS URL. Private IPs and localhost are blocked.

POST /v1/witness/log

Notarize a JSON log entry. Returns a signed receipt with Ed25519 signature.

Auth: Required | Cost: 1 credit

FieldTypeRequiredDescription
logobjectYesAny JSON object to notarize
curl -X POST https://api.solenoid.systems/v1/witness/log \
  -H "Authorization: Bearer sm_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "log": {
      "event": "user.login",
      "user_id": "usr_123",
      "timestamp": 1672531200
    }
  }'

Response (200 OK - delivered):

{
  "version": 1,
  "sequence_id": "42",
  "content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
  "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "chain_hash": "e4f6a8c2d0fedcba9876543210fedcba9876543210fedcba9876543210fedcba",
  "signature": "rT8pY3vZq...",
  "timestamp": 1672531200,
  "public_key": {
    "kty": "OKP",
    "crv": "Ed25519",
    "x": "d75Q3Cl07Ue4KjbAQfYN8X2..."
  },
  "log": {
    "event": "user.login",
    "user_id": "usr_123",
    "timestamp": 1672531200
  },
  "storage_status": "delivered",
  "storage_location": "s3://my-audit-logs/witness/receipts/42.json"
}

Response (202 Accepted - buffered):

If S3 is unavailable, receipts are buffered in purgatory and delivered when S3 recovers. The response includes X-Witness-Status: buffered header.

{
  "version": 1,
  "sequence_id": "42",
  "content_hash": "a1b2...",
  "prev_hash": "0000...",
  "chain_hash": "e4f6...",
  "signature": "rT8p...",
  "timestamp": 1672531200,
  "public_key": { "kty": "OKP", "crv": "Ed25519", "x": "d75Q..." },
  "log": { "event": "user.login" },
  "storage_status": "buffered"
}

The receipt is a self-contained proof. All verification data is embedded.

POST /v1/witness/verify

Verify a witness receipt. Checks signature, content hash, and chain link validity.

Auth: Required (witness:read scope) | Cost: Free

FieldTypeRequiredDescription
receiptJSONReceiptV1YesComplete receipt object from notarize endpoint
curl -X POST https://api.solenoid.systems/v1/witness/verify \
  -H "Authorization: Bearer sm_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "receipt": {
      "version": 1,
      "sequence_id": "42",
      "content_hash": "a1b2...",
      "prev_hash": "0000...",
      "chain_hash": "e4f6...",
      "signature": "rT8p...",
      "timestamp": 1672531200,
      "public_key": { "kty": "OKP", "crv": "Ed25519", "x": "d75Q..." },
      "log": { "event": "user.login" }
    }
  }'

Valid receipt (200 OK):

{
  "valid": true,
  "checks": {
    "signature_valid": true,
    "content_hash_matches": true,
    "chain_link_valid": true
  },
  "metadata": {
    "sequence_id": "42",
    "timestamp": 1672531200,
    "public_key": {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "d75Q3Cl07Ue4KjbAQfYN8X2..."
    }
  }
}

Invalid receipt (200 OK):

Invalid receipts return 200 with valid: false. All checks run without short-circuiting.

{
  "valid": false,
  "checks": {
    "signature_valid": false,
    "content_hash_matches": true,
    "chain_link_valid": true
  },
  "metadata": {
    "sequence_id": "42",
    "timestamp": 1672531200,
    "public_key": { "kty": "OKP", "crv": "Ed25519", "x": "d75Q..." }
  },
  "errors": [
    "Signature verification failed"
  ]
}

All three checks must pass for a receipt to be valid:

  • signature_valid — Ed25519 signature verification on chain_hash
  • content_hash_matches — SHA-256 of log matches content_hash
  • chain_link_valid — chain_hash matches SHA-256(prev_hash + sequence_id + content_hash)

GET /v1/witness/pubkey/:chainId

Get the Ed25519 public key for a witness chain. Used for offline verification.

Auth: Public | Cost: Free

:chainId is the chain_id returned by POST /v1/witness/sys/init. It is the only identifier this endpoint accepts: a verifier holds no credential here, so there is no account or project to infer.

curl https://api.solenoid.systems/v1/witness/pubkey/wc_9tQvXm2LpR4sK7nB1cD8fG3hJ5wY6zA0

Response:

{
  "kty": "OKP",
  "crv": "Ed25519",
  "x": "d75Q3Cl07Ue4KjbAQfYN8X2..."
}

Cache this key locally for offline verification. The key is stable for the lifetime of the chain.

404 Not Found means no chain has been initialized under that id. Check the chain_id against what init returned rather than constructing one by hand.

Data types

JSONReceiptV1

interface JSONReceiptV1 {
  version: 1;
  sequence_id: string;          // BigInt as string
  content_hash: string;         // Hex SHA-256 of log
  prev_hash: string;            // Hex SHA-256 of previous chain_hash
  chain_hash: string;           // Hex SHA-256 of concatenated values
  signature: string;            // Base64url Ed25519 signature
  timestamp: number;            // Unix epoch seconds
  public_key: JsonWebKey;       // Ed25519 public key
  log: Record<string, unknown>; // Original JSON log
  storage_status?: 'delivered' | 'buffered';
  storage_location?: string;    // S3 URL if delivered
}

VerificationResult

interface VerificationResult {
  valid: boolean;
  checks: {
    signature_valid: boolean;
    content_hash_matches: boolean;
    chain_link_valid: boolean;
  };
  metadata: {
    sequence_id: string;
    timestamp: number;
    public_key: JsonWebKey;
  };
  errors?: string[];
}

Client-side verification

You can verify receipts in the browser using the hosted verifier at solenoid.systems/witness/verify.

The verifier is a standalone HTML page that works offline. It uses Web Crypto API (crypto.subtle) to verify Ed25519 signatures without sending data to the server.

Verification steps:

  1. Compute content_hash — SHA-256 of JSON.stringify(receipt.log)
  2. Compute chain_hash — SHA-256 of prev_hash + sequence_id (8 bytes big-endian) + content_hash
  3. Verify Ed25519 signature on chain_hash using public_key

JSON canonicalization

content_hash is computed over JSON.stringify(log) — the bytes are non-canonical. Two semantically equivalent log objects with different key orderings produce different content hashes, and verification will fail.

If you reconstruct a log from your own storage (parse JSON, mutate, re-stringify), the byte-for-byte output may not match what Witness signed. To verify reliably:

  • Preferred: keep the receipt’s log field exactly as returned and pass it straight back to the verifier — don’t round-trip through your own JSON parser.
  • If you must serialize logs yourself before notarizing, use a canonicalization scheme (e.g. RFC 8785 JCS) for both signing input and verification, and stringify the same way on both sides.

The hosted verifier at solenoid.systems/witness/verify operates on the raw receipt object, so it avoids this pitfall by construction.

See the client verifier source for implementation details.

Next steps