Skip to content

Addon protocol

Build an addon for BitChord

Connect your own audio catalogue to BitChord with three small JSON endpoints. Your server searches tracks, resolves a playable stream, and describes the audio accurately.

  • 3 HTTP routes
  • JSON wire format
  • GET every request

Addon auth · New

Lock your addon to BitChord

From BitChord 1.9.1, your addon can require proof that a request comes from the official BitChord Android app. The app attaches a hardware-backed proof to each request, and your server checks it. Forks of BitChord, other music apps, scripts, curl and browsers get 401. It’s optional, and recommended for any addon you don’t want used outside BitChord.

What happens to addons that don’t adopt it

Nothing breaks. BitChord keeps searching, streaming and downloading from them exactly as before, without sending any credentials. If your manifest doesn’t declare "bitchordAuth": true, or there is no manifest at all, the addon’s row in Settings → Sources shows a small Outdated addon line. Tapping it tells the listener:

This addon is out of date. It still works, but ask its developer to update it by following the latest BitChord addon docs.

Once you publish the updated manifest, the note goes away the next time BitChord reads it, within about 10 minutes of the listener using the addon. Nobody has to re-add the addon.

Why a User-Agent or client-name check isn’t enough

BitChord is open source, so anything the app sends (its name, its User-Agent, an API key) can be copied into a fork in a minute. That is exactly how forks and other apps have been using addons built for BitChord. The one thing a fork can’t copy is BitChord’s release signing key. Addon auth has Android itself vouch for it.

01AttestKey made in phone hardware
02Token1-hour token, bound to that key
03SignEvery request signed by the key
04VerifyYour server checks, offline
  1. 1
    The app makes a key inside the phone’s secure hardware.

    Android’s key attestation produces a certificate chain, signed by Google, that names the app’s package and signing certificate and says whether the phone’s OS is genuine and its bootloader locked.

  2. 2
    BitChord’s auth service checks that proof.

    It accepts only the official, release-signed BitChord on a phone with a verified OS, and issues a 1-hour token bound to that hardware key. A fork can’t produce this proof without BitChord’s private signing key.

  3. 3
    Every addon request carries the token and a fresh signature.

    The signature is made by the hardware key over your hostname, the time and the token. The key never leaves the phone, so a token copied off a real install is useless anywhere else.

  4. 4
    Your server checks both, locally, in under a millisecond.

    It downloads BitChord’s public keys from https://bitchord-auth.kushagrasingh.in/.well-known/jwks.json about once an hour. It never calls the auth service per request, and there’s nothing to register: no account, no API key.

Who gets refused

ClientWorksWhy
Official BitChord for AndroidYesOn an ordinary phone with a locked bootloader, as before.
Forks, rebuilds, modified APKsNoEven if they keep the BitChord name, package or client string: they aren’t signed with BitChord’s key.
Other apps, scripts, curl, browsersNoThey can’t produce the hardware-backed proof.
Rooted phones, custom ROMsNoBitChord only accepts phones with a locked bootloader and a verified, unmodified OS.
Emulators, phones without secure key storageNoNo hardware to hold the key.
BitChord desktop and webNoThey can’t prove they’re genuine. Once you enforce auth, they can’t use your addon.

Addon auth

Set it up

Five steps. Step 1 clears the “Outdated addon” note; steps 2–4 are what keep other apps out, and step 5 protects your audio files.

01
Declare the flag in your manifest

Add "bitchordAuth": true to manifest.json. true, 1 and the same values as strings are accepted. Keep the manifest itself public: BitChord reads it without credentials to learn that it has to authenticate.

json
{
  "id": "com.example.myaddon",
  "name": "My Addon",
  "version": "1.2.0",
  "resources": ["search", "stream"],
  "bitchordAuth": true
}
02
Add the verifier file to your project

Download the file for your stack above and add it unchanged. Don’t rewrite it: it’s about 200 lines and it’s the reference implementation.

The verifier makes one outbound request: about once an hour it fetches BitChord’s public keys from https://bitchord-auth.kushagrasingh.in/.well-known/jwks.json. That address is built in, so there’s nothing to configure, but if your server restricts outbound traffic, allow HTTPS to bitchord-auth.kushagrasingh.in.

Your stackFileNeeds
Node 20+, Express, Fastify, Hono, Next.js, Cloudflare Workers, Deno, Bun, Vercel/Netlify edgebitchord-addon-auth.mjsNothing. It uses WebCrypto only.
Python 3.9+: FastAPI, Flask, Django, anythingbitchord_addon_auth.pypip install cryptography
Anything else: Go, Rust, PHP, Java…Port itAn Ed25519 and ECDSA P-256 library. See Protocol reference.
03
Check every route except the manifest

