# Profilarr API v1

> The complete Profilarr API v1 reference in a single file.

All requests use the base URL `${PROFILARR_URL}/api/v1`. Authenticate by sending the API key in the `X-Api-Key` header. `${PROFILARR_URL}` and `${API_KEY}` are placeholders for your own Profilarr instance URL and API key.

Profilarr manages configuration for Radarr and Sonarr by syncing
curated configuration databases into Arr instances.

## System

Health checks, system status, and API metadata.

### GET /health

Health Check

Public endpoint, no authentication required. Use this for uptime monitors
and load balancers. Returns 200 for healthy or degraded, 503 for unhealthy.

#### Responses

**200**: System is healthy or degraded (status field indicates which)

```json
{
  "status": "healthy",
  "timestamp": "2026-03-09T12:00:00.000Z"
}
```

**503**: System is unhealthy (status field will be "unhealthy")

```json
{
  "status": "unhealthy",
  "timestamp": "2026-03-09T12:00:00.000Z"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/health" \
  -H "X-Api-Key: ${API_KEY}"
```

### GET /status

System Status

Single-call overview designed for dashboard integrations (e.g. Homepage).
The `drift` field on each arr is reserved for future use and always null.

#### Responses

**200**: System status

```json
{
  "version": "2.0.0",
  "uptime": 86400,
  "timezone": "America/New_York",
  "databases": [
    {
      "id": 1,
      "name": "Dictionarry",
      "enabled": true,
      "lastSyncedAt": "2026-04-09T12:00:00Z",
      "syncStrategy": "auto",
      "counts": {
        "customFormats": 52,
        "qualityProfiles": 3,
        "regularExpressions": 120,
        "delayProfiles": 1
      }
    }
  ],
  "arrs": [
    {
      "id": 1,
      "name": "Radarr",
      "type": "radarr",
      "enabled": true,
      "sync": {
        "qualityProfiles": {
          "count": 2,
          "status": "idle",
          "lastSyncedAt": "2026-04-09T12:00:00Z"
        },
        "delayProfiles": {
          "configured": true,
          "status": "idle",
          "lastSyncedAt": "2026-04-09T12:00:00Z"
        },
        "mediaManagement": {
          "configured": true,
          "status": "idle",
          "lastSyncedAt": "2026-04-09T12:00:00Z"
        }
      },
      "drift": null
    }
  ],
  "jobs": {
    "active": 0,
    "queued": 2,
    "nextRunAt": "2026-04-09T13:00:00Z"
  },
  "backups": {
    "enabled": true,
    "lastBackupAt": "2026-04-09T00:00:00Z"
  },
  "announcements": {
    "unread": 2
  }
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to System"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/status" \
  -H "X-Api-Key: ${API_KEY}"
```

### GET /openapi.json

OpenAPI Specification

Returns the resolved OpenAPI 3.1 spec as a single JSON document.
Useful for generating client SDKs or feeding to documentation tools.

#### Responses

**200**: OpenAPI specification

```json
"string"
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to System"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/openapi.json" \
  -H "X-Api-Key: ${API_KEY}"
```

## Jobs

Job queue status and polling

### GET /jobs/{id}

Get Job Status

Returns the current status of a job queue entry.

**Use cases:**
- Polling for completion after creating a backup
- Checking if a scheduled job has run
- Inspecting job errors after failure

**Behavior:**
- Returns the job queue record with its current status
- If the job has executed, `result` includes the latest run history
- If the job is still queued or running, `result` is null

#### Parameters

| Name | In | Type | Required | Description |
| ---- | -- | ---- | -------- | ----------- |
| id | path | integer | yes | Job queue ID (returned by endpoints that create jobs) |

#### Responses

**200**: Job found

