Getting Started as a Client
This page is for a developer who has never seen Synara and wants to talk to a server from their own app. It lists the four surfaces a server exposes, helps you pick one, shows how to point at a server you can experiment with, explains the handful of conventions that run through every response, and walks through a first login-and-search in both the Kotlin RPC SDK and plain curl. The exact method and route reference lives in the generated RPC_SERVICES.md, MODELS.md and REST_API.md; this page only gets you to your first successful call.
What a server exposes
A single Synara process serves four independent API surfaces, all on the same host and port (Routing.kt):
| Surface | Endpoint | Protocol | Read more |
|---|---|---|---|
| Kotlin RPC | /rpc, /rpc/auth, /rpc/services |
kotlinx-rpc over WebSockets, CBOR frames | CLIENT_KOTLIN_RPC.md |
| REST | one route per service method, e.g. /song/search |
HTTP, JSON bodies, SSE for streams | CLIENT_REST.md, REST_API.md |
| Subsonic | /rest/... |
Subsonic / OpenSubsonic XML or JSON | below |
| MCP | POST /mcp |
JSON-RPC 2.0, streamable HTTP | MCP.md |
Two more things are not surfaces of their own but shape what you build:
- Server-driven UI — the server (and its plugins) can describe whole screens that your client renders with native widgets. Available through
IUiServiceon both RPC and REST. See SERVER_DRIVEN_UI.md. - OpenAPI — the REST surface documents itself at
/api.json, with Swagger UI at/swagger.
Which surface to pick
| Kotlin RPC | REST | Subsonic | MCP | |
|---|---|---|---|---|
| Language | Kotlin / Kotlin Multiplatform only | any | any | any |
| Coverage | every service, every method | every service, generated from the same interfaces | a fixed classic media API | listen history and statistics, read-only |
| Typed models | yes, the same data classes the server uses |
you write or generate them | Subsonic's own shapes | JSON-RPC tool results |
| Streams | Flow<T> |
Server-Sent Events | — | — |
| Auth | JWT bearer on the WebSocket upgrade | JWT bearer header or synara-auth cookie |
Subsonic credentials or an API key with the subsonic scope |
API key with the mcp scope |
| Best for | first-party apps on Android, iOS, macOS, desktop | web front-ends, scripts, integrations in other languages | reusing an existing Subsonic player | AI assistants |
The rule of thumb: if you are writing Kotlin, use the RPC SDK — it is the surface the first-party clients use, and you get the models, the reconnect logic and the token refresh for free. Everything else goes over REST, unless an existing Subsonic player is the whole point.
The Subsonic API is mounted at /rest by a built-in plugin (SubsonicRoutes.kt). It accepts the classic u+t+s token, u+p password (optionally enc:-hex), or an apiKey parameter holding the subsonic scope (SubsonicAuth.kt); every route also answers to POST and to a .view suffix.
Prerequisites
- A running Synara server, or the mock server (below).
- For REST, Subsonic or MCP: any HTTP client.
- For Kotlin RPC: a Kotlin Multiplatform build, and the
common-rpcmodule added as a git submodule. Setup is in CLIENT_KOTLIN_RPC.md. - An account on that server. There is no self-registration: the first account is seeded on startup from the
client.id/client.secretconfiguration properties — theCLIENT_IDandCLIENT_SECRETenvironment variables, documented in ENVIRONMENT_VARIABLES.md as the initial admin username and password — and that user is an admin. The row is inserted withinsertIgnore(DatabaseManager.kt), so it is created once on the first run and later changes to the variables do not overwrite it. Every further account is created by that admin throughPOST /registerorIUserService.createUser.
Pointing at a server
The quickest way to see real shapes without owning a server is the mock server, which registers every RPC service with generated dummy data and mounts the same REST routes:
docker run -p 8081:8081 ghcr.io/dertyp7214/synara-mock:latest-dev
It listens on port 8081, serves /rpc, /rpc/auth and /rpc/services, and accepts any Authorization header that starts with Bearer without validating the token — so you can exercise everything except real authentication. Details and limits are in MOCK_SERVER.md.
Against a real server, the first call worth making is the handshake, which tells you the API version the server speaks, whether it supports server-driven UI and whether TLS is available:
curl http://localhost:8080/handshake/handshake
An observed response, from a server built at the same point in time as this page:
{"secure":false,"sslSupported":false,"apiVersion":10,"uiSchemaVersion":2}
Running a server of your own is covered in DEVELOPMENT.md and the project README.
Five conventions
These hold across both RPC and REST and are worth internalising before you read any service reference.
- Ids are UUIDs. In JSON they are the usual hyphenated strings; over CBOR the same value is encoded as a 16-byte string, because the RPC transport swaps
UUIDSerializerforUUIDByteSerializer(Serializers.kt). If you write your own CBOR decoder, expect bytes, not text. - Timestamps come in two shapes. A
PlatformDatefield (AuthenticationResponse.expiresAt,UserSong.userSongCreatedAt) is epoch milliseconds as aLong; aPlatformInstantfield is an ISO-8601 string. Plain durations, such asSong.durationandaudioStartMs, are milliseconds. The model reference in MODELS.md names the type per field. - Lists are paginated. Any method with
pageandpageSizereturnsPaginatedResponse:data,page(0-based),total,pageSizeandhasNextPage. Page through untilhasNextPageis false rather than computing page counts yourself. explicitis a required filter on songs and an optional preference on albums.- Songs: queries (
allSongs,likedSongs,rankedSearch,byColor, …) take anexplicit: Booleanwith no default: passfalseto hide explicit tracks,trueto include them. Whatever your app's setting is, it belongs in that parameter. - Albums: on queries (
allAlbums,byArtist,byVersionGroup) the parameter is optional and means something else: when an album exists in an explicit and a clean edition,truemakes the explicit edition the main entry andfalsethe clean one. Left out, the explicit edition is preferred. - Album
rankedSearchandbyColorhave noexplicitat all.
- Songs: queries (
- Covers are ids plus a blur hash. Songs and albums carry
coverId, artists and playlists carryimageId, and a user carriesprofileImageId, each next to ablurHash— never an image URL. Render the blur hash immediately, then fetch the bytes from the public routeGET /image/imageData/{id}?size=<px>(size=0or omitted means original), or over RPC viaIImageService.getImageData. See RPC_SERVICES.md.
Headers to always send
| Header | Value | Why |
|---|---|---|
X-Api-Version |
the ApiVersion.CURRENT you built against |
Without it the server treats you as the legacy version and strips newer fields. See API_VERSIONING.md and API_CONSTANTS.md#api-version for the actual values. |
X-Ui-Schema-Version |
UiSchemaVersion.CURRENT |
Only needed if you render server-driven UI; omitting it turns every component into Fallback. See API_CONSTANTS.md#ui-schema-version. |
Accept-Language |
e.g. de-AT, de;q=0.9, en;q=0.5 |
Server-driven UI text arrives translated; default is en. |
Authorization |
Bearer <token> |
Everything except the handshake, the stats service, the auth service and the public image routes (GET /image/imageData/{id}, GET /animatedImage/imageData/{id} and GET /release/releaseImage/{releaseId}). See AUTHENTICATION.md. |
User-Agent |
something identifying your app | It is stored as the session's name, so it is what the user sees in their session list. |
On the RPC surface, headers are read at WebSocket upgrade time — changing the app language means reconnecting.
Hello Synara — Kotlin RPC
createRpcHttpClient builds a Ktor client with WebSockets, the kRPC CBOR codec, the X-Api-Version default header and a Synara/<version> (<platform>) user agent. BaseRpcServiceManager then owns the connection; you only supply storage for the URL and the tokens. A complete minimal subclass is in CLIENT_KOTLIN_RPC.md — assume SimpleRpcManager from there:
import dev.dertyp.rpc.createRpcHttpClient
import dev.dertyp.services.ISongService
import dev.dertyp.services.IUserService
suspend fun helloSynara() {
val manager = SimpleRpcManager(
client = createRpcHttpClient("1.0.0"),
host = "localhost",
port = 8080,
ssl = false,
)
manager.login("alice", "hunter2")
val me = manager.getService<IUserService>().me()
println("${me.displayName ?: me.username} — admin=${me.isAdmin}")
val hits = manager.getService<ISongService>().rankedSearch(
page = 0,
pageSize = 20,
query = "boards of canada",
explicit = true,
)
hits.data.forEach { println("${it.title} — ${it.artists.joinToString { a -> a.name }}") }
println("page ${hits.page} of ${hits.total}, more=${hits.hasNextPage}")
}
login is a two-liner on the sample subclass: it calls getAuthService().authenticate(username, password) — which connects to the public /rpc/auth endpoint — and hands the AuthenticationResponse to updateAuth, the hook where your subclass persists token, refreshToken and expiresAt. getService<T>() then talks to the authenticated /rpc/services endpoint with the stored token attached, and the manager refreshes the token on its own whenever isTokenExpired() says so.
Hello Synara — REST
TOKEN=$(curl -s -X POST http://localhost:8080/authenticate \
-H 'Content-Type: application/json' \
-H 'X-Api-Version: 6' \
-d '{"username":"alice","password":"hunter2"}' | jq -r .token)
curl -s http://localhost:8080/user/me \
-H "Authorization: Bearer $TOKEN" -H 'X-Api-Version: 6'
curl -s -G http://localhost:8080/song/search \
--data-urlencode 'query=boards of canada' \
--data-urlencode 'explicit=true' \
-H "Authorization: Bearer $TOKEN" -H 'X-Api-Version: 6'
The samples send X-Api-Version: 6, the convention for every sample in these docs. Send the ApiVersion.CURRENT you built against, as the note under Headers in CLIENT_REST.md explains.
POST /authenticate is the hand-written login route and takes a JSON body; it answers with token, refreshToken and expiresAt (epoch millis). GET /user/me is IUserService.me and GET /song/search is ISongService.rankedSearch — the route naming, the query-vs-path parameter rules and the SSE streams are described in CLIENT_REST.md, and every route is listed in REST_API.md.
Where next
| You want to… | Read |
|---|---|
| Build a Kotlin or KMP client | CLIENT_KOTLIN_RPC.md |
| Call the API from another language | CLIENT_REST.md, REST_API.md |
| Log in, refresh, manage sessions, use API keys | AUTHENTICATION.md |
Know what X-Api-Version changes |
API_VERSIONING.md |
| Play or download audio | STREAMING_AND_PLAYBACK.md |
| Render screens the server describes | SERVER_DRIVEN_UI.md |
| Store your client's settings on the server | CLIENT_SETTINGS.md |
| Look up a method or a model | RPC_SERVICES.md, MODELS.md |
| Understand how the server is put together | ARCHITECTURE.md |
| Run a server yourself | DEVELOPMENT.md, ../README.md |
| Find every other document | README.md, the documentation index |