Beta Release of TrackOrigin is live. We are still running verifications throughout June.
DEVELOPER API · TO-1.0

Build on the provenance layer.

A versioned, cryptographically-grounded API for music verification. Read certificates, look up tracks by audio hash, fetch artist profiles. Every response is independently verifiable against the published Ed25519 public key.

BASE URL
/api/v1
AUTH
Bearer key
SPEC
§01

Quick start

Most TrackOrigin endpoints are public — no key required for reads. Embed widgets, DSP front-ends and verification tools can hit them directly from a browser. For higher throughput, batch lookups, and the /me endpoint, mint a key from your account page and pass it as a Bearer token.

QUICKSTART · CURL
# 1. Fetch a certificate (no auth)
curl -fsSL https://trackorigin.io/api/v1/certificates/cer_01KS9QWHV7PHETYGQAYMHB3DM6

# 2. Reverse-lookup by audio hash (no auth)
SHA="$(openssl dgst -sha256 -hex master.wav | awk '{print $2}')"
curl -fsSL https://trackorigin.io/api/v1/tracks/by-sha256/$SHA

# 3. Authenticated batch lookup
curl -fsSL https://trackorigin.io/api/v1/certificates/bulk \
  -H "Authorization: Bearer to_pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"cert_ids":["cer_a","cer_b"]}'
§02

Authentication

Two key types, both passed as Bearer tokens in the Authorization header. Mint and revoke from /account.

UNAUTHENTICATED · BY IP
All public reads work without a key. Enough for one widget on a low-traffic site.
60req / min
PUBLISHABLE · to_pk_live_…
Read-only. Safe to embed in browsers, mobile apps, widgets. Per-key limit.
600req / min
SECRET · to_sk_live_…
Full read access including /me. Never ship in a client bundle.
3000req / min
[ STORAGE ]

We only store the SHA-256 of your key.

The full key value is shown once, at the moment you mint it. A database leak on our side does not expose usable credentials. Lose a key → revoke and remint; we cannot recover the value.

§03

Errors

Every non-2xx response is JSON in the envelope below. The HTTP status code is the source of truth; code is a stable machine-readable identifier; message is human-facing.

ERROR ENVELOPE
{
  "error": {
    "code": "certificate_not_found",
    "message": "No certificate with that ID.",
    "type": "not_found",
    "request_id": "req_01KS…"
  }
}
StatusMeaning
400Malformed input (bad cert ID format, sha256 not 64 hex chars, etc.). code: validation_error for body/query validation failures, with per-field detail in error.details.errors.
401Missing or invalid API key on an authenticated endpoint.
402Monthly free quota exhausted and credit balance is 0. code: insufficient_credits. See §05.
403Publishable key used on a secret-only endpoint.
404Resource doesn't exist.
409Conflict — e.g. a SHA-256 resolving to more than one active certificate (code: ambiguous_multiple_certificates).
422Unprocessable — e.g. a certificate has no fingerprint yet (code: fingerprint_unavailable) or an undecodable probe fingerprint.
429Rate limit exceeded; check the Retry-After header.
500Something's broken on our side; safe to retry with backoff.
§04

Rate limits

Per-tier limits are listed in §02. When you hit one you get a 429 response with a Retry-After header telling you how many seconds to wait. The bucket refills smoothly — you don't have to wait the full minute.

[ HEADERS ]

What to check on a 429.

Retry-After — integer seconds. Sleep at least this long before retrying. Best practice: exponential backoff with jitter if you're hitting limits repeatedly.

§05

Billing