```json
{
  "id": 42,
  "jobType": "backup.create",
  "status": "success",
  "source": "manual",
  "createdAt": "2026-03-15T10:00:00.000Z",
  "startedAt": "2026-03-15T10:00:01.000Z",
  "finishedAt": "2026-03-15T10:00:05.000Z",
  "result": {
    "status": "success",
    "output": "Backup created: backup-2026-03-15-100005.tar.gz (12.34 MB)",
    "error": null,
    "durationMs": 4000
  }
}
```

**400**: Invalid job ID (non-numeric or less than 1)

```json
{
  "error": "Invalid job ID"
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Jobs"
}
```

**404**: Job not found

```json
{
  "error": "Job not found"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/jobs/{id}" \
  -H "X-Api-Key: ${API_KEY}"
```

## Backups

Backup creation, listing, download, upload, and settings

### GET /backups

List Backups

#### Responses

**200**: List of backup files, sorted newest first

```json
[
  {
    "filename": "backup-2026-03-15-100005.tar.gz",
    "created": "2026-01-01T00:00:00Z",
    "size": 0,
    "sizeFormatted": "12.34 MB"
  }
]
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Backups"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/backups" \
  -H "X-Api-Key: ${API_KEY}"
```

### POST /backups

Create Backup

Async. Enqueues a `backup.create` job and returns 202 with the job ID.
Poll `GET /api/v1/jobs/{jobId}` for completion. The backup includes a
sanitized database copy with all secrets stripped.

#### Responses

**202**: Backup job enqueued

```json
{
  "jobId": 42
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have write access to Backups"
}
```

#### Example

```bash
curl -X POST "${PROFILARR_URL}/api/v1/backups" \
  -H "X-Api-Key: ${API_KEY}"
```

### GET /backups/{filename}

Download Backup

Download a backup archive. The local file on disk is full-fidelity, but
the downloaded copy is sanitized on the fly so it is safer to share.
The following are removed from the downloaded archive:

- Arr instances (URLs, API keys, sync configs, drift state, rename and
  cleanup history)
- Notification services (webhook URLs, tokens, history)
- User accounts and active sessions
- Personal access tokens for linked databases
- AI and TMDB API keys

The local archive on the server is not modified. Restoring the
downloaded file on a different host will require re-adding the removed
items.

#### Parameters

| Name | In | Type | Required | Description |
| ---- | -- | ---- | -------- | ----------- |
| filename | path | string | yes | Backup filename (must match `backup-*.tar.gz`) |

#### Responses

**200**: Sanitized backup file download

```json
"string"
```

**400**: Invalid filename or path traversal attempt

```json
{
  "error": "Invalid filename"
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Backups"
}
```

**404**: Backup file not found

```json
{
  "error": "Backup file not found"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/backups/{filename}" \
  -H "X-Api-Key: ${API_KEY}"
```

### DELETE /backups/{filename}

Delete Backup

#### Parameters

| Name | In | Type | Required | Description |
| ---- | -- | ---- | -------- | ----------- |
| filename | path | string | yes | Backup filename |

#### Responses

**200**: Backup deleted

```json
{
  "success": true
}
```

**400**: Invalid filename or path traversal attempt

```json
{
  "error": "Invalid filename"
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have write access to Backups"
}
```

**404**: Backup file not found

```json
{
  "error": "Backup file not found"
}
```

#### Example

```bash
curl -X DELETE "${PROFILARR_URL}/api/v1/backups/{filename}" \
  -H "X-Api-Key: ${API_KEY}"
```

### POST /backups/upload

Upload Backup

Only `.tar.gz` files, max 1GB. Archive contents are scanned for path
traversal entries (zip slip protection). Files without a `backup-` prefix
are renamed to `backup-uploaded-{timestamp}.tar.gz`. Duplicates rejected.

#### Request Body

`multipart/form-data` (required)

```json
{
  "file": "string"
}
```

#### Responses

**201**: Backup uploaded

```json
{
  "filename": "backup-uploaded-1710504000000.tar.gz",
  "size": 12345678,
  "sizeFormatted": "11.77 MB"
}
```