Create one verifier per process, at module scope, and run it before search, stream and anything else your addon serves. Send refusals with the helper it gives you, because BitChord reads the WWW-Authenticate header to decide whether to refresh its token. Recipes for each framework are in Framework recipes.

javascript · express
import express from "express"
import { createBitChordAuth } from "./bitchord-addon-auth.mjs"

const app = express()
const bitchord = createBitChordAuth({ hosts: ["myaddon.example.com"] })

app.get("/manifest.json", (req, res) => res.json(manifest)) // stays public
app.use(bitchord.express())                                  // everything below needs BitChord

app.get("/search", async (req, res) => { /* ... */ })
app.get("/stream/:id", async (req, res) => { /* ... */ })

// On success, req.bitchord holds the token's claims.
04
Set hosts to your public hostname(s)

The app signs each request for the hostname in the addon URL the listener pasted: lowercase, no https://, no port, no path. The verifier deliberately ignores the Host header and only trusts the hostnames you configure. If hosts is wrong, every request fails with invalid_proof.

Listener pasteshostsNote
https://myaddon.example.com/manifest.json["myaddon.example.com"]The usual case.
https://myaddon.example.com:8443/abc123/manifest.json["myaddon.example.com"]Port and path are dropped.
A custom domain and *.workers.dev both in useList bothSame for *.onrender.com, *.vercel.app, *.pages.dev.
05
Make media URLs short-lived

Auth protects your JSON endpoints. The audio URL your /stream returns is fetched by the player without BitChord’s headers, so a permanent URL can be shared. Sign it and let it expire: see Short-lived media URLs.

Addon auth

Framework recipes

The same three rules in every framework: one verifier at module scope, the manifest route stays public, and refusals go out through the helper so BitChord gets the status, the WWW-Authenticate header and the JSON body it expects.

javascript
import express from "express"
import { createBitChordAuth } from "./bitchord-addon-auth.mjs"

const app = express()
const bitchord = createBitChordAuth({ hosts: ["myaddon.example.com"] })

app.get("/manifest.json", (req, res) => res.json(manifest)) // stays public
app.use(bitchord.express())                                  // everything below needs BitChord

app.get("/search", async (req, res) => { /* ... */ })
app.get("/stream/:id", async (req, res) => { /* ... */ })

// On success, req.bitchord holds the token's claims.

What the helpers give you

HelperWhereWhat it does
express()JSExpress/Connect middleware. Sends the refusal itself; sets req.bitchord on success.
guard(request)JSFor Fetch-API handlers. Returns a 401/503 Response to send, or null when the request is allowed.
verifyRequest(req)JSVerifies a Fetch Request or a Node request. Returns { ok, claims } or { ok: false, status, error }.
rejection(result)JSThe status, headers and body to send for a failed result, for any other framework.
verify_headers(headers)PythonWorks with Flask, Starlette/FastAPI and Django header objects, or a plain dict.
result.status / headers / bodyPythonWhat to send back when result.ok is false.

In Python, verify_headers makes a blocking request about once an hour to refresh BitChord’s keys. That’s fine in FastAPI’s sync def routes and dependencies, which run in a thread pool. In an async def route, call it through run_in_threadpool.

Addon auth

Short-lived media URLs

Auth protects /search and /stream. The media URL you return is then opened by the player directly, without the BitChord headers. If that URL never expires, anyone who gets hold of one can play it anywhere.

  1. 1
    Expire media URLs in 15–30 minutes.

    Never less than 5: BitChord caches a stream answer for 5 minutes, and a long track that is paused and resumed fetches the URL again.

  2. 2
    Use your storage provider’s signed URLs.

    S3, R2, GCS and B2 can all pre-sign a URL with an expiry. Return that from /stream.

  3. 3
    Or sign your own.

    If you proxy audio yourself, put an HMAC of the track id and expiry in the query string and check it on the media route.

  4. 4
    Don’t require BitChord headers on the media route.

    The player doesn’t send them, so playback would fail.

javascript · node
import crypto from "node:crypto"
const MEDIA_SECRET = process.env.MEDIA_SECRET // long random string, server-side only

// Return this from /stream instead of a permanent URL.
export function signedMediaUrl(id, ttlSeconds = 1800) {
  const exp = Math.floor(Date.now() / 1000) + ttlSeconds
  const sig = crypto.createHmac("sha256", MEDIA_SECRET).update(`${id}.${exp}`).digest("base64url")
  return `https://myaddon.example.com/media/${encodeURIComponent(id)}?exp=${exp}&sig=${sig}`
}

