snvrkotics

Developers

Your rights data, over HTTP.

5 read endpoints over the catalog, releases, links, royalties and the ledger, plus 5 signed webhook events so an integration is told rather than having to ask. On Pro and above.

Read-only, deliberately. Writing rights records over an API is how a catalog gets quietly corrupted by a script nobody is watching; every change still happens in the product, where it is validated and attributed.

Getting a key

One header, and you are in

Make a key in Settings → API. It is shown once and stored only as a hash, so a copy of our database cannot call the API as you. Send it as a bearer token.

curl https://snvrkotics.com/api/v1/recordings \
  -H "Authorization: Bearer snvrkotics_sk_…"

Every response is scoped to that key’s workspace. 120 requests a minute per key, with X-RateLimit-Remaining on every response and a 429 when you pass it. A key stops working the moment its workspace drops below Pro, so entitlement is checked on each call rather than at creation.

Paging

The lists answer up to limit rows (1 to 200, default 50) and a next value. Send it back as after for the following page; it is null on the last one. The cursor is a row id, not an offset, so a walk that runs while the catalog changes still returns each row once. The two summaries are totals over every row and come back whole, with next always null.

curl "https://snvrkotics.com/api/v1/recordings?limit=100" -H "Authorization: Bearer snvrkotics_sk_…"
# { "data": [ … 100 rows … ], "count": 100, "next": "6f1c…" }

curl "https://snvrkotics.com/api/v1/recordings?limit=100&after=6f1c…" -H "Authorization: Bearer snvrkotics_sk_…"
# … until "next": null

Endpoints

5 read endpoints

GET/api/v1/recordings

Recordings in the workspace, newest first, with their identifiers and registration readiness. Paged by cursor.

  • isrc — Return only the recording with this ISRC.
  • limit — 1 to 200, default 50.
  • after — The `next` value from the previous page. Omit for the first page; `next` is null on the last.
{
  "data": [
    {
      "id": "…",
      "title": "Night Bus",
      "version": null,
      "artist": "Mara Vance",
      "isrc": "QZSNV2600001",
      "iswc": "T-000.000.001-0",
      "durationSeconds": 214,
      "releaseDate": "2026-01-16",
      "explicit": false,
      "oneStop": true,
      "readyToRegister": true,
      "completeness": 94
    }
  ],
  "count": 1,
  "next": null
}

GET/api/v1/releases

Releases with their UPC, label, date and track order, newest release date first. Paged by cursor.

  • limit — 1 to 200, default 50.
  • after — The `next` value from the previous page. Omit for the first page; `next` is null on the last.
{
  "data": [
    {
      "id": "…",
      "title": "Night Bus",
      "artist": "Mara Vance",
      "type": "ep",
      "upc": "197644000015",
      "releaseDate": "2026-05-22",
      "label": "Vance Songs",
      "tracks": [{ "trackNumber": 1, "title": "Night Bus", "isrc": "QZSNV2600001" }]
    }
  ],
  "count": 1,
  "next": null
}

GET/api/v1/royalties/summary

Net royalties by month, store and recording, from every imported statement. Totals over every row, returned whole: takes no `after`, and `next` is always null.

  • year — Limit to one calendar year, e.g. 2026.
{
  "currency": "USD",
  "totalNetCents": 165788,
  "unmatchedCents": 2026,
  "byMonth": [{ "month": "2026-08", "netCents": 39974 }],
  "byStore": [{ "store": "Spotify", "netCents": 78207, "units": 231044 }],
  "byRecording": [{ "title": "Night Bus", "isrc": "QZSNV2600001", "netCents": 81800 }],
  "next": null
}

GET/api/v1/ledger/summary

The year's profit and loss: income, costs, net and the tax set-aside. Totals over every row, returned whole: takes no `after`, and `next` is always null.

  • year — Defaults to the current calendar year.
{
  "year": 2026,
  "incomeCents": 435788,
  "costCents": 263307,
  "netCents": 172481,
  "taxSetAsideCents": 51744,
  "byMonth": [{ "month": "2026-08", "incomeCents": 39974, "costCents": 0, "netCents": 39974 }],
  "next": null
}

Webhooks

Told, not asked

Add an endpoint in Settings → API, pick the events, and we post to it. Every delivery is signed, and the signing secret is shown once.

statement.imported

A royalty statement finished importing.

{ "id": "…", "source": "DistroKid", "lineCount": 603, "netCents": 165788, "periodStart": "2026-01-01", "periodEnd": "2026-06-30" }

payout.created

A payout run was created for collaborators.

{ "id": "…", "totalCents": 42000, "status": "draft", "periodStart": "2026-01-01", "periodEnd": "2026-06-30" }

payout.finalized

A payout run was finalized: its amounts are final, statements can be sent and it counts in the ledger.

{ "id": "…", "totalCents": 199106, "people": 3, "status": "final", "periodStart": "2026-04-01", "periodEnd": "2026-06-30", "finalizedAt": "2026-07-02T15:00:00.000Z" }

link.published

A smart link went live. url is the address it answers on (own domain, nvr.to subdomain, or nvr.to).

{ "id": "…", "slug": "night-bus", "title": "Night Bus", "url": "https://vance.nvr.to/night-bus" }

split.accepted

A collaborator accepted their share of a split sheet.

{ "shareId": "…", "recordingId": "…", "collaborator": "Deon Park", "everyPartyAccepted": false }

Verifying a delivery

Each request carries X-SNVRK-Signature: t=<unix seconds>,v1=<hex>. The signature is an HMAC-SHA256 over "<t>.<raw body>" using your signing secret. Compare in constant time, and reject anything older than five minutes so a captured delivery cannot be replayed.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, header) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Delivery, stated plainly

One attempt, with a five-second timeout, and the outcome is shown against the endpoint in settings. There is no retry queue, and we would rather say so than imply one: if a delivery matters, read the matching endpoint above to reconcile. Deliveries never block the thing that caused them, so an endpoint being down cannot fail an import.

Webhooks follow the same plan rule as keys. Endpoints are added on Pro and above; if a workspace drops below it, its endpoints stay in settings but nothing is sent to them, and each one says so, until the plan returns.

Building something with it? Tell us — and the MCP server exposes SNVRKOTICS itself to an AI assistant with no key at all.

Rights, royalties & links — free.Start free
API and webhooks | SNVRKOTICS