Cryptographically signed performance reports
Fetch your portfolio metrics and time series straight from the AuditZK AMD SEV-SNP enclave. Every response is signed with an ECDSA-P256 key whose measurement you can compare against the published GitHub release, letting your investors verify the numbers end-to-end without trusting either your frontend or ours.
Requires the Enterprise plan. See pricing
Quickstart
From zero to a verified report in three steps
Mint an API key
Go to /dashboard/api-keys and click Create API key. The full key is shown exactly once. Store it in a secrets manager. Keys carry the reports:read scope and can be given an expiration between 1 and 365 days.
Fetch a signed report
Send your key in the x-api-key header and call GET /api/enterprise/reports/signed.
curl -H "x-api-key: azk_your_key_here" \"https://auditzk.com/api/enterprise/reports/signed\?startDate=2026-01-01\&endDate=2026-04-12\&benchmark=SPY"
Verify the signature
The response carries a standard ECDSA-P256 signature over a SHA-256 hash of the financial payload. Verify it client-side with the Web Crypto API or any language that supports ECDSA. See the Verify the signature section below for full examples.
Authentication
API key in the x-api-key header
All Enterprise API requests must include a valid API key in the x-api-key HTTP header. Keys are issued from the dashboard, bound to your Enterprise user, and can be revoked at any time without affecting other keys.
Scoped
Each key grants the reports:read permission, allowing access to signed performance reports.
Rate limited
Each key carries its own per-minute quota, counted server-side on every request. Rate limit headers included in every response.
Revocable
Disable a key without deleting it, re-enable it if a rotation goes wrong, delete it once nothing calls it. Keys are hashed at rest (SHA-256).
Key lifecycle
Rotate without a gap in service
An Enterprise account holds up to five keys at once, which is what makes rotation safe: the replacement can be live before the old key stops working. Deleting a key is irreversible, so it is the last step, not the first.
- 1.Mint a second key and deploy it to whatever calls the API.
- 2.Disable the old key. It is refused from the next request onward — the
isActiveflag is read on every call, there is no cached window — and callers still using it get401INVALID_API_KEY. - 3.Watch for those 401s. If something was still on the old key, re-enable it and finish the migration. A disabled key keeps its identity, its quota and its creation date.
- 4.Delete the old key once nothing calls it. The secret is gone at that point and cannot be brought back.
Keys are minted and deleted from the dashboard. To have a key disabled or re-enabled, or its quota raised, write to support.
Endpoint
/api/enterprise/reports/signedReturns a single signed report covering the requested period for the user that owns the API key. The request is scoped to the key's user. There is no userId parameter; users can only read their own data.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
startDate | ISO date | endDate − 365d | Start of the reporting period. Format: YYYY-MM-DD. |
endDate | ISO date | today | End of the reporting period. Format: YYYY-MM-DD. |
benchmark | "SPY" | "BTC" | SPY | Benchmark symbol used to compute alpha, beta, correlation and tracking error. |
includeRiskMetrics | boolean | true | Include VaR (95/99), expected shortfall, skewness, kurtosis in the financialData object. |
includeDrawdown | boolean | true | Include the full drawdownPeriods array and maxDrawdownDuration. |
Response
On success, the API returns a JSON object with a data.financialData field (the signed payload) and a set of cryptographic fields (signature, publicKey, reportHash, measurement) used for verification.
{"success": true,"data": {"financialData": {"reportId": "cb79acda-5902-4530-a7e3-bbb5559e142c","userUid": "user_abc...","generatedAt": "2026-04-12T21:47:49.622Z","periodStart": "2026-01-06T00:00:00.000Z","periodEnd": "2026-04-11T00:00:00.000Z","baseCurrency": "USD","benchmark": "SPY","dataPoints": 90,"exchanges": ["binance", "ibkr", "mexc"],"totalReturn": 0.234,"annualizedReturn": 0.89,"volatility": 0.18,"sharpeRatio": 1.82,"sortinoRatio": 2.4,"maxDrawdown": 0.07,"calmarRatio": 3.2,"var95": 0.0053,"var99": 0.0238,"expectedShortfall": 0.0110,"skewness": 2.44,"kurtosis": 25.70,"alpha": 0.12,"beta": 0.85,"informationRatio": 0.86,"trackingError": 0.14,"correlation": 0.72,"maxDrawdownDuration": 49,"currentDrawdown": 0.019,"drawdownPeriods": [{"startDate": "2026-01-18","endDate": "2026-03-10","depth": 0.0346,"duration": 49,"recovered": true}],"dailyReturns": [{"date": "2026-01-06","netReturn": 0.0,"benchmarkReturn": 0.0,"outperformance": 0.0,"cumulativeReturn": 0.0,"nav": 1.0}],"monthlyReturns": [{"date": "2026-01","netReturn": 0.021,"benchmarkReturn": 0.018,"outperformance": 0.003,"aum": 1024000}]},"signature": "MEQCIDqXBFjwOTf3sx78BJYR8Wpzp0MNBxBelrB0...","publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELUTN...","signatureAlgorithm": "ECDSA-P256-SHA256","reportHash": "0833628eada6be2b20339a852e8f2dcf303ef3f7c77da9e0858368fdfee5d2c2","measurement": "12068361369CF9179BB6AC08572B7E15...","enclaveVersion": "1.0.0-go","enclaveMode": "production"}}
Verification
Verify the signature with one of your existing libraries
The signature is computed as ECDSA-P256-SHA256(sha256(reportHashString)) where reportHashString is the hex-encoded SHA-256 of the signed financial payload. The snippets below reproduce this computation and call the standard ECDSA verify primitive from crypto.subtle or the Python cryptography package.
// Verifies an AuditZK signed report using the Web Crypto API.// Works in the browser and in Node.js 18+ (globalThis.crypto.subtle).interface SignedReport {financialData: unknownsignature: string // base64 DER-encoded ECDSA signaturepublicKey: string // base64 DER-encoded SPKIreportHash: string // hex-encoded SHA-256 of financialDatasignatureAlgorithm: 'ECDSA-P256-SHA256'measurement: string // hex-encoded SEV-SNP launch measurement}function base64ToBytes(b64: string): Uint8Array {const bin = atob(b64)const out = new Uint8Array(bin.length)for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)return out}export async function verifySignedReport(report: SignedReport,expectedMeasurement?: string): Promise<boolean> {// 1. Optional: bind the report to a specific audited enclave buildif (expectedMeasurement && report.measurement !== expectedMeasurement) {console.warn('Measurement mismatch', {got: report.measurement,want: expectedMeasurement,})return false}// 2. Import the enclave's public keyconst key = await crypto.subtle.importKey('spki',base64ToBytes(report.publicKey),{ name: 'ECDSA', namedCurve: 'P-256' },false,['verify'])// 3. The enclave signs SHA-256(reportHashString), so we need to// feed the hex-encoded report hash STRING through SHA-256 again// to match the signed payload.const reportHashDigest = await crypto.subtle.digest('SHA-256',new TextEncoder().encode(report.reportHash))// 4. Verify ECDSA-P256 over the double hashreturn crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' },key,base64ToBytes(report.signature),reportHashDigest)}
Hardware attestation
Bind the report to the audited enclave build
A valid ECDSA signature only proves some key signed this data. To bind the report to the specific AuditZK enclave build that produced it, compare the measurement field against the SEV-SNP launch measurement published on the matching GitHub release. If both match, the data was generated by the audited source code running inside a confidential VM, end to end.
Transitional state
The production enclave currently runs the legacy TypeScript build and does not yet emit the measurement field. Responses contain an empty measurement string. Signature verification still works (the ECDSA chain is intact); hardware binding will be enabled when the Go enclave is promoted to production. Follow the roadmap on GitHub.
Error codes
What each status code means
| Status | Code | Meaning |
|---|---|---|
400 | INVALID_PARAMETERS | Query parameters failed validation (bad date format, unknown benchmark, etc.). |
401 | MISSING_API_KEY | No x-api-key header provided. |
401 | INVALID_API_KEY | API key is unknown, revoked, or past its expiration date. |
403 | PLAN_REQUIRED | The user backing this key is no longer on the Enterprise plan. |
403 | INSUFFICIENT_SCOPES | Key does not have the reports:read scope. |
404 | NO_DATA | User has no snapshots in the requested period: cannot build a report. |
429 | RATE_LIMIT_EXCEEDED | Per-key quota exceeded. Check the Retry-After header for seconds until the window resets. |
502 | UPSTREAM_ERROR | Report service or enclave unreachable. Retry with exponential backoff. |
Rate limiting
Per-key quota, soft limits
Each API key has its own quota, counted per key in a fixed one-minute window by the API route that serves the request. Disabling or deleting one key never affects the counter of another. When a request is rate-limited, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait. Every successful response also includes:
X-RateLimit-Remaining: number of requests remaining in the current window.X-RateLimit-Reset: Unix epoch (seconds) at which the window resets.X-RateLimit-Limit: the quota configured on the key (default:2requests per minute).
The default suits a scheduled pull. If your integration needs more, tell support which key and what rate: the quota is raised on that key alone, and X-RateLimit-Limit reflects the new value on the next response.
Need a feature that isn't listed here? Reach out to support. We read every message.