// Check this on the media route. Do NOT require X-BitChord-* headers there.
export function mediaAllowed(id, exp, sig) {
  if (!/^\d+$/.test(String(exp)) || Number(exp) < Date.now() / 1000) return false
  const want = crypto.createHmac("sha256", MEDIA_SECRET).update(`${id}.${exp}`).digest()
  const got = Buffer.from(String(sig ?? ""), "base64url")
  return got.length === want.length && crypto.timingSafeEqual(got, want)
}

Addon auth

Test and roll out

Check it from a terminal

shell
# Manifest: public, and it declares the flag
curl -s https://myaddon.example.com/manifest.json

# Anything else without BitChord: refused with the BitChord challenge
curl -si "https://myaddon.example.com/search?q=test"
# HTTP/1.1 401
# www-authenticate: BitChord realm="bitchord-addons", error="missing", ...

curl -si https://myaddon.example.com/stream/anything     # 401 as well

# Forged headers are refused too
curl -si -H "X-BitChord-Token: x.y.z" -H "X-BitChord-Proof: v1.0.x" \
  "https://myaddon.example.com/search?q=test"            # 401

Check it in BitChord

Add the addon in the official BitChord app (Settings → Sources → Add an addon) and play a track. The row must not show “Outdated addon”, and search and playback must work. If BitChord is refused, its logs show didn’t accept BitChord’s verification (<code>) with one of the error codes.

Roll out without breaking anyone

01
Ship the flag with log-only verification

BitChord sends credentials as soon as it sees the flag. Verify them and log failures, but keep serving every request.

02
Watch the logs for a few days

invalid_proof should be near zero; if it isn’t, hosts is wrong. missing is expected: that’s every client that isn’t BitChord.

03
Switch to blocking

Replace the log-only check with the recipe for your framework. From then on, only BitChord gets through.

javascript
// Step 1 of the rollout: verify, log, but still serve.
app.use(async (req, res, next) => {
  if (req.path.endsWith("/manifest.json")) return next()
  const result = await bitchord.verifyRequest(req)
  if (!result.ok) console.log("bitchord-auth would refuse:", result.error)
  next()
})

Addon auth

Errors and claims

Every refusal is a 401 (or 503) with a WWW-Authenticate: BitChord header and a JSON body. The helpers produce exactly this; BitChord reads the header to decide what to do next.

http
HTTP/1.1 401 Unauthorized
content-type: application/json
cache-control: no-store
www-authenticate: BitChord realm="bitchord-addons", error="expired", error_description="Token expired"

{ "error": "bitchord_auth", "code": "expired", "message": "Token expired" }
CodeStatusMeaningWhat BitChord does
missing401No BitChord credentials: a fork, script or browser, or BitChord before its first authenticated request.Attaches credentials and retries.
expired401The token is past its expiry.Gets a new token and retries once.
invalid_token401The token is malformed, tampered with, or not signed by BitChord (for example a fork running its own auth server).Gets a new token and retries once.
invalid_proof401The signature doesn’t match: hosts is wrong, or a token is being used without its hardware key.Shows an error.
stale_proof401The proof is more than 2 minutes old. Your server’s clock is wrong; the app corrects its own.Shows an error.
unavailable503Your server couldn’t download BitChord’s public keys the first time.Treats it as a temporary outage.

What you get on success

ClaimMeaning
subA random id for this install’s hardware key. It changes when the token refreshes, about hourly. Use it with the IP for rate limiting. It is not a user identity.
verBitChord’s version code, for example 37. Use it if you need a minimum app version.
expWhen the token expires, in Unix seconds.
sltee or strongbox: where the key is held.

No account, email, device id or IMEI is ever sent. Express puts these on req.bitchord; the other helpers return them as result.claims.

Addon auth

Protocol reference

For porting the verifier to another language. Protocol version 1. The two reference verifiers are the authoritative implementation; when in doubt, match them.

Request headers

Sent only to addons that declare the flag (or answer with WWW-Authenticate: BitChord), and only over HTTPS.

http
GET /search?q=midnight&quality=lossless
X-BitChord-Token: eyJhbGciOiJFZERTQSIsImtpZCI6Ii4uLiJ9.eyJpc3MiOi...
X-BitChord-Proof: v1.1793895893081.MEUCIQ...  (86 base64url characters)

The token

A compact JWT signed with Ed25519 (alg: EdDSA, with a kid). The public keys are at https://bitchord-auth.kushagrasingh.in/.well-known/jwks.json. Cache them for an hour, refetch early when a token names an unknown kid (at most every 5 minutes), and answer 503 unavailable if they were never loaded.

json
// Header
{ "alg": "EdDSA", "kid": "h0nr3tdm5pQ", "typ": "JWT" }

