Base URL https://vybz.cloud/v1. Machine-readable: /v1/openapi.json.
- Auth:
Authorization: Bearer vybz_live_<48 hex>orX-API-Key. Keys belong to an organization and carry scopes. - Request id: every response has
X-Request-Id. Quote it to support. - Rate limit: per key per minute.
X-RateLimit-Remainingon success;429withRetry-After: 60when exceeded. - Errors: JSON body
{ "error": { "code", "message", "request_id", "docs", …extra } }. - Binary in: send raw bytes as the body, or one
multipart/form-datafile part. Batch routes take many parts, or JSON with URLs to fetch. - Audio in: any format for verify and detect (WAV, AIFF, FLAC, MP3, Ogg Vorbis, Opus in the edge; AAC/M4A, ALAC, MP4, MOV, WebM through the decode worker). Lossless only for registration.
GET /provenance/formatsreports the live list. - Console sessions: the console calls the same API with the member's session JWT plus
X-VYBZ-Org. Integrations use keys. - Binary out: watermarked copies return
audio/wavby default; sendAccept: application/jsonfor a stored copy and a one-hour link. - Size limits: audio 200 MB, blobs 500 MB per request (50 GB through chunked uploads). Batches: 25 files, 200 MB total. Analysis decodes up to about six minutes per file at 44.1 kHz and reports
truncatedbeyond that. - Versioning:
X-VYBZ-Api-Versionreports the contract date. Breaking changes ship as a new date and are announced 90 days ahead.
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthenticated, invalid_key, invalid_session |
Missing, unknown, revoked, or expired key; or an expired console session. |
| 402 | plan_limit_reached |
Developer plan cap hit (plan, used, included, upgrade in extra). Business and Enterprise are metered, never blocked. |
| 403 | insufficient_scope, not_a_member |
Key lacks the required scope (required_scope in extra); or the session user is not in the organization. |
| 404 | not_found, route_not_found |
Object not in this organization, or no such route. |
| 405 | method_not_allowed |
|
| 409 | checksum_mismatch, slug_taken, missing_blobs, head_moved, branch_exists, part_out_of_order, upload_incomplete, upload_closed |
Conflict; the extra fields say what to fix. |
| 413 | payload_too_large |
|
| 415 | unsupported_media_type |
JSON or multipart expected. |
| 422 | unsupported_audio, undecodable_audio, lossless_required, invalid_url, url_not_allowed, fetch_failed, too_many_items, invalid_multipart, invalid_events, too_many_endpoints, invalid_recipient, invalid_entries, invalid_path, invalid_hash, invalid_branch, … |
Validation. Audio errors carry supported (formats) in extra. |
| 429 | rate_limited |
|
| 503 | not_configured |
Watermarking secret absent on this deployment. |
| 500 | internal_error, db_error, storage_error |
Retry with backoff; include the request id when reporting. |
Service descriptor: products, agent entry points, docs. No auth required.
Organization and key metadata.
Metered usage. current is the running month against the plan's included quantities (issuances, detections, storage_bytes, included, hard_cap). reports holds one entry per closed month, newest first (default 12, maximum 36): totals, overage beyond the plan, amount_cents invoiced, and invoiced once the charge has been placed with the billing provider. Reports are written on the first of the following month; developer plans are not metered and have none.
Body: a lossless original (WAV 8 to 32-bit or float, AIFF/AIFC, FLAC), raw or as one multipart part. Headers: X-VYBZ-Title, X-VYBZ-External-Ref, optional X-VYBZ-Content-SHA256.
The file is stored as sent. It is hashed as bytes (sha256) and as canonical PCM (pcm_sha256, identical across lossless containers), and its first ten minutes are fingerprinted so later verifications can identify it without an id. Lossy input is refused with lossless_required.
Returns 201 with the asset, or 200 with existed: true when identical bytes were registered before.
{ "id": "…", "object": "provenance.asset", "title": "…", "sha256": "…", "pcm_sha256": "…", "source_format": "flac", "fingerprint_frames": 12904, "bytes": 1, "sample_rate": 48000, "channels": 2, "duration_sec": 214.5, "links": { … } }Adds issuance_count.
JSON { "recipient": string, "license"?: string, "store"?: boolean, "c2pa"?: boolean }.
recipient is your stable identifier for the receiving party. Each call creates a new issuance with a new watermark, even for the same recipient.
Response 201: WAV bytes with headers X-VYBZ-Issuance-Id, X-VYBZ-Watermark-Id, X-VYBZ-C2PA (1 when a Content Credentials manifest was attached), X-VYBZ-SHA256.
With Accept: application/json or store: true: the copy is stored and the body is the issuance plus download: { url, expires_in: 3600 }.
JSON { "recipients": (string | { "recipient", "license"? })[], "license"?: string, "c2pa"?: boolean }, up to 50 recipients. The original is decoded once; every recipient gets a distinct watermark, a stored copy, and a one-hour download link. Each successful item is one issuance.
Response 201 (or 200 when nothing was issued): { object: "list", asset_id, batch_id, data: [...], summary: { total, issued, errors }, manifest: { url, expires_in } }. Each data item is either { status: "ok", ...issuance, bytes, download } or { name, status: "error", error } in request order. manifest links a stored JSON copy of the response, so the list of links can be handed to whoever distributes the copies. If the plan runs out mid-batch the remaining recipients are reported as plan_limit_reached and no further work is done.
Ordered chain events for the asset: register, issue, c2pa, verify, detect.
Body: the suspect file in any supported format, raw or as one multipart part. It is decoded, resampled to the asset's sample rate when they differ, and correlated against every issuance of the asset. Metered as one detection.
{ "object": "provenance.detection", "input": { "format": "mp3", "sample_rate": 48000, "analyzed_sec": 31.2, "truncated": false, … },
"resampled_from": 48000, "attributed": { "issuance_id", "recipient", "watermark_id", "score", "exact" } | null,
"confidence": "exact" | "high" | "medium" | "none", "statistics": { "z", "ratio" }, "matches": [ …top 25… ], "candidates": 212 }Attribution is asserted on an exact byte or PCM match, when the top score exceeds 0.15 and is at least 2.5× the runner-up, or when the top score is a z-score outlier above 8 against the other candidates and a set of never-issued decoy keys. statistics: { z, ratio } is returned for your own policy. See Provenance.
Up to 25 suspect files: multipart/form-data with any number of file parts, or JSON { "items": [{ "url", "name"? }] } for files to fetch (https, public hosts only). Each item is processed independently and metered as one detection.
{ "object": "list", "asset_id": "…", "data": [ { "status": "ok", "name": "clip1.mp3", …detection }, { "status": "error", "name": "bad.txt", "error": { "code": "unsupported_audio", … } } ],
"summary": { "total": 2, "attributed": 1, "errors": 1 } }Body: any file, raw or as one multipart part. Every method of establishing what the file is runs in order of cost and is reported as evidence:
| Method | Answers | Cost |
|---|---|---|
exact_hash |
Are these the exact bytes of an original or an issued copy? | Free |
pcm_hash |
Is the decoded audio identical to one we hold, in any lossless container or with different metadata? | Free |
fingerprint |
Which registered original does this audio derive from, and at what offset? Survives codecs, bitrate, gain, trimming. | Free |
content_credentials |
Is a C2PA manifest present, and does a VYBZ manifest match the issuance record? | Free |
watermark |
Which recipient's copy is this? Runs with ?attribute=true on the identified asset, or on ?asset=<id>. |
One detection |
{ "object": "provenance.verification", "verdict": "derived_copy", "confidence": "high", "known": true, "kind": "derived",
"input": { "format": "opus", "codec": "opus", "sample_rate": 48000, "duration_sec": 42.1, "truncated": false, … },
"asset": { … }, "issuance": { … },
"evidence": [
{ "method": "exact_hash", "result": "no_match" },
{ "method": "pcm_hash", "result": "no_match" },
{ "method": "fingerprint", "result": "match", "asset_id": "…", "similarity": 0.91, "offset_sec": 41.2, "overlap_sec": 42.0, "votes": 388 },
{ "method": "content_credentials", "result": "absent" },
{ "method": "watermark", "result": "attributed", "confidence": "high", "attributed": { "recipient": "sync-house@partner.com", … }, "statistics": { "z": 37.1, "ratio": 6.2 } }
], "hint": null }Verdicts: original (the registered original, by bytes or PCM), issued_copy (a copy we issued, by bytes or PCM), derived_copy (altered audio attributed to a recipient by watermark), derived_unattributed (derives from a known original, no recipient established), unknown. known and kind remain for older integrations.
Same body shapes as detect/batch; attribute and asset may be form fields, JSON fields, or query parameters. Returns one verification per item and a summary counted by verdict.
The leak report: a stored verification with attribution on by default. Body: the suspect file, raw or as one multipart part; optional note (multipart field or X-VYBZ-Note header, 2,000 characters) is printed on the report. Pass attribute=false to skip the watermark step. Returns 201 with the report: verdict, confidence, asset, issuance (the recipient's copy the file was matched to), evidence[], note, report_hash, and links.pdf. Attribution is metered as one detection when it runs. The submitted file is not stored; its SHA-256 is.
report_hash is SHA-256 over {id, created_at, sha256, verdict, confidence, asset_id, issuance_id, evidence} as JSON. The PDF carries the same value in its footer and in the X-VYBZ-Report-Hash response header, so a report can be checked against the record later.
Reports, newest first. asset filters to one original.
The report as JSON. Append .pdf, pass ?format=pdf, or send Accept: application/pdf for the printable version (Content-Disposition: attachment).
Formats decoded in the edge, formats routed to the decode worker (and whether one is configured), what registration accepts, size and batch limits, and the list of verification methods.
Recomputes the organization's hash chain: { ok, length, first_bad_seq }.
Events are delivered to https endpoints as signed JSON. Scope webhooks:manage for writes, org:read for reads. Up to 20 endpoints per organization.
| Event | Fires | data |
|---|---|---|
asset.registered |
An original was registered. | Asset |
issuance.created |
A watermarked copy was issued. | Issuance plus asset { id, title } |
detection.completed |
A detection ran, from detect or from verify with attribute=true. |
Detection summary without the match list |
detection.attributed |
A detection attributed a recipient. | Same as above |
commit.created |
A Vault commit advanced a branch. | Commit plus repo { id, slug, name } |
ping |
POST /webhooks/{id}/test. |
{ endpoint_id, message } |
Body: { "id", "object": "event", "event", "created_at", "org_id", "data" }. Headers: X-VYBZ-Event, X-VYBZ-Delivery (the event id, stable across retries), X-VYBZ-Attempt, X-VYBZ-Signature.
Verifying a signature. X-VYBZ-Signature is t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, t + "." + raw_body). Compare in constant time and reject timestamps older than five minutes to defeat replay.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyVybz(secret: string, header: string, rawBody: string): boolean {
const t = /t=(\d+)/.exec(header)?.[1], v1 = /v1=([a-f0-9]+)/.exec(header)?.[1];
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Answer 2xx within 15 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours, then marked failed. Deliveries are kept for 30 days.
JSON { "url", "events"?: [...], "description"? }. events defaults to all ("*"). Returns 201 with the endpoint and its secret, shown once.
Any of url, events, description, active. "rotate_secret": true returns a new secret once.
Queues a ping and dispatches. 202.
Recent deliveries with status, attempt, last_status, last_error, and the payload as sent.
JSON { "name", "slug"?, "description"?, "daw"?, "default_branch"? }. 409 slug_taken on collision.
{repo} is an id or slug. Detail adds branches[] and commit_count.
JSON { "hashes": [sha256…] } (max 5000) → { "present": [], "missing": [] }.
Body: raw bytes. Headers: optional X-VYBZ-Content-SHA256 (rejected on mismatch), X-VYBZ-Mime.
Returns { hash, size, existed }. Blobs are deduplicated across the whole organization.
For files above 500 MB, up to 50 GB. Declare the file, send fixed-size parts in order, then complete:
POST /vault/repos/{repo}/uploadsJSON{ "sha256", "size", "mime"? }→201session{ id, part_size, parts, next_part, expires_at, links }. If the blob already exists:200blob withexisted: true. If a session for the same hash is open:200that session withresumed: true.PUT /vault/repos/{repo}/uploads/{id}/parts/{n}raw bytes, exactlypart_sizebytes except the last part. Parts must arrive in order;409 part_out_of_ordercarriesexpected.422 invalid_part_sizecarriesexpectedandreceived.POST /vault/repos/{repo}/uploads/{id}/complete→201blob.409 upload_incompletelistsreceived_bytesandnext_part;409 checksum_mismatchmeans the assembled bytes did not hash to the declared value and the partial object was discarded.GET /vault/repos/{repo}/uploads/{id}reads the session to resume;DELETEaborts it.
part_size is 6 MB. Sessions expire after 24 hours. The gateway hashes every part as it passes, so the blob record is only written once the declared hash is confirmed.
{ hash, size, mime, download: { url, expires_in: 900 } }.
JSON:
{ "branch": "main", "message": "…", "parent": "<expected head sha>" | null,
"entries": [ { "path": "Samples/kick.wav", "hash": "…", "size": 88244 } ], "meta": { "daw": "ableton", "bpm": 124 } }entriesis the complete tree. Paths are/-separated, no.., unique.- Every hash must exist (
409 missing_blobslists up to 100). - Omit
parentto commit on the current head; pass it to enforce optimistic concurrency (409 head_moved). - Identical tree to the head returns
200withunchanged: true. tree_sha= SHA-256 of the canonical sorted entries;sha= SHA-256 of{tree, parent, message, created_at, org}.
History by walking parents from ref (branch or sha; default branch when omitted).
Commit with full entries.
Diff returns added[], removed[], modified[{path, before, after, size}].
Create: { "name", "from"?: branch|sha }.
| Scope | Grants |
|---|---|
org:read |
/me, list and read webhook endpoints and deliveries |
provenance:read |
list/get assets, issuances, ledger, verify, chain |
provenance:write |
register, issue |
provenance:detect |
detect, and verify with attribute=true |
vault:read |
repos, history, commits, tree, diff, branches, blob links, exists |
vault:write |
create repo, upload blobs, commit, create branch |
webhooks:manage |
create, update, delete, test endpoints; retry deliveries |
GET /, GET /openapi.json, and GET /provenance/formats need no scope. A key with none of the scopes above can still authenticate and read the formats list.
Every response carries Access-Control-Allow-Origin: *; keys are bearer secrets, not cookies, so a browser origin gains nothing from it. Preflight allows GET, POST, PUT, PATCH, DELETE and the request headers Authorization, X-API-Key, Content-Type, Accept, Idempotency-Key, X-VYBZ-Org, X-VYBZ-Title, X-VYBZ-External-Ref, X-VYBZ-Content-SHA256, X-VYBZ-Mime, X-VYBZ-Name, X-VYBZ-Attribute, X-VYBZ-Asset. Exposed: X-Request-Id, X-RateLimit-Remaining, X-VYBZ-Watermark-Id, X-VYBZ-Issuance-Id, X-VYBZ-C2PA, X-VYBZ-SHA256, Content-Disposition.
Last updated: 2026-09-08