---
title: "Webhook callbacks vs longpoll for stream results"
description: "Choose how to receive AudD stream recognition events: webhook callbacks when you can host a public HTTPS endpoint, longpoll when you can't."
slug: "/resources/concepts/callback-vs-longpoll"
section: "concepts"
keywords: [audd, streams, callbacks, webhook, longpoll, real-time music recognition]
---

# Webhook callbacks vs longpoll for stream results

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 with
  `setCallbackUrl`. 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 returns `200 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.

```python
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 a `timestamp` and (by default) `play_length`.
- **Notifications** — stream health. Code `0` means everything's fine, `650`
  means AudD can't connect to the stream, and `651` means it's receiving white
  noise with no music. Use these to alert on a dead or misconfigured stream.

```json
{
  "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.

```text
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.

```python
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 responds `200 OK` and discards the payload. Set it once with
> `setCallbackUrl` (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:

```python
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 OK` marks the delivery failed and queues it. Return `200` immediately,
  then process asynchronously, so transient slowness doesn't back up the queue.
- **Ignoring `since_time` in longpoll.** Without carrying the response
  `timestamp` into 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_category` with browsers and apps, never the `api_token`. The
  category is derivable as the first nine chars of
  `MD5(MD5("your api_token") + "radio_id")`.
- **Expecting `play_length` with `callbacks="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**

- [Standard, enterprise, or streams: how to choose](/resources/concepts/standard-vs-enterprise-vs-streams)
- [Build a now-playing widget](/resources/recipes/now-playing-widget)
- [Monitor radio airplay](/resources/recipes/radio-airplay-monitor)
- [Streams docs](https://docs.audd.io/streams)
- [API reference](https://docs.audd.io)