// Payload
{
  "iss": "https://bitchord-auth.kushagrasingh.in",
  "aud": "bitchord-addons",
  "sub": "Qm9…",          // install's key thumbprint, rotates hourly
  "iat": 1793895893,
  "exp": 1793899493,      // 1 hour
  "ver": 37,              // BitChord version code
  "sl": "tee",            // tee | strongbox
  "cnf": { "jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" } }
}

The proof

text
// The bytes the device signs (UTF-8, \n is a single line feed):
bitchord-addon-proof/v1\n<host>\n<unix-ms>\n<base64url(SHA-256(token))>

// <host>    one of YOUR configured hostnames, never the request's Host header
// <unix-ms> the number from the proof header, within ±120 s of your clock
// signature ECDSA P-256 / SHA-256, raw r||s (64 bytes), base64url, no padding

Verification order

  1. 1
    Both headers present.

    Otherwise missing.

  2. 2
    Token signature.

    Three parts, alg is EdDSA, the kid is a known key, and the Ed25519 signature over header.payload verifies. Otherwise invalid_token.

  3. 3
    Claims.

    iss is https://bitchord-auth.kushagrasingh.in; aud contains bitchord-addons; iat isn’t in the future and exp hasn’t passed, each with 60 s of skew (expired for the latter); cnf.jwk is an EC P-256 key.

  4. 4
    Proof format and age.

    Matches ^v1\.(\d{10,16})\.([A-Za-z0-9_-]{86})$ (else invalid_proof), and the time is within ±120 s (else stale_proof).

  5. 5
    Proof signature.

    Verify the ECDSA signature with the cnf.jwk key for each configured host. Accept if any verifies, otherwise invalid_proof.

Addon auth

Auth FAQ

Can a fork get past this?

No. The proof comes from Android’s secure hardware and names the app’s signing certificate, which only the official BitChord release has. Changing the code, renaming the app or copying headers from a real install doesn’t produce a valid proof.

Do I need to register my addon or get an API key?

No. Any addon can verify BitChord tokens. The manifest flag and the verifier are the whole integration.

Does BitChord send these headers to every addon?

No. Only to addons that declare bitchordAuth or answer with WWW-Authenticate: BitChord, and only over HTTPS. Each proof is tied to one hostname, so an addon can’t replay what it receives to another addon.

Does this slow my addon down?

No. Verification is local and takes well under a millisecond. The only network call is fetching BitChord’s public keys, about once an hour.

What if BitChord’s auth service is down?

Your addon keeps working for every phone that already has a token (they last an hour), because verification is local and the public keys are cached. Phones that need a new token wait and retry; BitChord treats it as a temporary outage, not a refusal.

My addon shows “Outdated addon” in BitChord.

Your manifest.json doesn’t declare "bitchordAuth": true, or BitChord found no manifest at the addon URL. The addon still works; the note only tells listeners it hasn’t adopted addon auth. Add the flag and protect your routes, and the note goes away the next time BitChord reads your manifest.

Every request fails with invalid_proof.

hosts doesn’t match the hostname in the addon URL listeners pasted. List every hostname your addon is reached at, without scheme, port or path.

Requests fail with stale_proof.

Your server’s clock is more than 2 minutes off. Turn on NTP time sync.

Rooted users are complaining.

BitChord only accepts phones with a locked bootloader and a verified, unmodified OS, so rooted phones and custom ROMs are refused. You can stay in log-only mode to let them in, but then other clients get in too.

Can BitChord desktop or web use my addon?

Not once you enforce auth. Only the Android app can produce the hardware-backed proof.

Overview

How addons work

A listener adds either your root URL or its manifest.json URL. BitChord stores the normalized root, checks the manifest, then tries enabled addons in the order chosen by the listener. When one misses or is unavailable, resolution continues.

01DiscoverGET /manifest.json
02MatchGET /search?q=…
03ResolveGET /stream/{id}
04PlayOpen returned URL

What BitChord does with your answer

  • Matches title, artist, album, and duration to avoid the wrong recording.
  • Sends the listener’s pick from your quality options, or a lossless, high, or low tier.
  • Uses declared codec and transport metadata before guessing from a URL.
  • Caches manifests and searches for 10 minutes, streams for 5 minutes.
  • Attaches a hardware-backed proof to every request for addons that declare bitchordAuth, and marks the rest as outdated without blocking them. See Addon auth.

Build

Quickstart

Expose three routes from any HTTPS server. The example below is deliberately runtime-neutral: connect handleRequest to the router you already use.

manifest.jsonidentity + capabilities
GET /searchcatalogue lookup
GET /stream/:idplayable audio
javascript
import { createBitChordAuth } from "./bitchord-addon-auth.mjs"