Every authenticated /api/v1/* call (any request carrying a to_pk_live_ or to_sk_live_ key) counts against your monthly quota. Unauthenticated calls are never billed — that's the whole point of the open verification surface. Test keys (to_pk_test_… / to_sk_test_…) hit the sandbox and are never metered.

See current usage on your account page — it reads GET /v1/me/api-usage, an account endpoint authenticated by your signed-in session (not an API key).

FREE QUOTA · PER CALENDAR MONTH
Resets at 00:00 UTC on the 1st. Pooled across all your keys on the same account.
10,000calls / month
BEYOND FREE · DEBITED FROM CREDITS
Atomic debit on every 5,000 paid call boundary. Logged to the credit ledger as api_paid_calls.
1credit / 5,000 calls
[ 402 PAYMENT REQUIRED ]

What happens when you run out.

When the monthly free quota is exhausted and your credit balance reaches 0, authenticated calls are refused with HTTP 402. The error.code is insufficient_credits. Top up at /pricing and the next call goes through immediately. Unauthenticated public reads keep working regardless of balance.

GET /v1/me/api-usage · response
{
  "month": "2026-05",
  "free_quota": 10000,
  "free_used": 8421,
  "free_remaining": 1579,
  "paid_calls": 0,
  "paid_pending_to_next_credit": 0,
  "calls_per_credit": 5000,
  "credits_balance": 18,
  "estimated_calls_remaining": 91579
}
RESOURCE

Certificates

GET /api/v1/certificates/{cert_id} Public

Fetch a certificate's full signed manifest plus the base64 Ed25519 signature. This is the body you verify against the public key — see the verify page for code samples.

RESPONSE · 200 · TRIMMED
{
  "cert_id": "cer_01KS9QWHV7PHETYGQAYMHB3DM6",
  "verdict": "verified",
  "revoked": false,
  "issued_at": "2026-05-26T08:13:42.000Z",
  "standard": "TO-1.0",
  "master": { "sha256": "a1b2…" },
  "artist": { "handle": "marasings", "display_name": "Mara Vex" },
  "track":  { "title":  "Lighthouse" },
  "manifest":  { /* canonical signed body */ },
  "signature": "base64-encoded-ed25519-sig",
  "public_url": "/cert/cer_01KS9QWHV7PHETYGQAYMHB3DM6"
}
GET /api/v1/certificates/{cert_id}/status Public

Lightweight liveness check. Returns status (active / revoked), version, verdict, revoked, issued_at, standard, and checked_at. Designed for embed widgets that poll often.

QUERY
no_cache boolean · optional
Pass true for a fresh, uncached read (served Cache-Control: no-store). The Origin Seal attach flow uses this for the final pre-write certificate re-check so a just-revoked cert can never be served from an edge cache as active.
GET /api/v1/tracks/by-sha256/{sha256_hex} Public

Reverse-lookup: given the SHA-256 of an audio file, returns the active certificate bound to that exact content. Resolves via both master uploads and registered off-platform renditions, so a content-addressed transcode hash a DSP serves also resolves. Lets a platform decide whether to render an Origin Seal next to a track row without coordinating with the artist.

PATH
sha256_hex string · required
64-char hex digest of the audio bytes. Case-insensitive.
RESOLUTION
0 active matches
404
1 active match
200 with the certificate
>1 active match
409 ambiguous_multiple_certificates — resolve nothing
POST /api/v1/certificates/bulk API key

Batch lookup, up to 100 cert IDs per call. Returns an object keyed by cert_id; missing IDs map to null. Authenticated to discourage unauth fan-out abuse.

BODY · JSON
cert_ids array · 1–100 · required
Array of certificate IDs to look up.
RESPONSE · 200
{
  "results": {
    "cer_a": { "cert_id": "cer_a", "verdict": "verified", "revoked": false, … },
    "cer_b": null
  },
  "found": 1,
  "requested": 2
}
RESOURCE

Origin Seal

The Origin Seal proves that the audio a listing plays or downloads is the human-verified work behind an active certificate. TrackOrigin owns the cert fingerprints, the verification scoring, the signed revocation feed, and the monotonic change sequence; the consuming platform owns serving, the immutable binding, and the public render gate. A consumer fingerprints each served artifact from its own bytes, calls verify-audio, re-checks the cert fresh with ?no_cache=true, then binds. The seal renders only while that whole chain holds. Copy rule: the seal reads "Verified human-made" / "Verified work" — never "verified artist", "verified owner", or "official artist" (a faithful re-upload verifies the work, not authorship).

