Developers
Verify a track. One file.
A production quickstart for Chronatum credentialed audio, the legacy sidecar fallback, and the monitoring your integration should keep. The contract remains the live, model-generated API reference.
The package is being renamed. Install @proof-of-human/ts-sdk — that is the published package, currently 2.7.0, and its client export is PoHClient. It is being renamed @chronatum/ts-sdk (client export ChronatumClient), but that scope is not on npm yet, so installing it fails. The wire contract is identical either way: same base URLs, same x-api-key header, same request, response, verdict, and evidence values, so an existing integration needs no change and switching later is one import line.
Get keys. Partner API keys are issued directly by Chronatum during onboarding — there is no self-signup yet. Request partner access and you receive each key exactly once, over the channel agreed at onboarding; Chronatum never re-displays a secret.
Integrate on beta first. https://beta.api.chronatum.com/v1 runs the same contract and authentication as production on isolated pre-production infrastructure: separate API keys, a separate registry, disposable test proofs. Ask for a beta key alongside your production key — they are different secrets and never interchangeable. Point staging at beta (new PoHClient({ apiKey, baseUrl: "https://beta.api.chronatum.com/v1" })), then move to https://api.chronatum.com/v1 with your production key at go-live.
The former hostnames api.proofofhuman.fm and beta.api.proofofhuman.fm serve the identical API from the same infrastructure and keep working — an existing integration needs no change.
01 · Core flow
Upload, then verify
Keep the API key in a server-side secret manager. Ask for a short-lived upload slot, post every returned form field plus the WAV, then verify with only the upload ID.
import { readFile } from "node:fs/promises";
import { PoHClient } from "@proof-of-human/ts-sdk";
const chronatum = new PoHClient({ apiKey: process.env.CHRONATUM_API_KEY! });
const upload = await chronatum.createUpload();
const bytes = await readFile("credentialed-song.wav");
const form = new FormData();
for (const [key, value] of Object.entries(upload.fields)) form.append(key, value);
form.append("file", new Blob([new Uint8Array(bytes)], { type: "audio/wav" }), "credentialed-song.wav");
const posted = await fetch(upload.url, { method: "POST", body: form });
if (!posted.ok) throw new Error(`upload failed: ${posted.status}`);
const result = await chronatum.verify({ uploadId: upload.uploadId });
if (!result.good || result.verdict !== "valid") {
throw new Error(`proof rejected: ${result.verdict}`);
}Legacy fallback: base64 the separate .poh and call verify({ uploadId, poh }). One-file delivery is currently WAV/BWF, up to 65 MiB in a verification slot. AIFF, CAF, MP3, and M4A keep using the legacy pair.
02 · Decision
Branch on the verdict
| Verdict | Meaning | Action |
|---|---|---|
| valid | Chronatum signature, registry record, and exact audio binding passed. | Accept the credential; apply your own policy to report-only analysis. |
| audioChanged | Legacy audio bytes do not match the .poh. | Reject and request the matching pair. |
| tampered | The proof, embedded identity, or signature changed. | Reject. |
| unregistered | This is not a registered Chronatum-issued proof or credential. | Reject. |
| unreadable | No readable proof was found, or the embedded hard binding failed. | Reject and confirm the correct file. |
| local_seal | An intact artist-device seal was never registered with Chronatum. | Do not accept it as Chronatum-issued. |
Since API 2.7.0, every POST /verify response — success and failure alike — also carries a decision envelope built for automation, so your pipeline branches on stable slugs instead of prose: status (verified_binding, needs_review, invalid_proof), reasons, recommendedAction, the credential carrier, and counts of declarations, conflicts, and unverifiable facts. It is policy-neutral: it never labels a track human or AI, and needs_review means the evidence report asks for a human decision, not that the credential failed.
const result = await chronatum.verify({ uploadId: upload.uploadId });
// decision is the stable automation surface: policy-neutral status + machine reasons.
switch (result.decision?.status) {
case "verified_binding": // signature + exact audio binding passed, evidence has no open flag
return accept(result);
case "needs_review": // binding passed, but the evidence report asks for human review
return queueForReview(result, result.decision.reasons); // e.g. source_evidence_needs_review
case "invalid_proof": // unreadable, tampered, unregistered, changed audio, or local seal
return reject(result, result.decision.reasons);
default: // pre-2.7.0 response cached somewhere — fall back to the verdict
return result.verdict === "valid" ? accept(result) : reject(result);
}classification, grade, and interpretation are report-only. They may be absent on older proofs and do not change credential validity. A valid proof is a signed account of the editing Chronatum witnessed—not an “AI-free” certificate.
03 · Reviews
Track your review, keep your evidence
When a track lands in needs_review, the review workflow binds a registered proof to your own catalog identifiers and tracks the back-and-forth to your Accept / Hold / Reject outcome. Chronatum stores workflow state and bounded opaque references only — never source files, licences, creator names, or free-form notes — and review metadata expires automatically after 180 days. The outcome is your policy result, not a Chronatum claim of authorship.
const review = await chronatum.partner.createReview({
proofId,
bindings: { creatorId, accountId, submissionId, trackId }, // your opaque ids — never names/emails
});
// Ask the artist's side for source evidence, a licence, or clarification…
await chronatum.partner.requestEvidence(review.reviewId, { kind: "source_evidence" });
// …record the response reference from your own evidence system, then close it out:
await chronatum.partner.updateEvidence(review.reviewId, requestId, {
status: "accepted",
responseReference: "EVID-4821",
});
await chronatum.partner.resolveReview(review.reviewId, { outcome: "accepted" });04 · Operations
Monitor without copying proof data
Capture status, latency, request ID, and rate-limit headers in your existing APM. Never log the key, audio, proof, presigned form fields, request URL containing a proof ID, or full response body.
const monitoredFetch: typeof fetch = async (input, init) => {
const started = performance.now();
const response = await fetch(input, init);
yourApm.record("chronatum.api", {
status: response.status,
latencyMs: Math.round(performance.now() - started),
requestId: response.headers.get("x-request-id"),
quotaRemaining: response.headers.get("x-ratelimit-remaining"),
retryAfter: response.headers.get("retry-after"),
});
return response;
};
const chronatum = new PoHClient({
apiKey: process.env.CHRONATUM_API_KEY!,
fetch: monitoredFetch,
});- Probe authenticated
GET /partner/usage?days=1every five minutes. - Alert on sustained 5xx responses, p95 latency above your threshold, repeated 429s, and low quota headroom.
- Include
X-Request-Idand UTC time in support reports.