// Recommended: only the official BitChord app gets past this. See Addon auth.
const bitchord = createBitChordAuth({ hosts: ["audio.example"] })

const tracks = [
  {
    id: "track_8f31",
    title: "Midnight Signal",
    artist: "Example Artist",
    album: "Night Drive",
    duration: 242,
    artworkURL: "https://audio.example/art/8f31.jpg",
    format: "flac",
    audioQuality: "LOSSLESS"
  }
]

const MANIFEST = {
  id: "dev.example.reference-addon",
  name: "Reference Addon",
  version: "1.0.0",
  resources: ["search", "stream"],
  bitchordAuth: true
}

export async function handleRequest(request) {
  const url = new URL(request.url)

  if (url.pathname === "/manifest.json") {
    return json(MANIFEST) // stays public
  }

  const denied = await bitchord.guard(request) // 401 for anything but BitChord
  if (denied) return denied

  if (url.pathname === "/search") {
    const query = (url.searchParams.get("q") || "").toLowerCase()
    return json({
      tracks: tracks.filter((track) =>
        [track.title, track.artist, track.album]
          .join(" ").toLowerCase().includes(query)
      )
    })
  }

  if (url.pathname.startsWith("/stream/")) {
    const id = decodeURIComponent(url.pathname.slice(8))
    if (!tracks.some((track) => track.id === id)) {
      return new Response(null, { status: 404 })
    }
    return json({
      url: signedMediaUrl(id),
      codec: "flac",
      container: "flac",
      manifest: "none",
      encrypted: false,
      sampleRate: 96000,
      bitDepth: 24
    })
  }

  return new Response(null, { status: 404 })
}

function json(value) {
  return Response.json(value, {
    headers: { "Cache-Control": "no-store" }
  })
}

function signedMediaUrl(id) {
  // Sign and expire this in production: see Short-lived media URLs.
  return `https://audio.example/media/${encodeURIComponent(id)}.flac`
}

Build

Manifest

Serve the manifest at GET /manifest.json. BitChord needs a stableid and a searchable addon. Unknown fields are ignored, so the document can carry additional metadata for your own tooling.

json
{
  "id": "dev.example.reference-addon",
  "name": "Reference Addon",
  "version": "1.0.0",
  "resources": ["search", "stream"],
  "bitchordAuth": true,
  "allowDownloads": 1,
  "checkValidLossless": 0,
  "settings": [
    {
      "key": "quality",
      "type": "select",
      "default": "lossless",
      "options": [
        { "label": "Lossless", "value": "lossless" },
        { "label": "High", "value": "high" },
        { "label": "Low", "value": "low" }
      ]
    }
  ]
}
FieldRequiredMeaning
idRequiredStable, unique addon identifier. An empty id is rejected.
bitchordAuthRecommendedSet true to accept only the official BitChord app (see Addon auth). Without it the addon still works, and Sources marks it as outdated.
nameRecommendedHuman-readable name shown in the source list.
versionRecommendedDisplayed with health information.
resourcesRecommendedInclude search. Stream may be omitted only when rows carry streamURL.
settingsOptionalA quality select becomes a picker in Sources. Other settings send their defaults as query parameters.
allowDownloadsOptional · default 1Set 0 to allow playback but not downloads. See Download permission.
checkValidLosslessOptional · default 0Set 1 to be used only on a lossless-capable output. See Lossless output gate.

Build

Stream endpoint

Accept GET /stream/{id}. Return one openable rendition for the requested quality. A 404 means “not in this addon” and cleanly moves resolution onward.

json
GET /stream/track_8f31?quality=lossless&atmos=auto

{
  "url": "https://audio.example/media/track_8f31.flac",
  "format": "flac",
  "quality": "Lossless · 24-bit / 96 kHz",
  "codec": "flac",
  "container": "flac",
  "manifest": "none",
  "encrypted": false,
  "sampleRate": 96000,
  "bitDepth": 24,
  "bitrate": 2800000
}
FieldRequiredMeaning
urlRequiredAbsolute, directly playable media or manifest URL.
codecRecommendedMost reliable codec signal: flac, alac, mp3, aac, eac3-joc, and others.
containerRecommendedContainer when known.
manifestRecommendednone, hls, or dash. Declare it for extensionless manifest URLs.
encryptedRecommendedUse false. Protected renditions are refused.
sampleRateOptionalSample rate in Hz; a kHz value below 1000 is also normalized.
bitDepthOptionalInteger sample bit depth.
bitrateOptionalBits per second or kbps; BitChord normalizes either convention.
errorOptionalHelpful explanation when no URL can be returned.

Playback

Quality negotiation

BitChord sends a quality parameter on both search and stream requests. If your manifest lists quality options, the value comes from that list. Otherwise it is one of the native tiers below.

