Find the songwriter behind a name, an IPI name number or a recording's ISRC. The official Java SDK for Songlinks.
One songwriter is rarely one record. They're registered under different spellings, under several IPI name numbers, or with no number at all. Songlinks groups those registrations into one person, shows the evidence for every join, and keeps apart the ones it isn't sure of.
- Free, with no key and no sign-up.
- Typed. Every answer is a Java class with named fields, checked against the API's own description of itself on every build.
- Light. Java 17+, the JDK's own
HttpClient, and Jackson for JSON. Nothing else. - Built for real apps. Clear errors, automatic retries when the API is busy, and long lists paged for you.
New to this? Skip to Your first program, step by step: it starts from installing the language and ends with a working answer. The words used here are explained too.
Not on Maven Central yet. Until it is, install it from a checkout, which puts it in your local Maven repository:
git clone https://github.com/Alexkane/songlinks-java && cd songlinks-java && ./mvnw -q install -DskipTestsThen depend on it as usual:
<dependency>
<groupId>io.songlinks</groupId>
<artifactId>songlinks</artifactId>
<version>0.2.0</version>
</dependency>Gradle: implementation("io.songlinks:songlinks:0.2.0")
These lines go inside a main method; examples/ResolveName.java shows a whole program.
import io.songlinks.Songlinks;
import io.songlinks.model.*;
import java.util.List;
Songlinks songlinks = Songlinks.builder().userAgent("my-app/1.0").build();
LookupResponse answer = songlinks.lookup("mark hoppus");
if ("rejected".equals(answer.answer())) { // for example a one-word name, which matches too many people
throw new IllegalStateException("no answer: " + answer.rejection().code());
}
Cluster candidate = answer.candidates().get(0);
System.out.println(candidate.persistentId() + " " + candidate.names() + " " + candidate.works());
// SLP-ZYM7-GD3E-S [MARK HOPPUS] 302
songlinks.person("SLP-ZYM7-GD3E-S", 0, 3).worksList().items().forEach(w -> System.out.println(w.title()));
// I MISS YOU / ALL THE SMALL THINGS / DOWN
WritersResponse check = songlinks.writers(List.of("00338980326", "00340247397"));
check.writers().forEach(w -> System.out.println(w.name() + " " + w.personId()));
// MARK HOPPUS SLP-ZYM7-GD3E-S
// TRAVIS L BARKER SLP-MG23-XM9B-Y
RecordingAnswer recording = songlinks.songwriters(List.of("USMC19959123")).recordings().get(0);
recording.works().get(0).writers().forEach(w -> System.out.println(w.name() + " " + w.persistentId()));
// MARK HOPPUS SLP-ZYM7-GD3E-S
// TRAVIS L BARKER SLP-MG23-XM9B-Y
// THOMAS DELONGE SLP-MEA6-3QQJ-CEvery person has a page you can link to, such as songlinks.io/person/SLP-ZYM7-GD3E-S. It shows the registrations behind the person and what each join rests on.
Beta. Unverified machine output, from a dated snapshot of registration data (a lookup's or a person's
provenance().mlcSnapshot()gives its date, andversion()gives the current one). Some answers are wrong and some are incomplete. Not for rights or royalty decisions.
// Which of several writers with the same name wrote a song? Pass one of their songs.
SongMatch match = songlinks.lookup("amy allen", LookupOptions.song("espresso")).songMatch();
// match.match() = "exact", match.candidates() = [c1]: the candidate whose ref() is "c1" is the Amy Allen credited
// on that song
// Every work of a person, read lazily, a page at a time.
songlinks.personWorks("SLP-ZYM7-GD3E-S").forEach(w -> System.out.println(w.title()));Have a list of recordings, say from a label's catalogue or a distributor's report? songwriters() takes up to 50 of
their ISRCs at once and answers, for each recording, the songs it is registered to, the writers registered on each
song, and the Songlinks person behind each writer.
// Continuing from the code above, where `songlinks` was created. Also import java.util.HashMap and java.util.Map.
List<String> isrcs = List.of("USMC19959123", "US-SM1-95-00008", "not-an-isrc");
SongwritersResponse answer = songlinks.songwriters(isrcs);
// One answer per distinct ISRC. Match them to your list by ISRC, not by position.
Map<String, RecordingAnswer> byIsrc = new HashMap<>();
answer.recordings().forEach(r -> byIsrc.put(r.isrc(), r));
for (String isrc : isrcs) {
RecordingAnswer r = byIsrc.get(Songlinks.normalizeIsrc(isrc)); // "US-SM1-95-00008" -> "USSM19500008"
System.out.println(isrc + ": " + r.status());
for (RecordingWork work : r.works()) {
for (RecordingWriter w : work.writers()) {
System.out.println(" " + work.title() + " / " + w.name() + " / " + w.persistentId());
}
}
}
// USMC19959123: registered
// ALL THE SMALL THINGS / MARK HOPPUS / SLP-ZYM7-GD3E-S
// ALL THE SMALL THINGS / TRAVIS L BARKER / SLP-MG23-XM9B-Y
// ALL THE SMALL THINGS / THOMAS DELONGE / SLP-MEA6-3QQJ-C
// US-SM1-95-00008: registered
// STRANGER IN MOSCOW / MICHAEL JOE JACKSON / null
// not-an-isrc: invalidWhat each status() means:
registered: the recording is listed on at least one registered song.works()holds those songs, each with its title, ISWC (null when the song has none) and writers.unregistered: no registered song lists this recording. This is an ordinary answer, not an error: it says what the registration data holds today, andworks()is empty.invalid: the entry is not an ISRC (see the words used here for the shape). It is echoed back as you sent it, and the rest of the list is still answered.works()is empty.
A writer's person ID can be null, as Michael Jackson's is above, and that is common: nearly half of the
registered recordings have at least one such writer. It means the writer's registration record has not been grouped
into a stored Songlinks person yet. Records registered without an IPI name number are often in that state, but not
only those: the record above has one. The writer's name(), role() and ipiNameNumber() (when there is one) are still there; to see who that name could be,
pass it to lookup(). personUrl() is null whenever persistentId() is.
Good to know:
- Case doesn't matter, and spaces and dashes inside an ISRC are ignored:
US-MC1-99-59123,usmc1 99 59123andUSMC19959123are the same ISRC, and come back once, written asUSMC19959123.Songlinks.normalizeIsrc()writes your input the same way, so you can find its answer. - Only the plain hyphen (
-) counts as a dash. An ISRC typed with an en dash (–), as some word processors substitute, comes backinvalid. - The SDK refuses an empty list, more than
Songlinks.SONGWRITERS_MAX(50) entries, and an entry holding a comma, semicolon or line break, before calling the API. For a longer list, call it once per batch of 50, asexamples/WhoWrote.javadoes. - One call counts as one request against the rate limit, however many ISRCs it holds.
- Each work's
writers()list holds songwriters only: publishers and administrators are left out, and so are ownership shares.
Never called an API from code before? This takes about five minutes.
Before you start, you need two things:
- A terminal, where you type commands. On a Mac, open Terminal (in Applications, then Utilities). On Windows, open PowerShell from the Start menu. Type each command below on its own line and press Enter after it.
- A plain-text editor for code, such as the free Visual Studio Code. A word processor (Word, or TextEdit in its usual mode) adds formatting that stops code from running.
-
Install Java 17 or later, for example the free Temurin build, and Git from git-scm.com (on a Mac, the first
gitcommand offers to install it: say yes). Check they worked by typingjava -versionandgit --version. -
Get the SDK and build it. The first build downloads Maven (the tool Java projects use to build and to fetch libraries), so it takes a minute or two:
git clone https://github.com/Alexkane/songlinks-java cd songlinks-java ./mvnw -q install -DskipTestsOn Windows, in PowerShell, the last line is
.\mvnw.cmd -q install -DskipTests. It prints nothing when it works. On Java 24 or later you may first seeWARNINGlines about a "restricted method" orsun.misc.Unsafe: they come from Maven itself and are harmless. -
Run the first example, from the same folder, with any songwriter's full name:
scripts/run-example.sh ResolveName "mark hoppus"On Windows, in PowerShell, run these two lines instead (the first one only once):
.\mvnw.cmd -q dependency:build-classpath "-Dmdep.outputFile=target/classpath.txt" java -cp "target/classes;$(Get-Content target/classpath.txt)" examples/ResolveName.java "mark hoppus"
The first run builds once more and may print the same warnings. You should see
SLP-ZYM7-GD3E-S MARK HOPPUS 302 works, then the registrations behind that person and what joined them (modelmeans Songlinks judged two registrations to be the same person).Keep the quotes around the name: without them, only the first word is sent.
What you might see instead:
- Several lines. The name could belong to more than one person, so each possible person gets a line.
(no person ID yet). Songlinks found the registrations but hasn't given that person an ID yet. That is normal, and the rest of the line is still the answer.No answer: single-token. The name is one word, such asmadonna, which matches too many people. Use the full name.No answer: no-match. Nothing registered is close to that name. Check the spelling.
-
Change it. The program is
examples/ResolveName.java, about 40 lines. Edit it in your editor and run the same command again: changes to the examples need no separate build step.
To use the SDK in your own Maven project, add the dependency shown at the top of this page to its pom.xml; step 2
has already put the library where Maven will find it. (Gradle needs mavenLocal() in its repositories to look
there.) From here, the examples do real jobs, Reading an answer explains what comes
back, and the words used here are explained just below.
Music-publishing words, which you'll meet in the answers:
- Songwriter registration: when a song is written, its publisher registers it so royalties reach the people who wrote it. Each registration names the writers, as typed by whoever filed it. One songwriter can therefore appear under several spellings, and two different songwriters can share a name.
- Record: one of those registered writer entries: a name as it was typed, with its IPI name number if it has one, and the songs (works) registered under it.
- IPI name number: an 11-digit number that identifies a songwriter or publisher to collecting societies, such as
00338980326. Many records have none, and one person can have several. More about IPI name numbers. - Work: a song as a composition (the melody and lyrics), as opposed to a particular recording of it.
- ISWC: the international code for a work, such as
T0719661581. Not every registered work has one. - ISRC: the international code for one recording, such as
USMC19959123(Blink-182's recording of "All the Small Things"). An ISWC identifies the song as written; an ISRC identifies one recording of it, so one song with a studio version, a live version and a cover has one ISWC and three ISRCs. An ISRC is 12 characters: two letters (a country code), three letters or digits (who issued it), then seven digits (the year and a number). It is often written with dashes, as inUS-MC1-99-59123. - Song code: the registration data's own code for a work, such as
A12144. Every registered work has one, so it is the way to tell works apart when one has no ISWC. - Person and person ID: Songlinks groups the records it judges to be one human into a person and gives them a
person ID, such as
SLP-ZYM7-GD3E-S. Every person has a page, such as songlinks.io/person/SLP-ZYM7-GD3E-S. - Candidate: what a lookup by name returns: the records that name reaches, grouped into people for that one question. A candidate usually matches one person ID, but it is worked out fresh each time.
- Evidence: why two records were grouped: the same IPI name number, or a judgement that they are the same person (for example, the same co-writers and publishers).
- Rejected: a lookup that gave no candidates, with a reason, such as a one-word name (
madonna) that matches too many people to group. It is an answer, not an error.
Programming words:
- API: a service that programs talk to over the internet. The Songlinks API answers questions such as "who is this songwriter?".
- SDK: this library. It does the talking to the API for you, so your code calls a function instead of building web requests by hand.
- Rate limit: the most requests the API accepts from one internet address in a stretch of time: about 100 every 5 minutes, or 20 from VPNs and cloud servers. Over it, the SDK tells you how long to wait.
- Maven and dependency: Maven is the tool Java projects use to build and to fetch libraries; a dependency is a library your project uses, such as this one, listed in its
pom.xml.
The API answers one name at a time, which suits an app. If you administer a catalogue, send us the writers instead. For each one we send back every registration we can group to them, across spellings, stage names and more than one IPI name number, with the evidence behind each join and the records we deliberately left out. See bulk songwriter lookup, or write to alex@songlinks.to.
Runnable programs for the jobs people use Songlinks for, each under 40 lines. Copy one and change it:
| Program | What it does |
|---|---|
ResolveName.java |
Every person a name could be, with their registrations and what joined them. |
WhichNamesake.java |
Two writers share a name: which one wrote this song? |
CheckRoster.java |
A list of IPI name numbers in, CSV out: who holds each and their works. |
ExportCatalogue.java |
Every work of a person, as CSV. |
WhoWrote.java |
ISRCs in: the songs each recording is registered to, and their writers. |
From a checkout, scripts/run-example.sh builds the SDK once and runs any of them:
scripts/run-example.sh CheckRoster roster.txt > checked.csvThe examples are compiled in CI against the SDK, so one that misreads a field fails the build.
| Method | API | What it answers |
|---|---|---|
lookup(name), lookup(name, LookupOptions) |
GET /v1/lookup |
The people a songwriter's name could be. A song the writer wrote ranks the candidates by it. |
person(id), person(id, offset, limit) |
GET /v1/person/{id} |
One person: their registrations, names, IPI name numbers and a page of works. |
personWorks(id) |
GET /v1/person/{id} |
Every work of a person, as a lazy Stream. |
writers(ipiNameNumbers) |
GET /v1/writers |
Up to 50 IPI name numbers checked at once: the stored person behind each. |
songwriters(isrcs) |
GET /v1/songwriters |
Up to 50 recordings by ISRC: the songs each is registered to, their writers and the person behind each. |
version() |
GET /v1/version |
What is serving: the release, the model and the date of the registration data. |
A person and a candidate are different things: see the words used here.
- A candidate can count more works than its person. "mark hoppus" reaches two registrations, 302 works between
them; the person
SLP-ZYM7-GD3E-Sholds one of them as theirs (294 works) and lists the other inpossible().person()is the authority on what an ID holds. - Match a writers check by number, not position. Repeats and two forms of one number come back as one entry, as
11 digits.
Songlinks.normalizeIpi()turns your input into the same form. Itsworks()andname()are null unlessstatus()is"person". - Match a songwriters answer by ISRC, not position. Repeats and two forms of one ISRC come back as one entry,
12 characters in upper case.
Songlinks.normalizeIsrc()turns your input into the same form. A writer'spersistentId()is often null: see songwriters by ISRC. - A lookup candidate is not a stored person. It is the lookup's own grouping, made for the name you asked.
persistentId()is the stored person when there is exactly one. A candidate whose records belong to more than one stored person has nopersistentId()and lists them instoredPersons(). Read each record's ownpersistentId(). - Null is ordinary. Many records have no person ID yet. Lists are never null; an absent list is empty.
- Person IDs can be prerelease. A prerelease ID may stop resolving, or point at a different person, without notice.
Check
prerelease()before you store one.
Every non-success answer throws an unchecked SonglinksException with the HTTP status(), the API's code() (its
error field, the one to branch on), a message for people, and the answer's body() as JSON.
// Continuing from the code above, where `songlinks` was created.
String personId = "SLP-0000-0000-0"; // an ID no one holds, to see the not-found branch
try {
songlinks.person(personId);
} catch (PersonWithdrawnException e) {
System.out.println("withdrawn " + e.withdrawn().withdrawnAt()); // a 410, and a valid answer to keep
} catch (RateLimitedException e) {
System.out.println("retry in " + e.retryAfter().map(d -> d.toSeconds() + " s").orElse("a few minutes"));
} catch (SonglinksException e) {
if ("not_found".equals(e.code())) { // a 404, such as for SLP-0000-0000-0
System.out.println("no person with that ID");
} else {
System.out.println(e.status() + " " + e.code() + ": " + e.getMessage());
}
}- Retried automatically:
429and503answers and network failures, up tomaxRetriestimes (default 2). When the API asks for a wait (in aRetry-Afterheader or the body'sretry_after_seconds) of a minute or less, the client waits that long; otherwise it backs off from half a second. - Thrown at once: a requested wait longer than a minute, for you to schedule (
retryAfter()on a 429). - Never retried: any other error.
- Merged person IDs are followed to the person they now point at, and
redirectedFrom()names the ID you asked for.
Songlinks songlinks = Songlinks.builder()
.baseUrl("https://songlinks.io") // the default
.timeout(Duration.ofSeconds(30)) // per request
.maxRetries(2) // 0 turns retrying off
.userAgent("my-app/1.2") // appended to songlinks-java/<version>
.httpClient(myHttpClient) // your own; give it Redirect.NORMAL
.build();The API is free and open. It allows about 100 requests per 5 minutes from one address, across every endpoint, and 20
from VPNs, proxies and hosting providers, which includes most cloud servers. Please send a userAgent that names your
app, keep to one request at a time per user action, and cache answers you reuse. Over the limit, the SDK throws a
RateLimitedException whose retryAfter() is the most you need to wait; for a whole roster, see
bulk songwriter lookup.
- API reference, with every endpoint runnable from the page.
- How Songlinks decides: what a person ID is, and how registrations are grouped.
- MCP server: the same answers inside Claude or any MCP client.
- TypeScript SDK:
npm install songlinks. - Something wrong in an answer? Open an issue, or write to alex@songlinks.to.
./mvnw verify # compile and test
SONGLINKS_LIVE=1 ./mvnw test # also against the live API
curl -sf https://songlinks.io/v1/openapi.json -o src/test/resources/openapi.json
python3 scripts/gen_models.py # regenerate the records from the API's documentModelTest fails when a record and the API's document disagree, so a new API field is a regeneration, not a surprise.
MIT, © 2026 Kane Technology Group, LLC. The API's answers are subject to the Songlinks terms.