**400**: Invalid file (wrong type, too large, zip slip, or duplicate)

```json
{
  "error": "Invalid file type. Only .tar.gz files are allowed."
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have write access to Backups"
}
```

#### Example

```bash
curl -X POST "${PROFILARR_URL}/api/v1/backups/upload" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
  "file": "string"
}'
```

### GET /backups/settings

Get Backup Settings

#### Responses

**200**: Current backup settings

```json
{
  "schedule": "daily",
  "retentionDays": 30,
  "enabled": true,
  "includeDatabase": true,
  "compressionEnabled": true
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Backups"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/backups/settings" \
  -H "X-Api-Key: ${API_KEY}"
```

### PATCH /backups/settings

Update Backup Settings

Partial update. After saving, backup jobs are rescheduled to match the
new settings.

#### Request Body

`application/json` (required)

```json
{
  "schedule": "hourly",
  "retentionDays": 0,
  "enabled": false
}
```

#### Responses

**200**: Updated backup settings

```json
{
  "schedule": "hourly",
  "retentionDays": 0,
  "enabled": false,
  "includeDatabase": false,
  "compressionEnabled": false
}
```

**400**: Invalid input (bad schedule, retention out of range, empty body)

```json
{
  "error": "retentionDays must be between 1 and 365"
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have write access to Backups"
}
```

#### Example

```bash
curl -X PATCH "${PROFILARR_URL}/api/v1/backups/settings" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
  "schedule": "hourly",
  "retentionDays": 0,
  "enabled": false
}'
```

## Arr

Arr instance management, library, and sync endpoints

### GET /arr

List Arr Instances

Returns all Arr instances (Radarr/Sonarr) with secrets stripped.

**Use cases:**
- Dashboard widgets showing connected instances
- Automation scripts checking instance state
- Prerequisite checks (e.g. onboarding)

**Behavior:**
- Returns an empty array if no instances are connected
- The `api_key` field is never included

#### Responses

**200**: List of Arr instances

```json
[
  {
    "id": 0,
    "name": "string",
    "type": "radarr",
    "url": "string",
    "external_url": null,
    "tags": null,
    "enabled": 0,
    "library_refresh_interval": 0,
    "library_last_refreshed_at": null,
    "created_at": "string",
    "updated_at": "string"
  }
]
```

**401**: Not authenticated

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Arr"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/arr" \
  -H "X-Api-Key: ${API_KEY}"
```

## Databases

Linked database instance management

### GET /databases

List Databases

Secrets are stripped: `personal_access_token` is replaced by `hasPat` (boolean)
and `local_path` is excluded.

#### Responses

**200**: List of linked databases

```json
[
  {
    "id": 0,
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "name": "string",
    "repository_url": "string",
    "sync_strategy": 0,
    "auto_pull": 0,
    "enabled": 0,
    "is_private": 0,
    "local_ops_enabled": 0,
    "git_user_name": null,
    "git_user_email": null,
    "conflict_strategy": "override",
    "last_synced_at": null,
    "created_at": "string",
    "updated_at": "string",
    "hasPat": false
  }
]
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Databases"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/databases" \
  -H "X-Api-Key: ${API_KEY}"
