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
| Field | Type | Required | Description |
|---|---|---|---|
endpoint | string | Yes | S3 endpoint URL (HTTPS required) |
bucket | string | Yes | Bucket name |
region | string | Yes | AWS region |
accessKeyId | string | Yes | Access key ID |
secretAccessKey | string | Yes | Secret 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
| Field | Type | Required | Description |
|---|---|---|---|
log | object | Yes | Any 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
| Field | Type | Required | Description |
|---|---|---|---|
receipt | JSONReceiptV1 | Yes | Complete 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_hashcontent_hash_matches— SHA-256 of log matches content_hashchain_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:
- Compute
content_hash— SHA-256 ofJSON.stringify(receipt.log) - Compute
chain_hash— SHA-256 ofprev_hash + sequence_id (8 bytes big-endian) + content_hash - Verify Ed25519 signature on
chain_hashusingpublic_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
logfield 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
- Quick Start — notarize your first log
- Errors — error codes and resolution