Production policy gate.

A production match is only ever returned under an accepted verification policy — never an accept-all or placeholder threshold. When no accepted policy applies, verify-audio returns matched: false with confidence: "uncalibrated" and renditions are refused, so nothing can seal on an unproven threshold. Build and test the full integration against sandbox certificates (e.g. cer_test_synthetic) in dev; a public seal requires the accepted policy plus the full served-audio verification chain.

POST /api/v1/certificates/{cert_id}/verify-audio Public · rate-limited

Verify one artifact's perceptual fingerprint against one certificate master. Read-only — mutates nothing. Public callers get a coarse confidence band and no exact score; authenticated callers get the exact score and the full receipt to store in a binding. Returns 422 fingerprint_unavailable if the cert has no fingerprint yet, and enforces the second independent fingerprint when the cert is high-risk.

BODY · JSON
fingerprint string · required
base64 Chromaprint fingerprint computed from this artifact's own bytes.
fingerprint2 string · optional
base64 independent fingerprint — required only if the cert is high-risk.
duration_ms integer · optional
Artifact duration for the decode/duration sanity gate.
RESPONSE · 200 · AUTHENTICATED
{
  "cert_id": "cer_…",
  "matched": true,
  "cert_status": "active",
  "cert_version": 12,
  "cert_status_checked_at": "2026-06-01T09:20:15Z",
  "algorithm": "chromaprint",
  "algorithm_version": 1,
  "verification_policy_id": "origin-seal-fp-policy-2026-01",
  "policy_accepted": true,
  "requires_dual_fingerprint": false,
  "confidence": "high",
  "score": 0.9821,
  "checked_at": "2026-06-01T09:20:15Z"
}
POST /api/v1/renditions API key · allowlisted

Register an off-platform artifact (a transcode a DSP/distributor serves) so by-sha256 resolves its content hash to the cert. v1 is allowlist-only; only registered integrators may call it. TrackOrigin verifies the supplied fingerprint against the cert master, re-checks the cert active, and registers only on a match under an accepted policy. Each rendition uses a fingerprint computed from that artifact's own bytes — never the upload's.

BODY · JSON
cert_id string · required
The certificate to bind to.
sha256 string · required
64-char content hash of the served artifact.
fingerprint string · required
base64 fingerprint of that artifact's bytes.
fingerprint2 / duration_ms optional
As for verify-audio.
GET /api/v1/certificates/changes API key

Issuance + revocation feed. Pass ?since_sequence=N for the preferred monotonic mode: returns sequenced revocation events with sequence > N plus the current sequence head. Store last_applied_sequence and reject any response whose head is ≤ last_applied (unless identical) — timestamps alone are not trusted. (Legacy ?since=<ISO> timestamp mode still works and now also carries sequence.)

GET /.well-known/trackorigin-revocations.json Public · signed

Account-less, Ed25519-signed revocation snapshot. The signature covers the canonical bytes of the payload without its own signature field; verify with the key at /.well-known/trackorigin-public-key.json. Carries sequence (monotonic head) and generated_at so a poller can enforce freshness and fail closed if the feed goes stale. Edge-cached ~5 min.

RESOURCE

Artists

GET /api/v1/artists/{handle} Public

Public artist profile. Includes display name, bio, location, avatar URL, verified-since date, and total certificate count. JSON mirror of /u/{handle}.

GET /api/v1/artists/{handle}/certificates Public

Paginated list of an artist's certificates, newest first. Cursor-based — pass back the next_cursor from the previous response to get the next page.

QUERY
limit integer · optional
1–100, default 25.
cursor string · optional
Opaque value from a previous response's next_cursor.
RESOURCE

Account

GET /api/v1/me Secret only

Whoami — sanity-check that your secret key is valid and identify which TrackOrigin account it's tied to. Useful as a partner-integration smoke test. Refuses publishable keys with a 403.