```

### POST /databases

Link Database

Clones the repository, validates its PCD manifest, and processes dependencies.
This is synchronous and may take several seconds for large repositories.

Field dependencies:
- `personal_access_token` requires `git_user_name` and `git_user_email`
- `local_ops_enabled` requires `personal_access_token`
- `conflict_strategy` is only accepted without a PAT, or with PAT + `local_ops_enabled`

#### Request Body

`application/json` (required)

```json
{
  "name": "string",
  "repository_url": "string",
  "branch": "string",
  "personal_access_token": "string",
  "git_user_name": "string",
  "git_user_email": "string",
  "sync_strategy": 0,
  "auto_pull": false,
  "local_ops_enabled": false,
  "conflict_strategy": "override"
}
```

#### Responses

**201**: Database linked

```json
{
  "id": 0,
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "name": "string",
  "repository_url": "string",
  "sync_strategy": 0,
  "auto_pull": 0,
  "enabled": 0,
  "is_private": 0,
  "local_ops_enabled": 0,
  "git_user_name": null,
  "git_user_email": null,
  "conflict_strategy": "override",
  "last_synced_at": null,
  "created_at": "string",
  "updated_at": "string",
  "hasPat": false
}
```

**400**: Validation error

```json
{
  "error": "Name and repository URL are required"
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have write access to Databases"
}
```

**409**: A database with this name already exists

```json
{
  "error": "A database with this name already exists"
}
```

**422**: Link failed (invalid URL, clone failure, bad manifest)

```json
{
  "error": "Repository not found or inaccessible"
}
```

#### Example

```bash
curl -X POST "${PROFILARR_URL}/api/v1/databases" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "string",
  "repository_url": "string",
  "branch": "string",
  "personal_access_token": "string",
  "git_user_name": "string",
  "git_user_email": "string",
  "sync_strategy": 0,
  "auto_pull": false,
  "local_ops_enabled": false,
  "conflict_strategy": "override"
}'
```

### GET /databases/{id}

Get Database

Secrets are stripped: `personal_access_token` is replaced by `hasPat` (boolean)
and `local_path` is excluded.

#### Parameters

| Name | In | Type | Required | Description |
| ---- | -- | ---- | -------- | ----------- |
| id | path | integer | yes | Database instance ID |

#### Responses

**200**: Database detail

```json
{
  "id": 0,
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "name": "string",
  "repository_url": "string",
  "sync_strategy": 0,
  "auto_pull": 0,
  "enabled": 0,
  "is_private": 0,
  "local_ops_enabled": 0,
  "git_user_name": null,
  "git_user_email": null,
  "conflict_strategy": "override",
  "last_synced_at": null,
  "created_at": "string",
  "updated_at": "string",
  "hasPat": false
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Databases"
}
```

**404**: Database not found

```json
{
  "error": "Database not found"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/databases/{id}" \
  -H "X-Api-Key: ${API_KEY}"
```

### PATCH /databases/{id}

Update Database

Partial update. Only the provided fields are changed. Unknown fields
are silently ignored.

Field dependencies:
- `personal_access_token` requires `git_user_name` and `git_user_email`
- `local_ops_enabled` requires `personal_access_token`
- `conflict_strategy` is only accepted without a PAT, or with PAT + `local_ops_enabled`

#### Parameters

| Name | In | Type | Required | Description |
| ---- | -- | ---- | -------- | ----------- |
| id | path | integer | yes | Database instance ID |

#### Request Body

`application/json` (required)

```json
{
  "name": "string",
  "personal_access_token": "string",
  "git_user_name": "string",
  "git_user_email": "string",
  "sync_strategy": 0,
  "auto_pull": false,
  "local_ops_enabled": false,
  "conflict_strategy": "override"
}
```

#### Responses

**200**: Database updated

```json
{
  "id": 0,
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "name": "string",
  "repository_url": "string",
  "sync_strategy": 0,
  "auto_pull": 0,
  "enabled": 0,
  "is_private": 0,
  "local_ops_enabled": 0,
  "git_user_name": null,
  "git_user_email": null,
  "conflict_strategy": "override",
  "last_synced_at": null,
  "created_at": "string",
  "updated_at": "string",
  "hasPat": false
}
```

**400**: Validation error or empty body

```json
{
  "error": "No updatable fields provided"
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have write access to Databases"
}
```

**404**: Database not found

```json
{
  "error": "Database not found"
}
```

**409**: A database with this name already exists

```json
{
  "error": "A database with this name already exists"
}
```

#### Example

```bash
curl -X PATCH "${PROFILARR_URL}/api/v1/databases/{id}" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "string",
  "personal_access_token": "string",
  "git_user_name": "string",
  "git_user_email": "string",
  "sync_strategy": 0,
  "auto_pull": false,
  "local_ops_enabled": false,
  "conflict_strategy": "override"
}'
```

### DELETE /databases/{id}

Unlink Database

Removes the database row, deletes the cloned repository from disk,
and cancels any scheduled sync jobs.

#### Parameters

| Name | In | Type | Required | Description |
| ---- | -- | ---- | -------- | ----------- |
| id | path | integer | yes | Database instance ID |

#### Responses

**204**: Database unlinked

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have write access to Databases"
}
```

