How to Receive Webhooks from Sonarr and Radarr (Self-Hosted, Step by Step)
Receive Sonarr, Radarr, and other *arr Connect webhook notifications on your own server with HTTP Basic authentication, Docker setup, and RabbitMQ routing, verified end to end against a real Sonarr v4.
By the end of this guide you will have a self-hosted endpoint that receives Sonarr webhook notifications, authenticates every delivery with HTTP Basic credentials, and routes the events to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real Sonarr v4 instance and the real Core Webhook Module Docker image: Sonarr's own test mechanism sent the deliveries quoted here. The same setup works for the whole *arr family - Radarr, Lidarr, Prowlarr, Readarr - because they share the identical Webhook connection type.
How Sonarr webhooks work
Sonarr notifies external systems through Connections (also called Connect notifications). The Webhook connection POSTs a JSON payload to your URL when the events you tick happen: On Grab, On Import, On Upgrade, On Rename, On Series Delete, On Health Issue, and more. Every payload carries an eventType field naming the event, plus the relevant objects. The test event, captured from a real delivery, looks like this:
{
"series": {
"id": 1,
"title": "Test Title",
"path": "C:\\testpath",
"tvdbId": 1234,
"type": "standard",
"tags": ["test-tag"]
},
"episodes": [
{"id": 123, "episodeNumber": 1, "seasonNumber": 1, "title": "Test title"}
],
"eventType": "Test",
"instanceName": "Sonarr",
"applicationUrl": ""
}
Real events replace eventType with Grab, Download, HealthIssue, and so on, and fill in the actual series, episode, release, or health data. The request arrives with a User-Agent like Sonarr/4.0.19.2979 and Content-Type: application/json.
Authentication: Sonarr does not sign webhook payloads. The Webhook connection form has these fields (taken from Sonarr's own API schema): Webhook URL, Method (POST or PUT), Username, Password, and Headers. Username and Password become standard HTTP Basic authentication on every delivery, which the gateway verifies with a constant-time comparison. Newer versions can also attach custom headers via the Headers key-value list if you prefer token-style auth.
Prerequisites
- Docker on any machine (your laptop is fine for the test run)
- A public HTTPS URL for production, unless Sonarr and your receiver share a network. Two ways to get one:
- HTTPS for webhooks with nginx and Let's Encrypt if you have a public server
- Receiving webhooks through Cloudflare Tunnel if you do not want to open inbound ports
Step 1: Configure the receiver
Create a working directory with two files.
webhooks.json:
{
"sonarr_events": {
"data_type": "json",
"module": "log",
"basic_auth": {
"username": "{$SONARR_WEBHOOK_USER}",
"password": "{$SONARR_WEBHOOK_PASS}"
}
}
}
connections.json:
{}
What each parameter does:
sonarr_eventsis the webhook ID; it becomes the URL path (/webhook/sonarr_events).module: logprints each authenticated event to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.basic_authvalidates standard HTTP Basic credentials (RFC 7617) with constant-time comparison.- The
{$...}placeholders pull both credentials from environment variables, so they never live in a config file.
Generate a strong password (or use our webhook secret generator):
openssl rand -hex 24
Step 2: Run it
docker run -d --name webhook-gateway -p 8000:8000 \
-v "$PWD/webhooks.json:/app/webhooks.json:ro" \
-v "$PWD/connections.json:/app/connections.json:ro" \
-e SONARR_WEBHOOK_USER="gateway" \
-e SONARR_WEBHOOK_PASS="your-generated-password" \
spiderhash/webhook:latest
Confirm it is up:
docker logs webhook-gateway
# ... INFO: Application startup complete.
The receiver now answers on http://localhost:8000/webhook/sonarr_events, with auto-generated API docs at http://localhost:8000/docs.
Step 3: Test it locally
Simulate exactly what Sonarr sends. Save the test payload from the top of this article as test_event.json, then:
curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:8000/webhook/sonarr_events \
-u gateway:your-generated-password \
-H "Content-Type: application/json" \
--data-binary @test_event.json
Expected result:
{"message":"200 OK"}
HTTP 200
Now prove the authentication works. Wrong credentials:
{"detail":"Invalid credentials"}
HTTP 401
And a request with no credentials at all:
{"detail":"Missing Authorization header"}
HTTP 401
Those are the exact responses the gateway returns. docker logs webhook-gateway shows the accepted payload.
Step 4: Create the connection in Sonarr
In Sonarr: Settings > Connect > + > Webhook:
- Name: something like
webhook-gateway - Notification Triggers: tick the events you want (On Grab and On Import are the usual starting point)
- Webhook URL:
https://your-domain/webhook/sonarr_events - Method:
POST - Username:
gateway(the value ofSONARR_WEBHOOK_USER) - Password: the value of
SONARR_WEBHOOK_PASS
Press Test: Sonarr sends the eventType: "Test" payload immediately, and the button goes green when your endpoint answers 200. Save. In Radarr, Lidarr, Prowlarr, and Readarr the form is the same, with series/episodes replaced by that app's objects (movie in Radarr, and so on); give each app its own webhook ID on the gateway to keep the streams separate.
Troubleshooting
401 {"detail":"Invalid credentials"}: the Username/Password in the connection and the gateway's env vars differ. They must be byte-identical.401 {"detail":"Missing Authorization header"}: the Username and Password fields in Sonarr are empty. Fill both; Sonarr only sends the Authorization header when credentials are configured.- Test button fails instantly: Sonarr must be able to reach the URL. From a Docker-based Sonarr,
localhostpoints at the Sonarr container itself, not the host; use the host's address or a shared Docker network. - Events fire but with unexpected shapes: every event type carries different objects. Branch on
eventTypein your consumer, and treatTestas a no-op. Health issue events, for example, carry ahealthCheckobject instead ofseries.
Going to production
Printing to logs is not a pipeline. Swap the module for a real destination; here is the RabbitMQ variant, verified end to end (a real Sonarr test delivery landed as a message in the sonarr_events queue):
webhooks.json:
{
"sonarr_events": {
"data_type": "json",
"module": "rabbitmq",
"connection": "rabbitmq_local",
"module-config": {
"queue_name": "sonarr_events"
},
"basic_auth": {
"username": "{$SONARR_WEBHOOK_USER}",
"password": "{$SONARR_WEBHOOK_PASS}"
}
}
}
connections.json:
{
"rabbitmq_local": {
"type": "rabbitmq",
"host": "{$RABBITMQ_HOST:localhost}",
"port": "{$RABBITMQ_PORT:5672}",
"user": "{$RABBITMQ_USER:guest}",
"pass": "{$RABBITMQ_PASS:guest}"
}
}
The same pattern works for 17 other destinations: archive your media pipeline history to S3, publish to Kafka, or fan out to several at once with webhook chaining. Built-in rate limiting and retry handling cover the operational edges; a season pack import that fires a burst of events is exactly when acknowledging fast and queueing durably pays off. The full authentication reference, including Basic auth, header auth, and 10 other methods, is in the docs.