Quality options

Declare a setting with key: "quality" and an options list, and BitChord shows the option in use under your addon’s name in Settings → Sources. Listeners tap it to pick another option. Whenever BitChord asks for its best audio, which covers playback and search, it sends their pick, or your first option when they haven’t picked one.

json + http
// manifest.json — a quality select, best option first:
{
  "id": "dev.example.hires-addon",
  "name": "Hi-Res Addon",
  "resources": ["search", "stream"],
  "settings": [
    {
      "key": "quality",
      "type": "select",
      "label": "Audio quality",
      "default": "max",
      "options": [
        { "value": "max", "label": "Max (best available)" },
        { "value": "hires", "label": "Hi-Res (up to 24-bit / 192 kHz FLAC)" },
        { "value": "lossless", "label": "CD (16-bit / 44.1 kHz FLAC)" },
        { "value": "high", "label": "AAC (320 kbps)" }
      ]
    }
  ]
}

// Settings → Sources shows "Max (best available)" under the addon's name.
// Nothing picked yet, so the first option is sent:
GET /search?q=midnight&quality=max
GET /stream/track_8f31?quality=max

// The listener taps it and picks "Hi-Res (up to 24-bit / 192 kHz FLAC)":
GET /search?q=midnight&quality=hires
GET /stream/track_8f31?quality=hires

// Other query parameters are left out here.
Option fieldRequiredMeaning
valueYesSent as quality=. A string or number. Options without one are skipped.
labelRecommendedWhat the listener reads in the picker. The value is shown when it is missing.

Quality option rules that matter

  1. 1
    List your best option first.

    With nothing picked, BitChord sends the first option. It doesn’t use default for this, and it doesn’t look for the word lossless, so the order you list the options in is what decides.

  2. 2
    Offer at least two options.

    With a single option there is nothing to choose: BitChord sends it but shows no picker.

  3. 3
    The pick is the listener’s.

    It is stored on their device for your addon, and the same value goes on search and stream requests. Remove an option and anyone who picked it moves to your first option.

  4. 4
    Changes apply on the next manifest read.

    BitChord saves your options when the addon is added and updates them each time it reads your manifest. Manifests are cached for 10 minutes. Because the options are stored, the picker also works while your addon is unreachable or waiting on the lossless output gate, without contacting you.

  5. 5
    Smaller requests are still matched by name.

    A download the listener keeps at High or Standard asks for HIGH or LOW instead, and their pick doesn’t apply. BitChord sends the option named like that tier: an exact name, then words such as high, 320 or normal for HIGH, and low, 96, 128 or min for LOW. If nothing matches, the first option is sent, so name your smaller renditions that way.

The picker needs BitChord 1.9.1 or later. Earlier versions show no picker and send the option whose name best matches the tier being asked for, so on those an option named lossless is sent ahead of one listed above it.

Without options

An addon that lists no quality options receives one of these tiers as the value.

LOSSLESSBit-exact request

Prefer FLAC, ALAC, WAV, or another lossless codec.

HIGHBest lossy request

Return your best lossy rendition, commonly near 320 kbps.

LOWMetered request

Return a compact rendition, commonly 128 kbps or below.

Do not overstate the stream

BitChord verifies quality using the codec it actually decodes. A lossless label on a lossy URL does not create a lossless badge. Accurate codec, sample-rate, bit-depth, and bitrate fields make selection and playback diagnostics reliable.

Playback

Dolby Atmos

Dolby support has two layers: selecting the immersive recording and describing its stream. Some catalogues expose Dolby as another rendition of one id; others return a separate track id. Your addon should support both shapes.

Request behavior

Respond to atmos=auto

BitChord adds this hint only when the device has a compatible decoder and the listener has Dolby enabled. “Auto” means prefer Dolby when available, otherwise return stereo.

http + json
// BitChord sends this only when Dolby is enabled and supported:
GET /search?q=midnight&quality=lossless&atmos=auto
GET /stream/dolby_8f31?quality=lossless&atmos=auto

// Search result for a distinct Dolby mix:
{
  "id": "dolby_8f31",
  "title": "Midnight Signal",
  "artist": "Example Artist",
  "album": "Night Drive",
  "duration": 242,
  "format": "dash",
  "audioModes": ["DOLBY_ATMOS"],
  "atmos": true
}

// Stream response:
{
  "url": "https://audio.example/media/dolby_8f31",
  "format": "dash",
  "quality": "Dolby Atmos",
  "codec": "eac3-joc",
  "container": "mp4",
  "manifest": "dash",
  "sampleRate": 48000,
  "audioMode": "DOLBY_ATMOS",
  "encrypted": false
}