§06

Versioning

Endpoints under /api/v1 form a stable contract. We will only add fields and add new endpoints under this prefix. Removing or renaming fields, or changing response shapes, requires a major version bump.

Breaking changes go to /api/v2 with a deprecation notice; v1 remains live for at least 12 months.

  • Safe to depend on: existing field names, types, and HTTP status codes within v1.
  • Additive: new optional response fields, new endpoints, new error codes.
  • Don't depend on: field order in JSON, internal opaque ID format, error message string exactness.
§07

Webhooks

Subscribe to certificate lifecycle events. We POST a signed JSON envelope to your URL when a matching event fires. Subscriptions are managed from /account.

Three event types are emitted:

  • cert.issued — a new certificate has been signed and is live on its public page.
  • cert.revoked — a previously-live certificate has been revoked. The public page now shows revoked state.
  • cert.updated — manifest content has changed (rare; reserved for cases like display-name corrections).

Each delivery includes these headers:

HeaderValue
TO-EventEvent type (e.g. cert.issued).
TO-Event-IdStable identifier for this event. De-dupe on this in your receiver.
TO-Delivery-IdIdentifier for this specific delivery attempt.
TO-TimestampUnix seconds at sign time.
TO-Signaturet={timestamp},v1={hmac-sha256-hex}
EXAMPLE · CERT.ISSUED PAYLOAD
{
  "id": "evt_01KS9QW…",
  "type": "cert.issued",
  "created_at": "2026-05-26T08:13:42.000Z",
  "data": {
    "cert_id": "cer_01KS9QWHV7PHETYGQAYMHB3DM6",
    "verdict": "verified",
    "issued_at": "2026-05-26T08:13:42.000Z",
    "standard": "TO-1.0",
    "master": { "sha256": "a1b2…" },
    "artist": { "id": "art_01…", "handle": "marasings", "display_name": "Mara Vex" },
    "track":  { "id": "trk_01…", "title": "Lighthouse" },
    "public_url": "/cert/cer_01KS9QWHV7PHETYGQAYMHB3DM6"
  }
}
[ VERIFY ]

Always verify the signature.

Compute HMAC-SHA256(secret, "{timestamp}.{raw_request_body}"); compare constant-time to the v1= value in the TO-Signature header. Reject if the timestamp is more than 5 minutes from your wall clock to prevent replay.

VERIFY · NODE.JS
const crypto = require('crypto');

function verifyTrackOriginWebhook(req, secret) {
  const sig = req.headers['to-signature'] || '';
  // Format: "t=<timestamp>,v1=<hex>"
  const parts = Object.fromEntries(sig.split(',').map((s) => s.split('=')));
  const ts = parts.t, given = parts.v1;
  if (!ts || !given) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${req.rawBody}`)  // rawBody = exact bytes received
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(given, 'hex')
  );
}
VERIFY · PYTHON
import hmac, hashlib, time

def verify_trackorigin_webhook(headers, raw_body: bytes, secret: str) -> bool:
    sig = headers.get("TO-Signature", "")
    parts = dict(p.split("=", 1) for p in sig.split(","))
    ts, given = parts.get("t"), parts.get("v1")
    if not ts or not given:
        return False
    if abs(time.time() - int(ts)) > 300:
        return False
    expected = hmac.new(
        secret.encode("utf-8"),
        f"{ts}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, given)

Retries. Failed deliveries (timeout, network error, or any non-2xx response) are retried with exponential backoff: 1 min → 5 min → 15 min → 1 h → 4 h → 6 h, then marked failed. After 10 consecutive failed deliveries the subscription auto-disables; re-enable from /account.

Test mode. Click Test on a subscription to fire a synthetic cert.issued envelope to your URL — useful for verifying your receiver before going live.

§08

Roadmap

One surface is on the roadmap and not yet shipped:

  • Partner submission API — programmatically submit tracks for verification on behalf of contracted artists. Contact us to be in the design loop.