On the wire · 05:07:28 UTC Live 585 · Prematch 14,345 · Markets 57,120 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Tutorial · Kotlin · WebSocket

Pinnacle odds WebSocket in Kotlin (OkHttp)

Updated · PinnFeed · 2 min read

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.

The WebSocket plan: Builder, €99/mo.

Real-time Pinnacle odds pushed over one WebSocket, all 13 sports, every market, no-vig fair odds and closing lines. 3 days free, cancel in one click.

Form K-1 · Free API key

Get your free API key.

One field. No password, no card: enter your email, click the link we send, and your key is on screen.

By continuing you agree to the Terms and Privacy Policy.

One free key per email. Link valid for 20 minutes.