**404**: Database not found

```json
{
  "error": "Database not found"
}
```

#### Example

```bash
curl -X DELETE "${PROFILARR_URL}/api/v1/databases/{id}" \
  -H "X-Api-Key: ${API_KEY}"
```

### POST /databases/{id}/sync

Trigger Sync

Async. Enqueues a `pcd.sync` job and returns 202 with the job ID.
Poll `GET /api/v1/jobs/{jobId}` for completion.

#### Parameters

| Name | In | Type | Required | Description |
| ---- | -- | ---- | -------- | ----------- |
| id | path | integer | yes | Database instance ID |

#### Responses

**202**: Sync job enqueued

```json
{
  "jobId": 42
}
```

**400**: Database is disabled

```json
{
  "error": "Database is disabled"
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have write access to Databases"
}
```

**404**: Database not found

```json
{
  "error": "Database not found"
}
```

#### Example

```bash
curl -X POST "${PROFILARR_URL}/api/v1/databases/{id}/sync" \
  -H "X-Api-Key: ${API_KEY}"
```

## Announcements

In-app announcements fed from the bulletin repo

### GET /announcements

List Announcements

Visible announcements only: withdrawn, expired, and version-incompatible
entries are filtered out server-side. Bodies are not included; use the
detail endpoint to fetch one.

#### Responses

**200**: Visible announcements, newest first.

```json
[
  {
    "id": "01HXYZ0000000000000000000A",
    "title": "API v1 migration landing in 2.4",
    "severity": "warning",
    "publishedAt": "2026-04-10T10:00:00Z",
    "expiresAt": null,
    "minVersion": "2.0.0",
    "maxVersion": null,
    "link": "https://github.com/Dictionarry-Hub/profilarr/discussions/999",
    "readAt": null
  }
]
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Announcements"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/announcements" \
  -H "X-Api-Key: ${API_KEY}"
```

### GET /announcements/{id}

Get Announcement

Returns the full announcement including its markdown body. The body
is lazy-fetched from the bulletin repo on first call and cached
locally; subsequent calls read from cache.

404 is returned for unknown ids and for ids that have not yet been
ingested by the fetch job (e.g. calling the API between a new
bulletin PR merging and the next scheduled reconcile).

#### Parameters

| Name | In | Type | Required | Description |
| ---- | -- | ---- | -------- | ----------- |
| id | path | string | yes | ULID of the announcement. |

#### Responses

**200**: Announcement detail with body.

```json
{
  "id": "string",
  "title": "string",
  "severity": "info",
  "publishedAt": "2026-01-01T00:00:00Z",
  "expiresAt": null,
  "minVersion": null,
  "maxVersion": null,
  "link": null,
  "readAt": null,
  "body": null
}
```

**401**: Not authenticated

```json
{
  "error": "Unauthorized"
}
```

**403**: API key does not have the required permission

```json
{
  "error": "API key does not have read access to Announcements"
}
```

**404**: Announcement not found.

```json
{
  "error": "Announcement not found"
}
```

#### Example

```bash
curl -X GET "${PROFILARR_URL}/api/v1/announcements/{id}" \
  -H "X-Api-Key: ${API_KEY}"
```

---

Index of this site's Markdown pages: https://profilarr.com/llms.txt
