Developers
Verify a track. One file.
A production quickstart for PoH credentialed audio, the legacy sidecar fallback, and the monitoring your integration should keep. The contract remains the live, model-generated API reference.
Get keys. Partner API keys are issued directly by PoH during onboarding — there is no self-signup yet. Request partner access and you receive each key exactly once, over the channel agreed at onboarding; PoH never re-displays a secret.
Integrate on beta first. https://beta.api.proofofhuman.fm/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.proofofhuman.fm/v1" })), then move to https://api.proofofhuman.fm/v1 with your production key at go-live.
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 poh = new PoHClient({ apiKey: process.env.POH_API_KEY! });
const upload = await poh.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 poh.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 | PoH 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 PoH-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 PoH. | Do not accept it as PoH-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 poh.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 PoH 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. PoH 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 PoH claim of authorship.
const review = await poh.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 poh.partner.requestEvidence(review.reviewId, { kind: "source_evidence" });
// …record the response reference from your own evidence system, then close it out:
await poh.partner.updateEvidence(review.reviewId, requestId, {
status: "accepted",
responseReference: "EVID-4821",
});
await poh.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("poh.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 poh = new PoHClient({
apiKey: process.env.POH_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.