Skip to content

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

VerdictMeaningAction
validPoH 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 PoH-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 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=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.