Dolby rules that matter

  1. 1
    Mark separate rows.

    Use atmos: true, audioMode, or audioModes. This lets BitChord prefer the Dolby row even if its catalogue quality label says LOW.

  2. 2
    Use a recognized codec.

    Return eac3-joc or a clear Dolby label. BitChord normalizes common underscore and hyphen spellings.

  3. 3
    Return stereo when Dolby is absent.

    With atmos=auto, an ordinary track must still play. Do not turn “no Dolby mix” into an error.

  4. 4
    Declare the transport.

    Dolby is often carried by DASH. Set manifest: "dash" even when the URL has no file extension.

Playback

Download permission

Listeners can save tracks for offline playback. By default an addon serves those downloads the same way it serves playback. Set allowDownloads to 0 in your manifest if your addon may be streamed but should never be the source of a saved file.

Manifest switch

allowDownloads · default 1

With 0, BitChord leaves your addon out of every download and asks the next enabled source in the listener’s order instead. Playback is not affected. If the key is missing, downloads are allowed.

json + behaviour
// manifest.json — stream in BitChord, but never be saved from it:
{
  "id": "dev.example.reference-addon",
  "name": "Reference Addon",
  "resources": ["search", "stream"],
  "allowDownloads": 0
}

// Listener taps Download on a track this addon would play:
//   1. Your addon is skipped for the download.
//   2. The next enabled source in the listener's order is asked
//      (another addon, then JioSaavn if enabled, then YouTube).
//   3. Playback is unaffected — /search and /stream keep working.

Download rules that matter

  1. 1
    Write it as a number or a boolean.

    1/0, true/false, and the same values as strings are all accepted. Any other value is ignored and the default applies, so a typo never flips the setting the wrong way.

  2. 2
    Expect no separate download request.

    A download uses the same /search and /stream calls as playback. With 0, those calls are never made for a download, so you don’t need to detect downloads on your server.

  3. 3
    Listeners see it.

    The addon’s row in Settings → Sources shows Streaming only. That tells the listener why a download came from another source.

  4. 4
    Changes apply on the next manifest read.

    BitChord saves the value when the addon is added and updates it each time it reads your manifest. Manifests are cached for 10 minutes.

Playback

Lossless output gate

Some catalogues only want to serve listeners who will actually hear lossless audio. On BitChord for Android, set checkValidLossless to 1 and BitChord uses your addon only while the phone has a lossless-capable output connected. Otherwise your server isn’t contacted at all.

Manifest switch

checkValidLossless · default 0

With 1, BitChord checks the connected audio output before every request. On the phone speaker or a lossy Bluetooth codec it sends nothing: no manifest, health check, search or stream. If the key is missing, there is no check.

json + http
// manifest.json — only use this addon on a lossless-capable output:
{
  "id": "dev.example.hires-addon",
  "name": "Hi-Res Addon",
  "resources": ["search", "stream"],
  "checkValidLossless": 1
}

// Phone speaker, or Bluetooth on SBC / AAC / aptX:
//   no requests at all — not /manifest.json, not /search, not /stream.
//   Tracks resolve from the next source; Sources shows a warning.

// Wired, USB, HDMI, or Bluetooth on LDAC / LHDC / aptX Lossless:
GET /search?q=midnight&quality=lossless&atmos=auto
GET /stream/track_8f31?quality=lossless&atmos=auto

Outputs that qualify

OutputQualifiesNotes
3.5mm headphones / headsetYesWired headphones or a headset on the headphone jack.
USB-C audioYesUSB-C headphones, USB DACs and USB audio accessories.
Line out / auxYesAnalog or digital line out, and aux line.
HDMI, ARC, eARCYesA TV, AV receiver or soundbar over HDMI.
DockYesDigital and analog audio docks.
Bluetooth · LDACYesAny LDAC quality mode: 990, 660, 330 kbps or adaptive.
Bluetooth · LHDCYesLHDC V3, V4 and V5.
Bluetooth · aptX LosslessYesWhen Android reports the codec as aptX Lossless.
Bluetooth · SBC, AAC, aptX, aptX HD, aptX AdaptiveNoLossy codecs below CD bitrate.
LE Audio (LC3), Opus, hearing aidsNoLossy codecs.
Phone speaker / earpiece, castingNoThe audio doesn’t reach a lossless path.
How Bluetooth is checked

BitChord reads the codec Android reports

On Android 12 and later this needs the Nearby devices permission. If an addon is waiting and BitChord can’t read the codec, Settings → Sources offers Check Bluetooth codec to grant it. LHDC and aptX Lossless are named by Android 15+ and by phones whose maker reports vendor codecs. Where Android doesn’t report the codec, BitChord treats it as unknown and doesn’t count it.

