Skip to content
Proof of Human is now Chronatum. What changes for you

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

VerdictMeaningAction
validChronatum signature, registry record, and exact audio binding passed.Accept the credential; apply your own policy to report-only analysis.
audioChangedLegacy audio bytes do not match the .poh.Reject and request the matching pair.
tamperedThe proof, embedded identity, or signature changed.Reject.
unregisteredThis is not a registered Chronatum-issued proof or credential.Reject.
unreadableNo readable proof was found, or the embedded hard binding failed.Reject and confirm the correct file.
local_sealAn 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=1 every five minutes.
  • Alert on sustained 5xx responses, p95 latency above your threshold, repeated 429s, and low quota headroom.
  • Include X-Request-Id and UTC time in support reports.