Pinnacle odds WebSocket in Kotlin (OkHttp)
Answer: with OkHttp, build a Request for wss://pinnfeed.io/v1/stream with the x-api-key header, call newWebSocket, send a JSON subscribe in onOpen, and reply {"type":"pong"} to each server ping (every 20 s). Frames are welcome, subscribed, snapshot parts, then delta. Builder (€99/mo) or Pro.
Gradle#
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.json:json:20240303")
}
Client#
import okhttp3.*
import org.json.JSONObject
import java.util.concurrent.TimeUnit
fun main() {
val key = System.getenv("PINNFEED_KEY") ?: error("set PINNFEED_KEY") // ps_live_...
val client = OkHttpClient.Builder().readTimeout(0, TimeUnit.MILLISECONDS).build()
fun connect(delayMs: Long = 0) {
Thread.sleep(delayMs)
val req = Request.Builder().url("wss://pinnfeed.io/v1/stream").header("x-api-key", key).build()
client.newWebSocket(req, object : WebSocketListener() {
override fun onOpen(ws: WebSocket, response: Response) {
ws.send("""{"type":"subscribe","sports":["soccer"],"markets":["moneyline"],"main":true}""")
}
override fun onMessage(ws: WebSocket, text: String) {
val msg = JSONObject(text)
when (msg.getString("type")) {
"ping" -> ws.send("""{"type":"pong"}""")
"delta" -> {
val changes = msg.getJSONArray("changes")
for (i in 0 until changes.length()) {
val ch = changes.getJSONObject(i)
if (ch.getString("kind") == "market" && ch.getString("op") == "upsert") {
val m = ch.getJSONObject("market")
if (m.optDouble("maxStake", 0.0) >= 100) println("${m.getLong("eventId")} ${m.getJSONArray("outcomes")}")
}
}
}
"error" -> println("error ${msg.optString("code")}: ${msg.optString("message")}")
}
}
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
println("closed $code $reason")
if (code != 4001) connect(2000) // 4001 = key revoked
}
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
println("failure ${response?.code} ${t.message}")
connect(5000)
}
})
}
connect()
}
readTimeout(0) stops OkHttp from closing an idle socket; PinnFeed's own heartbeat handles liveness (closed after 60 s without a pong). The maxStake >= 100 check skips thin markets. Use exponential backoff in production.
Close codes#
4002 no_subscription (no subscribe in 30 s) · 4008 slow_consumer (buffer over 16 MB: move parsing off the callback thread) · 4001 key_revoked. A Free/Hobby key fails the upgrade with 403 plan_lacks_ws; onFailure receives that response.
Plans#
Builder (€99/mo, 3 connections) or Pro (10). 3-day trial; a card is required and it is charged after day 3 unless you cancel. Java's built-in client: Java.
Educational content for adults (18+).
Questions people ask#
Can I use this in an Android app?#
Technically yes, but the key would ship inside the app. Keep the stream on your server and send your app only what it needs.
Ktor instead of OkHttp?#
Ktor's WebSocket client works the same: set the x-api-key header in the request builder.
Does PinnFeed compress frames?#
Only when the client negotiates permessage-deflate; then frames over 1 KB are compressed. Check compression in the welcome frame to see whether it is on.