Extension: fora-music-v1
Overview
Section titled “Overview”FORA-Music v1 defines a standardized vocabulary of ext field keys for music recordings traded through FORA Exchanges. It enables agents and Brokers to:
- Discover recordings by genre, mood, instruments, suitability, energy, vocals, tempo and duration
- Filter a catalog search with required
music.*constraints inDiscoveryRequest.search_filters - Find a specific recording by its ISRC, and identify the source recording of a delivered file
The profile is recording-discovery metadata. It is not a rights model: it has no fields for composition ownership, royalty splits or clearance. Permission to use a recording comes only from the offer’s license terms.
Who Should Implement This
Section titled “Who Should Implement This”- Exchanges serving music libraries, production music catalogs or labels
- Brokers that search music catalogs by musical facets
- Agents that choose music for videos, podcasts, games or other productions
Prerequisites
Section titled “Prerequisites”Implementers must be familiar with:
- FORA protocol (
proto/fora/v1/fora.proto) - The
extfield mechanism (google.protobuf.Structon all major messages) - The
OfferandDiscoveryRequestmessages
Live Demo: Music shows the profile in use: an agent finds a track and buys a license to use it in a video.
Profile Declaration
Section titled “Profile Declaration”An Exchange declares support for this profile by including "fora-music-v1" in its WellKnownManifest.supported_profiles array (role=ROLE_EXCHANGE):
{ "ver": "1.0", "role": "ROLE_EXCHANGE", "domain": "music-exchange.example.com", "name": "Example Music Exchange", "supported_profiles": ["fora-music-v1"], "endpoint": "https://music-exchange.example.com/fora/v1"}For this profile, the declaration means one thing: the Exchange forwards the publisher’s music.* keys unchanged inside the signed offer. The Exchange does not validate them. It treats fora-news-v1 the same way. This is narrower than the general rule in Extension Profiles, which says that an Exchange validates profile fields against the profile’s JSON Schema.
The Broker does not route requests by supported_profiles.
Field Summary
Section titled “Field Summary”This profile defines 10 field names and 9 search filter keys. Every key is namespaced with music., and all keys are optional.
| Message | Field Count | Purpose |
|---|---|---|
Offer.ext | 10 | Recording metadata on an offer: 1 descriptive field and 9 fields used for matching |
DiscoveryRequest.search_filters | 9 | Required constraints on a catalog search |
Offer Metadata (Offer.ext)
Section titled “Offer Metadata (Offer.ext)”Each field is one key of Offer.ext, named music.<field>, like the keys of the news, academic and legal profiles (for example news.section). In a catalog feed record the keys are in the record’s ext, and the Exchange copies the record’s ext into Offer.ext before it signs the offer.
An offer carries fora-music-v1 metadata when its ext has at least one music.<field> key. A nested ext.music object is not part of the profile: consumers treat the key music as an unknown key.
The search filter keys use the same names: music.genre filters on the field whose key is music.genre. This page names a field without its prefix (genre, vocals) when the meaning is clear.
| Key | Type | Required | Used for Matching | Description |
|---|---|---|---|---|
music.artist | string | Optional | No | Display credit. Case is preserved. It does not identify the publisher, the rights owner or a contributor. |
music.isrc | string | Optional | Yes | ISRC of the source recording: 12 characters without hyphens (^[A-Z]{2}[A-Z0-9]{3}[0-9]{7}$). When the delivered file is an excerpt or an edit, isrc still names the source recording. |
music.genre | label | Optional | Yes | One primary genre. |
music.mood | label[] | Optional | Yes | Moods. |
music.instruments | label[] | Optional | Yes | Instruments heard in the file. Category labels such as brass or strings are valid. |
music.suitable_for | label[] | Optional | Yes | Editorial suitability, for example vlog, documentary or trailer. It does not give permission to use the file. |
music.energy | string | Optional | Yes | Editorial energy level: low, medium or high. It does not depend on bpm. |
music.vocals | boolean | Optional | Yes | See Vocals. |
music.bpm | integer | Optional | Yes | Tempo in beats per minute, 1 or more. Fractional BPM is not supported. |
music.duration_seconds | number | Optional | Yes | Duration of the delivered file, more than 0. For a 60-second excerpt it is the excerpt’s duration (for example 60.03), not the duration of the source recording. |
artist is descriptive. No search filter refers to it. isrc is used only by the music.isrc filter, as an exact match.
Labels
Section titled “Labels”genre, mood, instruments and suitable_for hold labels. A label:
- is not empty;
- has no whitespace at the start or at the end;
- has no upper-case or title-case letter.
Both rules use Unicode. Whitespace includes the non-breaking space. Upper case and title case are the Unicode categories Lu and Lt, so électronique is a label and Électronique is not.
Consumers do no stemming and no singular/plural matching, so guitar and guitars are different labels. The feed must use one label for one thing.
Vocals
Section titled “Vocals”vocals describes the delivered file. It is true when the file contains any human voice that can be heard: sung lyrics, wordless singing, choir or spoken word. It is false only when the file contains no human voice.
The definition is broad on purpose. An agent that filters on vocals: false usually needs a track that does not compete with narration, and any voice does.
Absent, Empty and Null
Section titled “Absent, Empty and Null”- An absent field means “unknown”. An absent
vocalsnever meansfalse. - An empty array means “known to be empty”. It is different from an absent array.
nullis not a valid value, and the schema rejects it. A publisher leaves out an unknown field. A consumer that receivesnulltreats the field as absent.- Consumers ignore fields they do not know. A publisher can send other fields, for example other rights or release identifiers, and consumers still work. A field is added to this profile when a consumer uses it.
Query Vocabulary (DiscoveryRequest.search_filters)
Section titled “Query Vocabulary (DiscoveryRequest.search_filters)”An agent can limit a catalog search with DiscoveryRequest.search_filters. The Broker answers these filters only when its catalog search (query discovery) is switched on.
| Key | Type | Required | Description |
|---|---|---|---|
music.isrc | string or string[] | Optional | ISRC, 12 upper-case characters without hyphens (^[A-Z]{2}[A-Z0-9]{3}[0-9]{7}$). The match is exact. An array must not be empty. Example: "USXXX2300011". |
music.genre | label or label[] | Optional | Genre. An array must not be empty. Example: "cinematic", ["jazz", "blues"]. |
music.mood | label or label[] | Optional | Mood. An array must not be empty. Example: "calm". |
music.instruments | label or label[] | Optional | Instruments. An array must not be empty. Example: ["piano", "strings"]. |
music.suitable_for | label or label[] | Optional | Editorial suitability. An array must not be empty. Example: "trailer". |
music.energy | string or string[] | Optional | low, medium or high. An array must not be empty. Example: "high". |
music.vocals | boolean | Optional | Example: false. |
music.bpm_range | string | Optional | MIN-MAX, MIN- or -MAX with positive integers. Both bounds are inclusive, and MIN is not greater than MAX. Example: "115-125", "120-". |
music.duration_seconds_range | string | Optional | MIN-MAX, MIN- or -MAX with positive numbers of seconds. Both bounds are inclusive, and MIN is not greater than MAX. Example: "30-90", "-60.5". |
A range key is the name of the field it limits, followed by _range.
How Filters Match
Section titled “How Filters Match”- Filters are required. A track must match every filter key. When a key has several values, the track must match at least one of them. In other words, the values of one key are combined with OR, and the keys are combined with AND.
- A track with an unknown field does not match a filter on that field.
music.isrcmatches a track only when the track’smusic.isrcis exactly one of the given values. The Broker never reads an ISRC from the free-textquery, so only this filter finds a track by its ISRC.- The Broker also reads tags from the free-text
query. These tags are preferences, and they apply only to the tracks that pass every filter. Among those tracks, the tracks that match the most query tags come first. When no passing track matches a query tag, or the query has no known tag, the Broker returns every track that passes the filters. A query tag never adds a track that fails a filter. - When both the filters and the query give bounds for the same field, the Broker uses the overlap of the two ranges. If the ranges do not overlap, no track matches.
- A request with filters and no
querydoes not call the query model. This includes a request with onlymusic.isrc,music.bpm_rangeormusic.duration_seconds_range. - A valid filter value that no track has, for example
music.genre: "funk"in a catalog without funk, matches no track. The answer isNOT_IN_CATALOG. The Broker never drops such a filter.
Order of Checks and Errors
Section titled “Order of Checks and Errors”The Broker checks a request in this order:
- Explicit URIs come first. When the request lists
uris, the Broker resolves them and ignoresqueryandsearch_filters. It does not validate the filters, and the query discovery switch does not apply. - Unknown keys are ignored. A key that this profile does not define is ignored, for example
music.tempoor a key of another profile. - Invalid values are rejected. A defined key with a value outside its format rejects the request with
INVALID_ARGUMENT, and no search runs. Examples:music.bpm_range: "fast", an upper-case label, an ISRC with hyphens or lower-case letters, an empty array. - A search input is required. A request with no
uris, noqueryand no defined filter key is rejected withINVALID_ARGUMENT. This is the same as a request with neitherquerynoruris. - Filters need catalog search. When the Broker’s query discovery is switched off, a request with a defined filter key is rejected with
UNIMPLEMENTED. The Broker in this configuration cannot apply the filters, and it must not return results that ignore them. The code is notINVALID_ARGUMENT, because the request is valid. It is notFAILED_PRECONDITION, because the caller cannot change the Broker’s configuration.
Behavioral Conventions
Section titled “Behavioral Conventions”Validation
Section titled “Validation”| Party | What it checks |
|---|---|
| The JSON Schema | The shape of all ten fields, including the ISRC format and the label format. |
| Broker | The nine matching fields, when it loads its catalog feed. An invalid matching field fails the load, and the error names the record and the field. The Broker does not read artist. |
| Exchange | Nothing. It forwards the music.* keys unchanged inside the signed offer, so the offer signature covers every key, including unknown ones. |
Tempo Groups
Section titled “Tempo Groups”Tempo groups such as slow, medium and fast are Broker behavior. They are not part of the profile. The profile carries only the exact bpm.
Examples
Section titled “Examples”Example 1: Music Track Offer
Section titled “Example 1: Music Track Offer”An Exchange returns an offer for a 60-second excerpt of an instrumental track. The offer sells a one-time license for one personal video. The terms are shortened; see Licensing Terms.
{ "offer_id": "1f0c7a52-8d3e-4b1a-9c6e-2a4f5b7d8e90", "title": "Mesmerizing Galaxy — Kevin MacLeod", "pricing": { "model": "PRICING_MODEL_FLAT", "rate": "29", "currency": "EUR", "metering": "PRICING_METERING_NONE" }, "delivery_method": "DELIVERY_METHOD_DIRECT", "identity": { "canonical_url": "https://music-exchange.example.com/tracks/mesmerizing-galaxy-loop.mp3", "content_hash": "sha256:b430ef4d...", "hash_method": "sha256", "resource_mutability": "RESOURCE_MUTABILITY_STATIC" }, "terms": [ /* one LicenseTerm: individuals, one personal video */ ], "ext": { "music.artist": "Kevin MacLeod", "music.genre": "electronica", "music.mood": ["grooving", "mysterious"], "music.instruments": ["synths"], "music.suitable_for": ["podcast", "game"], "music.energy": "medium", "music.vocals": false, "music.bpm": 124, "music.duration_seconds": 60.03 }, "signature": "ZXhjaGFuZ2Utc2lnbmF0dXJl...", "signature_algorithm": "EdDSA"}The offer has no music.isrc, so the source recording is unknown. The offer has no ext_critical, so a consumer that does not know this profile can ignore the music.* keys.
Example 2: Filtered Discovery Request
Section titled “Example 2: Filtered Discovery Request”An agent asks for funk tracks between 115 and 125 BPM with no vocals, for a cooking video:
{ "query": "upbeat background music for a cooking video", "search_filters": { "music.genre": "funk", "music.bpm_range": "115-125", "music.vocals": false }}The Broker returns only tracks with music.genre: "funk", a music.bpm from 115 to 125 and music.vocals: false. A track with no music.vocals key is not returned. Among the tracks that pass, the tracks that match the most tags the Broker reads from the query come first. If the catalog has no funk track, the answer is NOT_IN_CATALOG.
Example 3: Rejected Filter
Section titled “Example 3: Rejected Filter”An agent sends a tempo word instead of a range:
{ "query": "fast drum and bass", "search_filters": { "music.bpm_range": "fast" }}"fast" is not in the MIN-MAX, MIN- or -MAX format, so the Broker rejects the request with INVALID_ARGUMENT and runs no search. The agent can send "music.bpm_range": "160-", or leave the tempo in the free-text query.
Conformance
Section titled “Conformance”The profile defines no conformance levels. Every field is optional, and a publisher sends the fields it knows.
- An Exchange that declares
fora-music-v1MUST forward the publisher’smusic.*keys unchanged inside the signed offer. It does not validate them. - A Broker that answers
music.*search filters MUST apply them as required constraints, in the order of checks above. When its catalog search is switched off, it MUST reject a request with a defined filter key withUNIMPLEMENTED. - A publisher can validate the
extof each record against the JSON Schema before it pushes the feed. This finds the errors early, because the Broker refuses a feed with an invalid matching field.
SDK Types
Section titled “SDK Types”The SDKs have a typed view of the music.* keys of Offer.ext:
| Language | Import | Names |
|---|---|---|
| Go | github.com/FORA-Protocol/protocol/sdk/go/profiles/music | music.Music, music.ProfileID |
| TypeScript | @fora-protocol/sdk/music | Music, ProfileID |
| Python | fora_sdk.music | Music, PROFILE_ID |
The types keep the rules from Absent, Empty and Null: an absent key stays absent, and an empty list stays an empty list. Decoding an Offer.ext into the Go type keeps only the music.* keys. A music.bpm written as a whole number with a fraction or an exponent, for example 90.0 or 1e2, is valid, because JSON Schema counts it as an integer. The Go type accepts it as 90 or 100. The types do not validate the values. Use the JSON Schema to validate input that you do not trust.
JSON Schema
Section titled “JSON Schema”The schema is music-v1.schema.json (JSON Schema Draft 2020-12). It applies to the whole ext object and allows other keys, including unknown music.* keys.
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://fora-protocol.org/schemas/music/v1/music-v1.schema.json", "title": "fora-music-v1 — the music.<field> keys of Offer.ext", "type": "object", "additionalProperties": true, "properties": { "music.artist": {"type": "string", "minLength": 1}, "music.isrc": {"type": "string", "pattern": "^[A-Z]{2}[A-Z0-9]{3}[0-9]{7}$"}, "music.genre": {"$ref": "#/$defs/Label"}, "music.mood": {"$ref": "#/$defs/Labels"}, "music.instruments": {"$ref": "#/$defs/Labels"}, "music.suitable_for": {"$ref": "#/$defs/Labels"}, "music.energy": {"enum": ["low", "medium", "high"]}, "music.vocals": {"type": "boolean"}, "music.bpm": {"type": "integer", "minimum": 1}, "music.duration_seconds": {"type": "number", "exclusiveMinimum": 0} }, "$defs": { "Label": { "type": "string", "pattern": "^[^\t-\r\u0085\\p{Z}\\p{Lu}\\p{Lt}](?:[^\\p{Lu}\\p{Lt}]*[^\t-\r\u0085\\p{Z}\\p{Lu}\\p{Lt}])?$" }, "Labels": { "type": "array", "items": { "$ref": "#/$defs/Label" } } }}Validating in Python
Section titled “Validating in Python”The label pattern uses Unicode property classes (\p{Z}, \p{Lu}, \p{Lt}). JSON Schema allows them, because it uses ECMA-262 regular expressions. Python’s re module does not support them. The standard pattern check of the Python jsonschema package uses re, so jsonschema.validate() fails on any object that has a label field, with the error bad escape \p.
Validators in Go and JavaScript that follow ECMA-262, such as santhosh-tekuri/jsonschema and Ajv 8, need no change.
In Python, replace the pattern check with one that uses the regex package, which supports these classes. Install both packages:
pip install jsonschema regexThen build the validator from the extended class, and use it for every check. Do not call jsonschema.validate() directly, because it uses the standard pattern check.
import json
import regexfrom jsonschema import Draft202012Validator, ValidationError, validators
def _unicode_pattern(validator, pattern, instance, schema): # Replaces the standard "pattern" check, which uses Python's re module. # The regex module supports the \p{...} classes that the label pattern uses. if validator.is_type(instance, "string") and not regex.search(pattern, instance): yield ValidationError(f"{instance!r} does not match {pattern!r}")
MusicValidator = validators.extend(Draft202012Validator, {"pattern": _unicode_pattern})
with open("music-v1.schema.json", encoding="utf-8") as f: validator = MusicValidator(json.load(f))
music = {"music.genre": "Jazz", "music.bpm": 90}for error in validator.iter_errors(music): print(f"{error.json_path}: {error.message}")This prints the error for the upper-case label Jazz. Save the schema above as music-v1.schema.json before you run it.
Out of Scope
Section titled “Out of Scope”- Rights-model fields: composition ownership, royalty splits, clearance.
- Rights and release identifiers other than ISRC: ISWC, ISNI, IPI, UPC, GRid, ISAN, EIDR, HFA. Publishers can send them as unknown fields.
- Broker routing by
supported_profiles. - Validation of the
music.*keys by the Exchange.
References
Section titled “References”Normative
Section titled “Normative”- ISO 3901 International Standard Recording Code (ISRC) / ISRC Handbook
- JSON Schema Draft 2020-12
- Unicode Character Database (UAX #44): the general categories Lu and Lt, and the White_Space property
- ECMA-262 regular expressions: the syntax of the schema’s
patternvalues