Lossless gate rules that matter

  1. 1
    The first contact is the manifest.

    BitChord has to read your manifest once to learn the switch: when the listener tests or saves the addon. After that the value is stored on the device, so a gated addon is never contacted just to check whether it is still gated.

  2. 2
    Nothing reaches you while no output qualifies.

    The addon is left out of playback, search and downloads, and every request path refuses before it opens a connection. That includes queued tracks that already name your addon. They resolve from the next source.

  3. 3
    It resumes on its own.

    When the listener plugs in headphones or switches Bluetooth to LDAC/LHDC, the addon rejoins the source order immediately, with no restart. Its health check runs again on the Sources screen.

  4. 4
    The listener is told why.

    The addon’s row in Settings → Sources says Needs a lossless output. If Bluetooth is connected, the row names the current codec so the listener knows what to change.

  5. 5
    Turning it on for an existing addon.

    Devices that already have your addon learn the new value the next time they read your manifest (cached for 10 minutes). That read is the last request they make until a qualifying output is connected.

Both switches need BitChord 1.7.1 or later. allowDownloads is honoured by BitChord for Android and desktop; checkValidLossless by Android only, and the desktop app ignores it. Earlier versions ignore unknown manifest keys, so adding them is always safe.

Ship

Install and test

01
Publish your server

Make the root and all three routes reachable over HTTPS. With addon auth, check every route but the manifest.

02
Add the URL

In BitChord, open Settings → Sources → Add an addon. Paste the root or manifest URL.

03
Run the health check

BitChord reads the manifest, notes whether it declares bitchordAuth, and verifies the addon is searchable.

04
Play representative tracks

Test direct audio, manifest-based audio, every quality tier, misses, and Dolby.

Fast contract checks

GET /manifest.json → 200, "bitchordAuth": trueGET /search?q=test without BitChord → 401GET /search?q=test&quality=LOSSLESSGET /stream/<encoded-id>?quality=LOSSLESS

Ship

Production checklist

bitchordAuth: true in the manifest
Every route but manifest.json verified
hosts lists every public hostname
Signed media URLs, 15–30 minute expiry
Stable addon and track ids
HTTPS on every returned URL
JSON content on all success responses
Search answers quickly
Stream URLs survive at least five minutes
404 for a missing track
429 includes Retry-After
No encrypted rendition unless requested
Accurate codec and transport fields
Logs never expose path-based tokens
Stereo fallback for Dolby auto mode
Representative duration metadata
Quality options listed best first
allowDownloads set deliberately
checkValidLossless only if you need it

Reference

Troubleshooting

“Outdated addon” under your addon in Sources

Your manifest doesn’t declare "bitchordAuth": true, or there’s no manifest at the addon URL. The addon still works. To clear it, follow Addon auth → Set it up; devices pick up the change the next time they read your manifest.

“didn’t accept BitChord’s verification (invalid_proof)”

The hosts you passed to the verifier don’t match the hostname in the addon URL. List every hostname, without scheme, port or path.

“That URL answered, but not with an addon manifest”

Return an object with a non-empty id from /manifest.json, or confirm the pasted URL resolves to your addon root.

The addon is healthy but returns no results

Check the q parameter, return a tracks array, and ensure each row has both id and title.

Audio fails on an extensionless URL

Declare manifest as hls or dash. For direct audio, provide codec and container.

A track falls back to another source

Return 404 only for a true miss. Check for an empty URL, an encrypted response, malformed URL, or unsupported Dolby stream.

Dolby always returns stereo

Read atmos=auto on both search and stream. If Dolby is a separate catalogue row, mark that row with atmos or audioModes.

Downloads come from another source

Your manifest has allowDownloads set to 0 (the row in Sources says Streaming only). Remove the key or set it to 1; devices pick the change up on their next manifest read.

The addon gets no requests at all

If checkValidLossless is 1, BitChord stays silent until a wired, USB, HDMI or LDAC/LHDC/aptX Lossless output is connected. The row in Sources says Needs a lossless output. On Bluetooth, grant Nearby devices so the codec can be read.

LDAC headphones are not accepted

Check that the phone is actually using LDAC (Developer options → Bluetooth audio codec) and that Nearby devices is allowed for BitChord. Without that permission the codec is unknown and does not count.

Listeners get a lower tier than your best

With nothing picked, BitChord sends your first quality option, not default and not the option named lossless. Move your best option to the top of options.

No quality picker appears under the addon

Declare a setting with key quality and at least two options that each have a value. Devices show it after their next manifest read; opening Settings → Sources triggers one.

The wrong recording is selected

Improve artist, album, and duration metadata. Avoid live, edit, remix, or instrumental variants unless the query asks for them.