JellyNext supports custom webhook integrations for maximum flexibility in handling download requests. This allows you to integrate with any external system that accepts HTTP requests.
Webhook mode enables you to send HTTP requests (GET, POST, PUT, or PATCH) to custom URLs when users trigger downloads from virtual libraries. You can fully customize the request format including:
- Dynamic URLs with placeholder substitution
- Custom HTTP headers with placeholder support
- JSON payloads with customizable templates
- Different configurations for movies vs TV shows
Webhook mode is ideal for:
- Custom download systems not based on Radarr/Sonarr
- External automation triggered by Jellyfin playback events
- Notification services (Discord, Slack, custom endpoints)
- Third-party integrations requiring custom data formats
- Development/testing of custom media management solutions
If you're using Radarr/Sonarr, the Native integration mode is recommended for direct API communication. If you're using Jellyseerr, use the Jellyseerr integration mode instead.
Navigate to: Dashboard → Plugins → JellyNext → Download Integration
- Under Download Integration, select 🔗 Webhooks
- This will reveal the webhook configuration section
Choose the HTTP method for your webhook requests:
- GET: Simple requests with data in URL/headers
- POST: Send data in request body (most common)
- PUT: Update existing resources
- PATCH: Partial updates
Default: POST
Set the URL where movie download requests will be sent.
Example URLs:
https://example.com/api/download/movie?tmdb={tmdbId}&user={jellyfinUserId}
https://api.myserver.com/movies/request
http://localhost:8080/webhook/movie
Use these placeholders in URLs, headers, and payloads for movies:
| Placeholder | Description | Example Value |
|---|---|---|
{tmdbId} |
The Movie Database ID | 550 |
{imdbId} |
IMDb ID | tt0137523 |
{title} |
Movie title | Fight Club |
{year} |
Release year | 1999 |
{jellyfinUserId} |
Jellyfin user ID who triggered download | a1b2c3d4-e5f6-... |
Add custom headers for authentication, content-type, or other requirements.
Common examples:
Authorization: Bearer {your-api-token}
X-Api-Key: {your-api-key}
Content-Type: application/json
X-User-Id: {jellyfinUserId}
Click + Add Header to add new headers. Both header names and values support placeholders.
Configure the JSON payload template for POST/PUT/PATCH requests.
Default payload template:
{
"tmdbId": "{tmdbId}",
"imdbId": "{imdbId}",
"title": "{title}",
"year": "{year}",
"jellyfinUserId": "{jellyfinUserId}"
}Custom examples:
{
"type": "movie",
"media": {
"tmdb": "{tmdbId}",
"imdb": "{imdbId}"
},
"metadata": {
"title": "{title}",
"year": {year}
},
"requestedBy": "{jellyfinUserId}",
"timestamp": "2025-01-14T12:00:00Z"
}Note: Placeholders are replaced with actual values at runtime. Use the clickable placeholder buttons below the payload editor to insert placeholders easily.
Similar to movies, but with additional placeholders for TV-specific data.
Example URLs:
https://example.com/api/download/show?tvdb={tvdbId}&season={seasonNumber}
https://api.myserver.com/shows/request
http://localhost:8080/webhook/show?anime={isAnime}
Use these placeholders in URLs, headers, and payloads for TV shows:
| Placeholder | Description | Example Value |
|---|---|---|
{tvdbId} |
TheTVDB ID | 73739 |
{tmdbId} |
The Movie Database ID | 1396 |
{imdbId} |
IMDb ID | tt0903747 |
{title} |
Show title | Breaking Bad |
{year} |
First air year | 2008 |
{seasonNumber} |
Season number being requested | 2 |
{isAnime} |
Whether show is anime | true or false |
{jellyfinUserId} |
Jellyfin user ID who triggered download | a1b2c3d4-e5f6-... |
Default payload template:
{
"tvdbId": "{tvdbId}",
"tmdbId": "{tmdbId}",
"imdbId": "{imdbId}",
"title": "{title}",
"year": "{year}",
"seasonNumber": {seasonNumber},
"isAnime": {isAnime},
"jellyfinUserId": "{jellyfinUserId}"
}Note: {seasonNumber} and {isAnime} are numeric/boolean values and should not be quoted in JSON payloads.
Click Save at the bottom of the page to apply webhook settings.
When a user clicks "Play" on a virtual library item:
- JellyNext intercepts the playback and extracts content metadata
- Placeholder values are resolved from the content metadata
- Placeholders are replaced in URL, headers, and payload
- HTTP request is sent to the configured webhook URL
- User receives notification based on webhook response (success/failure)
- Playback is stopped to prevent the dummy video from playing
User clicks "Play" on virtual item
↓
PlaybackInterceptor detects virtual path
↓
Extracts content IDs and metadata
↓
WebhookDownloadProvider selected
↓
Replaces placeholders in URL/headers/payload
↓
Sends HTTP request to webhook endpoint
↓
Returns success/failure to user
↓
Stops playback automatically
- 2xx status codes (200, 201, 204, etc.) are treated as successful
- Non-2xx status codes are logged as errors and shown to the user
- Network errors (timeouts, connection refused) are caught and reported
The webhook endpoint should return a 2xx status code to indicate the request was accepted. The actual download/processing can happen asynchronously after responding.
Use case: Trigger a download via URL parameters only
Movie URL:
https://api.example.com/download?type=movie&tmdb={tmdbId}&user={jellyfinUserId}
Show URL:
https://api.example.com/download?type=show&tvdb={tvdbId}&season={seasonNumber}&user={jellyfinUserId}
Method: GET Headers: None Payload: Empty (GET requests don't use payloads)
Use case: Secure API with authentication header
Movie URL:
https://api.example.com/media/movie
Show URL:
https://api.example.com/media/show
Method: POST Headers:
Authorization: Bearer YOUR_API_TOKEN_HERE
Content-Type: application/json
Movie Payload:
{
"mediaType": "movie",
"tmdbId": "{tmdbId}",
"requestedBy": "{jellyfinUserId}"
}Show Payload:
{
"mediaType": "show",
"tvdbId": "{tvdbId}",
"seasonNumber": {seasonNumber},
"requestedBy": "{jellyfinUserId}"
}Use case: Send notifications to Discord/Slack when downloads are requested
Movie URL:
https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN
Method: POST Headers: None (Discord handles JSON automatically)
Movie Payload:
{
"content": "New movie download requested",
"embeds": [{
"title": "{title} ({year})",
"description": "TMDB: {tmdbId}\nRequested by: {jellyfinUserId}",
"color": 5814783
}]
}Show Payload:
{
"content": "New show download requested",
"embeds": [{
"title": "{title} ({year}) - Season {seasonNumber}",
"description": "TVDB: {tvdbId}\nAnime: {isAnime}\nRequested by: {jellyfinUserId}",
"color": 5814783
}]
}Use case: Custom media management system with user-specific quality preferences
Movie URL:
https://downloads.myserver.com/api/v1/request/movie
Show URL:
https://downloads.myserver.com/api/v1/request/show
Method: POST Headers:
X-Api-Key: YOUR_API_KEY
X-User-Id: {jellyfinUserId}
X-Request-Source: JellyNext
Movie Payload:
{
"media": {
"type": "movie",
"ids": {
"tmdb": "{tmdbId}",
"imdb": "{imdbId}"
},
"title": "{title}",
"year": "{year}"
},
"request": {
"userId": "{jellyfinUserId}",
"source": "jellynext",
"timestamp": "ISO8601_TIMESTAMP_HERE"
}
}Show Payload:
{
"media": {
"type": "show",
"ids": {
"tvdb": "{tvdbId}",
"tmdb": "{tmdbId}",
"imdb": "{imdbId}"
},
"title": "{title}",
"year": "{year}",
"season": {seasonNumber},
"anime": {isAnime}
},
"request": {
"userId": "{jellyfinUserId}",
"source": "jellynext",
"timestamp": "ISO8601_TIMESTAMP_HERE"
}
}Use case: Different systems handle movies vs shows
Movie URL:
https://movies.myserver.com/api/request
Show URL:
https://shows.myserver.com/api/request
Method: POST (both) Headers (both):
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
Configure each payload template according to your specific API requirements.
If you're building a custom endpoint to receive JellyNext webhooks, here's what to expect:
Movie Requests (POST):
POST /your/webhook/path HTTP/1.1
Host: example.com
Content-Type: application/json
[Your custom headers]
{
"tmdbId": "550",
"imdbId": "tt0137523",
"title": "Fight Club",
"year": "1999",
"jellyfinUserId": "a1b2c3d4-e5f6-7890-1234-567890abcdef"
}Show Requests (POST):
POST /your/webhook/path HTTP/1.1
Host: example.com
Content-Type: application/json
[Your custom headers]
{
"tvdbId": "73739",
"tmdbId": "1396",
"imdbId": "tt0903747",
"title": "Breaking Bad",
"year": "2008",
"seasonNumber": 2,
"isAnime": false,
"jellyfinUserId": "a1b2c3d4-e5f6-7890-1234-567890abcdef"
}Your endpoint should respond quickly (< 5 seconds) with:
Success:
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "accepted",
"message": "Download request queued"
}Failure:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"status": "error",
"message": "Invalid TMDB ID"
}JellyNext only checks the HTTP status code - the response body is logged but not displayed to users.
- Return 2xx immediately: Queue the actual download/processing work asynchronously
- Validate input: Check that required IDs (tmdbId/tvdbId) are present and valid
- Log requests: Keep audit trail of who requested what and when
- Handle duplicates: Check if the same media is already being processed
- Implement rate limiting: Protect against abuse or accidental spam
- Use authentication: Always validate API keys/tokens in production
from flask import Flask, request, jsonify
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route('/webhook/movie', methods=['POST'])
def movie_webhook():
data = request.json
# Validate required fields
if not data.get('tmdbId'):
return jsonify({'status': 'error', 'message': 'Missing tmdbId'}), 400
# Log the request
logging.info(f"Movie request: {data['title']} ({data['year']}) - TMDB: {data['tmdbId']}")
logging.info(f"Requested by Jellyfin user: {data['jellyfinUserId']}")
# Queue your download logic here (async)
# queue_movie_download(data['tmdbId'], data['jellyfinUserId'])
return jsonify({'status': 'accepted', 'message': 'Download queued'}), 200
@app.route('/webhook/show', methods=['POST'])
def show_webhook():
data = request.json
# Validate required fields
if not data.get('tvdbId'):
return jsonify({'status': 'error', 'message': 'Missing tvdbId'}), 400
# Log the request
logging.info(f"Show request: {data['title']} S{data['seasonNumber']:02d}")
logging.info(f"TVDB: {data['tvdbId']}, Anime: {data['isAnime']}")
logging.info(f"Requested by Jellyfin user: {data['jellyfinUserId']}")
# Queue your download logic here (async)
# queue_show_download(data['tvdbId'], data['seasonNumber'], data['jellyfinUserId'])
return jsonify({'status': 'accepted', 'message': 'Download queued'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/movie', (req, res) => {
const { tmdbId, imdbId, title, year, jellyfinUserId } = req.body;
// Validate required fields
if (!tmdbId) {
return res.status(400).json({ status: 'error', message: 'Missing tmdbId' });
}
// Log the request
console.log(`Movie request: ${title} (${year}) - TMDB: ${tmdbId}`);
console.log(`Requested by Jellyfin user: ${jellyfinUserId}`);
// Queue your download logic here (async)
// queueMovieDownload(tmdbId, jellyfinUserId);
res.json({ status: 'accepted', message: 'Download queued' });
});
app.post('/webhook/show', (req, res) => {
const { tvdbId, tmdbId, imdbId, title, year, seasonNumber, isAnime, jellyfinUserId } = req.body;
// Validate required fields
if (!tvdbId) {
return res.status(400).json({ status: 'error', message: 'Missing tvdbId' });
}
// Log the request
console.log(`Show request: ${title} S${seasonNumber.toString().padStart(2, '0')}`);
console.log(`TVDB: ${tvdbId}, Anime: ${isAnime}`);
console.log(`Requested by Jellyfin user: ${jellyfinUserId}`);
// Queue your download logic here (async)
// queueShowDownload(tvdbId, seasonNumber, jellyfinUserId);
res.json({ status: 'accepted', message: 'Download queued' });
});
app.listen(8080, () => {
console.log('Webhook server listening on port 8080');
});- Check integration mode: Ensure Webhooks is selected in plugin settings
- Verify URLs are configured: Both movie and show URLs must be set
- Check Jellyfin logs: Look for "WebhookDownloadProvider" entries
- Test URL accessibility: Ensure webhook endpoint is reachable from Jellyfin server
- Check endpoint logs: See what error your webhook endpoint is returning
- Verify HTTP method: Ensure your endpoint accepts the configured method (GET/POST/PUT/PATCH)
- Check authentication: Verify API keys/tokens are correct in custom headers
- Test placeholders: Ensure all placeholders are being replaced correctly (check logs)
- Validate JSON: If using POST/PUT/PATCH, ensure payload template is valid JSON
- Check placeholder syntax: Must use curly braces
{tmdbId}not$tmdbIdor%tmdbId% - Case sensitivity: Placeholders are case-insensitive (
{TMDBID}works same as{tmdbId}) - Available placeholders: Only use placeholders listed in this guide
- Missing data: If a placeholder value is empty (e.g., missing IMDb ID), it will be replaced with empty string
- Go to: Dashboard → Plugins → JellyNext
- Select user and click User Settings
- Enable Extra Logging
- Reproduce the issue
- Check: Dashboard → Logs
- Search for: "WebhookDownloadProvider"
Look for entries showing:
- Webhook URL after placeholder replacement
- HTTP method and headers
- Request payload (if POST/PUT/PATCH)
- Response status code
- Error messages if request failed
Always use authentication for production webhooks:
- API Keys: Add
X-Api-KeyorAuthorizationheader - Bearer Tokens: Use
Authorization: Bearer YOUR_TOKEN - HMAC Signatures: Sign payloads for verification (implement in custom endpoint)
Use HTTPS URLs for production to encrypt data in transit:
✅ https://api.example.com/webhook
❌ http://api.example.com/webhook
Always validate webhook data on your endpoint:
- Check that required IDs are present and numeric
- Validate user IDs match expected format
- Sanitize string inputs (title, year) before using in SQL/shell commands
- Implement rate limiting to prevent abuse
- Firewall: Only allow webhook requests from Jellyfin server IP
- VPN/Private Network: Keep webhooks on internal network when possible
- Reverse Proxy: Use nginx/Caddy to add additional security layers
Use {jellyfinUserId} to route requests to user-specific quality profiles or download paths:
URL:
https://api.example.com/download?user={jellyfinUserId}&tmdb={tmdbId}
Your endpoint can then look up per-user preferences and apply them.
Chain webhooks by having your endpoint trigger additional webhooks:
- JellyNext → Your validation endpoint (checks quota, permissions)
- Your endpoint → Download system (if validation passes)
- Your endpoint → Notification system (notify user of status)
Use {isAnime} to route anime to different systems:
Show Payload:
{
"tvdbId": "{tvdbId}",
"seasonNumber": {seasonNumber},
"category": "{isAnime}",
"targetSystem": "automatic"
}Your endpoint can check category and route to anime-specific downloaders.
Use {jellyfinUserId} to maintain audit logs:
- Who requested what content
- When requests were made
- How many requests per user
- Usage patterns and analytics
If migrating from Native (Radarr/Sonarr) integration:
- Note your current Radarr/Sonarr URLs and API keys
- Build a webhook endpoint that forwards to Radarr/Sonarr APIs
- Test thoroughly before switching
- Update JellyNext to Webhook mode
- Configure webhook URLs pointing to your new endpoint
If migrating from Jellyseerr integration:
- Note your Jellyseerr configuration
- Build a webhook endpoint that calls Jellyseerr API
- Use Jellyseerr's
/api/v1/requestendpoint in your webhook - Include
X-Api-Userheader for per-user attribution - Update JellyNext to Webhook mode
For webhook integration issues:
- Check this guide thoroughly
- Enable debug logging and check Jellyfin logs
- Test your webhook endpoint independently (curl/Postman)
- Report issues at: https://github.com/luall0/jellynext/issues
Include in bug reports:
- Webhook configuration (URLs, method, headers - redact secrets!)
- Jellyfin logs showing webhook requests
- Your endpoint logs showing received requests
- JellyNext version
| Feature | Native | Jellyseerr | Webhook |
|---|---|---|---|
| Radarr/Sonarr Required | ✅ Yes | Via Jellyseerr | No |
| Custom Systems | ❌ No | ❌ No | ✅ Yes |
| Approval Workflows | ❌ No | ✅ Yes | Custom |
| Request Tracking | ❌ No | ✅ Yes | Custom |
| Flexibility | Low | Medium | High |
| Setup Complexity | Low | Medium | High |
| Best For | Direct downloads | Multi-user with approvals | Custom integrations |
Ready to configure webhooks? Head to Dashboard → Plugins → JellyNext → Download Integration and select 🔗 Webhooks to get started!