Webhooks (credential-authenticated, no session)
HTTP routes and behavior for webhooks (credential-authenticated, no session).
POST /api/webhooks/arr/{instanceID} # Sonarr/Radarr/Chaptarr -> Connect -> Webhook (Basic Auth)Instant updates are on by default: the app invokes this configuration right after a Radarr/Sonarr/Chaptarr/Lidarr instance is created (a failure is reported without undoing the create), and the instance editor’s Configure instant updates button re-runs it to repair or refresh. Either way the action asks the server to rotate a per-instance credential and create or update a Cantinarr Connect webhook in Radarr/Sonarr/Chaptarr/Lidarr. The secret moves only between servers: instance API responses and the app never receive it. Managed records use webhook Basic Auth; query-string credentials are rejected and access logs omit all query strings. Set CANTINARR_ARR_CALLBACK_URL (formerly CANTINARR_PUBLIC_URL, still accepted) when Cantinarr is behind a reverse proxy; callback generation uses that trusted origin and never trusts client-supplied forwarded headers. The callback must be resolvable and reachable from inside the arr containers. Cantinarr explicitly acknowledges the lineage’s warning for a private or cluster-local callback because Cantinarr constructs it from the configured or validated request origin rather than accepting an arbitrary callback from the app, while real validation failures remain fatal; creates still run the arr’s callback test, and updates run that test explicitly before saving. A validation failure surfaces the arr’s own verdict in the admin-facing error (extracted from the lineage validation shape only; never the raw body, and with credentials redacted) plus the exact callback URL that was registered, so “Unable to send test message” reads directly as a reachability problem with the URL to check. In Docker/k8s topologies a cluster-internal origin (http://cantinarr:8585) is usually correct; a public FQDN works only if the arrs can egress (or hairpin) to it. The configurator can still recognize an old copy/paste record by its callback path and migrate it. Rotation keeps the current and pending credentials valid until the arr accepts the update, and configuration is serialized per instance, so failed or concurrent attempts cannot break a working hook. The GET form of the route backs the editor’s status line: {supported, configured, state} derived from the arr’s Connect list on every read: never a stored flag, which would keep claiming configured after an admin deleted the record there: where state explains a negative answer (missing; stale for a record targeting a different callback than the server now expects; credential_missing when the arr record looks right but this server holds no accepted credential, e.g. a restored database; no_public_url). An arr that cannot be read is a 502, never missing: blindness and absence must not render the same. Handled events: Grab, Download, MovieAdded/SeriesAdd, MovieDelete/SeriesDelete, MovieFileDelete, EpisodeFileDelete, and for Chaptarr the book set (AuthorAdded, BookAdded, AuthorDelete, BookDelete, BookFileDelete, Rename, Retag); for Lidarr every event invalidates and only the import events (Download/AlbumImport and their history spellings) announce: invalidate availability, broadcast WebSocket updates, and (for imports) send new-content pushes; Test and everything else is acknowledged with 200 so the arr’s Test button just works. A Sonarr Download does one thing more: the payload’s own episodes[] air dates are read to spot an import that landed on an episode which has not aired yet; more than twelve hours before its air time, the same margin floor the season verdict applies, because TheTVDB stamps a binge premiere as a runtime-staggered linear schedule and a file that “airs” in twenty minutes is a release-day grab, not a finding; and that lone comparison is what hands a pre-air season fill to the detector below without anyone reporting it.