Webhook callbacks vs longpoll for stream results
Choose how to receive AudD stream recognition events: webhook callbacks when you can host a public HTTPS endpoint, longpoll when you can't.
Once you’ve registered a stream with AudD, recognition results have to reach your code somehow. There are two delivery mechanisms — webhook callbacks and longpoll — and the right one depends almost entirely on whether you can host a public HTTPS endpoint. This page explains both, their delivery guarantees, and how to choose.
TL;DR
- Can you host a public HTTPS endpoint that returns
200 OK? Use callbacks. AudD POSTs each result to a URL you set once withsetCallbackUrl. It’s the recommended path: lowest latency, no polling, and a retry queue if your server is briefly down. - Can’t expose a public endpoint — local dev, a browser widget, a mobile
client, no static IP? Use longpoll. Your client pulls events with
GET https://api.audd.io/longpoll/. - Key fact, easy to miss: longpoll still requires a callback URL to be
configured on the account. If you have no real receiver, set it to the no-op
https://audd.tech/empty/, which simply returns200 OK. Without a callback URL set, longpoll returns nothing.
The two are not mutually exclusive — you can run both at once (a backend on callbacks, a widget on longpoll for the same stream).
Why this matters
Stream recognition is asynchronous: songs are identified as they play, so the result for “the song airing right now” arrives seconds or minutes after you added the stream, and again for the next song, and so on indefinitely. There’s no request you can block on to “get the answer.” Something has to carry each event from AudD’s servers to yours as it happens.
Callbacks invert control: AudD calls you. That’s efficient — you do nothing until a result exists — but it requires a publicly reachable HTTPS endpoint that’s up when AudD POSTs. Longpoll keeps control on your side: you ask, and the connection is held open until an event is ready or a timeout elapses. That works from anywhere a client can make an outbound HTTPS request — a browser, a phone, a laptop behind NAT — with no inbound endpoint to host.
Getting this choice wrong is rarely fatal but always annoying: you’ll either build a webhook receiver you can’t expose from a browser, or you’ll poll from a server that could have just received a push. Match the mechanism to where your result-handling code actually runs.
Callbacks: AudD POSTs to you
You register one callback URL per account with setCallbackUrl, and from then
on every recognition result and notification is POSTed to it as JSON. You can
change the URL anytime by calling setCallbackUrl again.
from audd import AudD
audd = AudD("your-api-token") # get your own token at dashboard.audd.io
audd.streams.set_callback_url("https://yourapp.example/audd-callback")
audd.streams.add(url="https://npr-ice.streamguys1.com/live.mp3", radio_id=3249)
Your endpoint receives two kinds of payload:
- Results — the identified song(s) for a stream, keyed by your
radio_id, with atimestampand (by default)play_length. - Notifications — stream health. Code
0means everything’s fine,650means AudD can’t connect to the stream, and651means it’s receiving white noise with no music. Use these to alert on a dead or misconfigured stream.
{
"status": "success",
"result": {
"radio_id": 7,
"timestamp": "2020-04-13 10:31:43",
"play_length": 111,
"results": [
{
"artist": "Alan Walker, A$AP Rocky",
"title": "Live Fast (PUBGM)",
"album": "Live Fast (PUBGM)",
"release_date": "2019-07-25",
"label": "MER Recordings",
"score": 100,
"song_link": "https://lis.tn/LiveFastPUBGM"
}
]
}
}
Your handler must respond 200 OK. Anything else (or an unreachable server)
is treated as a failed delivery and queued for retry — see delivery semantics
below. Callbacks are the recommended mechanism whenever you can host a
receiver.
Longpoll: you pull from AudD
When you can’t host an inbound endpoint, longpoll lets any number of clients fetch new results over plain outbound HTTPS — no server, no static IP.
GET https://api.audd.io/longpoll/?category=92b1cc7f0&timeout=50&since_time=1652123144400
A longpoll request holds the connection open until a new event is ready or
timeout seconds pass, then returns. When a response includes a timestamp,
use it as the since_time for your next request so you don’t miss or repeat
events. The subscription category is the longpoll_category field from the
getStreams response, or the first nine characters of
MD5(MD5("your api_token") + "radio_id"). You can share a category with
client-side software, but never embed your API token there.
audd = AudD("your-api-token")
with audd.streams.longpoll(category="92b1cc7f0", timeout=50) as poll:
for match in poll.matches:
print(match.song.artist, "—", match.song.title)
Longpoll still needs a callback URL configured. Even if you never run a webhook receiver, the account must have a callback URL set or longpoll returns nothing. Point it at the no-op receiver
https://audd.tech/empty/, which responds200 OKand discards the payload. Set it once withsetCallbackUrl(or via the web interface) before you start polling.
Because longpoll runs client-side, it’s what powers browser and mobile
experiences. AudD also ships a ready-made HTML widget driven by longpoll —
https://widget.audd.tech/?ch=-92b1cc7f0&background&history&shadow, using the
category prefixed with - as the ch parameter — so a now-playing display
needs no backend at all.
Delivery semantics: retries, ordering, and missed events
The two mechanisms behave differently when something goes wrong, and that difference is often the deciding factor.
Callbacks have a retry queue. If your server doesn’t return 200 OK, or
AudD can’t reach it, the callbacks are queued. When your server is back online,
the backlog is delivered gradually rather than dropped. There’s a send-side
rate limit: with fewer than 500 streams you won’t receive more than 3 callbacks
per second, with a burst size of 15. So a brief outage costs you a delay, not
the data — provided your endpoint eventually returns 200 OK.
Longpoll is catch-up by cursor. There’s no server-pushed retry; instead you
control continuity with since_time. Carry forward the timestamp from each
response into the next request and you’ll receive everything since that point.
Skip that step — or let too much time pass between requests — and you risk a
gap. The cursor is the mechanism that prevents missed and duplicated events, so
treat persisting and reusing it as mandatory, not optional.
Ordering. Both deliver events as songs are recognized over time. With
callbacks, order follows AudD’s send sequence; with longpoll, order follows the
since_time progression you drive. In both cases, key results by radio_id
(and timestamp) rather than assuming strict global ordering across many
streams.
You can also disable longpoll for your streams by adding
disable-lastsong=true to your callback URL — useful when you’re committed to
callbacks only and don’t want results retained for polling.
The callbacks="before" option
By default, a result is delivered after a song finishes playing, and it
includes play_length (the total time that song was on air). For a now-playing
display that’s too late — you’d announce the song only once it’s over.
Set callbacks="before" when adding the stream to be notified as soon as a
song starts instead:
audd.streams.add(
url="twitch:monstercat",
radio_id=3249,
callbacks="before",
)
The trade-off: with callbacks="before" you won’t receive the played-time
total, because the song hasn’t finished. Choose by use case — before for
live “now playing” UIs, the default (after) for airplay logs and reports where
you want accurate durations. This setting governs when an event is generated;
it applies whether you then receive that event via callbacks or longpoll.
Worked example
Two consumers of the same recognition data, each on the mechanism that fits.
A server backend logging airplay (callbacks). You run a backend with a
public HTTPS endpoint, so callbacks are the clear choice. Call
setCallbackUrl("https://yourapp.example/audd-callback") once, then addStream
for each station with the default callback timing so each result carries
play_length. Your handler writes every result row keyed by radio_id and
returns 200 OK. If the backend restarts, AudD’s retry queue replays the
backlog when it’s reachable again, so a deploy doesn’t lose plays. Wire the
650/651 notifications to an alert so a dead stream surfaces immediately.
A browser now-playing widget (longpoll). A web page can’t host an inbound
webhook, so callbacks are out. First set the account’s callback URL to the
no-op https://audd.tech/empty/ (required for longpoll to return anything),
and add the stream with callbacks="before" so the widget updates the instant
a song starts. Then either embed AudD’s hosted widget with the stream’s
category as ch=-<category>, or have your own front end poll
https://api.audd.io/longpoll/ with the category and carry each response’s
timestamp into the next since_time. No backend, no static IP — and it can
run alongside the callback-based backend on the same stream.
Common mistakes
- Starting longpoll without a callback URL set. Longpoll requires a
configured callback URL to retain results. With none set you’ll poll and get
nothing. Set
https://audd.tech/empty/first. - Returning a non-200 from your callback handler. Anything other than
200 OKmarks the delivery failed and queues it. Return200immediately, then process asynchronously, so transient slowness doesn’t back up the queue. - Ignoring
since_timein longpoll. Without carrying the responsetimestampinto the next request, you’ll re-fetch old events or skip new ones. The cursor is how longpoll stays gapless. - Putting your API token in client-side longpoll code. Share the
longpoll_categorywith browsers and apps, never theapi_token. The category is derivable as the first nine chars ofMD5(MD5("your api_token") + "radio_id"). - Expecting
play_lengthwithcallbacks="before". Start-of-song delivery can’t include a total played time. Use the default (after) timing when you need durations. - Building a webhook receiver for a browser or mobile UI. Those clients can’t accept an inbound POST. Use longpoll (or the hosted widget) for anything that runs on the client side.
Related
Reading this as an AI agent? The raw Markdown is at concepts/callback-vs-longpoll.md, and the full index is /resources/llms.txt.
