REST API¶
Every response uses the envelope {"success": bool, "data": ..., "error": "..."}. Some refusals also carry a stable, machine-readable code next to error (api_key_expired, rate_limited); match it rather than the English text.
Authentication¶
There is no unauthenticated mode. Apart from the public routes below, every request needs either
- a session: the
mr_sessioncookie set byPOST /api/v1/setuporPOST /api/v1/auth/login(HttpOnly,SameSite=Strict,Secureover HTTPS). Unsafe methods (POST,PUT,PATCH,DELETE) must also send the session's CSRF token asX-CSRF-Token, or they are rejected with403; or - an API key:
Authorization: Bearer <key>orX-API-Key: <key>. Keys are created under Settings → API keys (mr_<prefix>_<secret>, shown once); aMONGORESCUE_API_KEYof an earlier build is imported once as a key. API-key requests need no CSRF token. The command line (mongorescue backup,restore,list,verify,status) is a client of these routes with an API key.
Public routes: / and the dashboard assets, GET /api/v1/health, GET /api/v1/setup/status, POST /api/v1/setup, POST /api/v1/auth/login, GET /api/v1/auth/methods and the single sign-on routes GET /auth/oidc/start and GET /auth/oidc/callback. GET /api/v1/auth/me answers signed-out visitors with 200 and an empty session ({"user": null, "csrf_token": "", "auth": "", "role": "", "scope": ""}); every other protected route answers 401. /metrics takes an API key (not a session) unless the security.metrics_public setting is on. /mcp, the MCP endpoint, takes an API key only, never a session.
Dashboard roles¶
Every user has a dashboard role: viewer, operator or admin (design). A session gets the scope of its user's role: viewers read, operators operator, administrators admin. The first user (setup) is always an admin, users that existed before v0.17.0 are admins, and POST /api/v1/users creates a viewer unless the request names another role. An API key created by a user can never do more than that user's current role: its effective scope is the lower of the key's scope and the creator's role, recomputed on every request, so demoting a user caps their keys at once (GET /api/v1/api-keys shows effective_scope; GET /api/v1/auth/me shows role, the effective scope and, for keys, key_scope). Keys without a creator (imported from MONGORESCUE_API_KEY) keep their scope. A key cannot be created with a scope above the creator's (403). The last admin can be neither demoted nor deleted (409), and nobody can change their own role (400).
Connection access¶
A user or an API key reaches every connection or exactly a list of them (design). The access is two fields, all_connections and connection_ids: {"all_connections": true} is every connection, {"all_connections": false, "connection_ids": [...]} exactly those, and an empty connection_ids is no connection, never every connection. Users and keys stored before connection access existed have all_connections: true. Administrators set a user's access with PUT /api/v1/users/{id}/connections (or in POST /api/v1/users) and a key's in POST /api/v1/api-keys; a request with connection_ids but without all_connections means exactly that list, one with neither field every connection, and all_connections: true with a non-empty list is refused (400). Saving a user's list drops, silently, the IDs of deleted connections the user still had, so removing a deleted connection never forces anyone to widen access; other unknown IDs are refused (400). A key is always capped by its creator's current connections, like its scope by the creator's role: a key with all_connections created by a limited user reaches the user's connections, one under a creator with no connection reaches none, and a list naming a connection outside the creator's is refused (404 for a limited caller, like any connection it cannot see). Administrators, and keys with the admin scope, always reach every connection: any limit on them is refused (400), and promoting a user to admin gives them every connection. Group mappings carry connections too.
For a caller limited to some connections, every route behaves as if the other connections, their jobs, backups, restores and run logs did not exist:
- lists (
GET /api/v1/connections,/jobs,/backups,/restores, theirdatabaseslists,/runs/active,/readinesswith its PITRstreams,/pitr/streams,/stats,/stats/history,/notifications/rules,/pending-changes) contain only the caller's records; filters naming another connection find nothing; - a PITR stream belongs to its connection: another connection's stream, its chunks,
POST /api/v1/pitr/streams/{id}/baseand.../chain-test, and a point-in-time restore or preflight naming it (by stream or connection ID, inPOST /api/v1/restoreand/restores/preflight, MCPpitr_restore, CLIrestore --pitr) answer404(PITR stream not found), before the admin check those restores need;pitr_statuslists the caller's streams only, and the targets of the caller's streams count as its targets; - a route whose path, query or body names another connection or one of its records answers
404(connection not found,job not found,backup not found, ...), exactly like an ID that does not exist; restoring into another connection is404too; - bulk actions skip such items as
not_found; - storage targets: only the targets the caller's jobs and backups use and the default target are listed and readable (
404otherwise), and a backup may only name those; the storage scans (GET /api/v1/storage-targets/{id}/scan, and the scans and sweep ofGET /api/v1/integrity) cover every connection's archives and are not shown; GET /metricsanswers403: the metrics label every connection, so a limited key cannot scrape them (an unlimited read key can);- the audit logs, users, settings changes, connections and storage targets themselves stay
admin, which a limited caller never is.
A backup's own connection decides, not its job's: a job that moved from another connection shows a limited caller only the backups taken from its connections (lists, statistics, history, readiness evidence, restore tests), and the RTO of the readiness report counts only restores whose source and target the caller may touch. GET /api/v1/auth/me returns all_connections and, for a limited caller, connection_ids; GET /api/v1/users returns each user's all_connections and connection_ids; GET /api/v1/api-keys returns a key's own access and its effective one, effective_all_connections and effective_connection_ids ([] without effective_all_connections: none, for example because its list and its creator's do not overlap). GET /api/v1/settings shows the connections of the group mappings to administrators only. MCP tools and the command line use these routes, so they follow the same rules: a refused CLI command exits 4 (not found).
API key expiry¶
A key may expire: POST /api/v1/api-keys takes expires_at (RFC 3339, in the future) or expires_in_days (1 to 3650), not both (400); without either the key never expires. While security.api_key_max_lifetime_days is set, a key without an expiry or with a later one than that many days from now is refused (400), and existing keys get a deadline (security.md). A request with an expired key answers 401 with {"success": false, "error": "unauthorized: the API key expired at …", "code": "api_key_expired"} (on /mcp also WWW-Authenticate: Bearer … error="invalid_token"), also for GET /api/v1/auth/me. GET /api/v1/api-keys and GET /api/v1/auth/me (key_expires_at) show the expiry; keys without one have no expires_at. The list also marks the key imported from MONGORESCUE_API_KEY (imported: true, the only key a maximum lifetime leaves without an expiry) and names the API key that created a key in created_by_key.
Rate limits¶
Each API key and each session has a request budget (security.api_rate_limit_per_minute, default 1200, with bursts of security.api_rate_limit_burst, default 300), and routes that start work (backups, restores, job runs, verifications, scans, bulk actions, tests) spend a tighter one too (security.api_work_rate_limit_per_minute, default 60, bursts of 20). Unauthenticated requests get 300 a minute (bursts of 100) per client address, with the sign-in routes (setup, login, GET /api/v1/auth/methods, GET /api/v1/auth/me, single sign-on) in a bucket of their own, so failed credentials elsewhere cannot lock out signing in (behind a reverse proxy, turn on security.trust_proxy_headers). A request over a budget is refused before it runs: 429 with Retry-After (seconds) and {"success": false, "error": "too many requests: over the general rate limit of 1200 per minute; retry in 3s", "code": "rate_limited"}; send it again after that. GET /api/v1/health and /metrics with a valid key are never limited; /mcp has its own limit. See security.md.
API key scopes¶
Every API key has a scope, chosen when it is created (read when omitted); sessions have the scope of their user's dashboard role. The auth middleware checks the effective scope of every request against one route table (internal/server/scopes.go) and answers 403 saying what the request needs and what limits the caller (the role, the key's scope, or the creator's role capping the key) when it is too small:
| Scope | Allowed |
|---|---|
read |
Every GET route except the audit logs (GET /api/v1/audit, /api/v1/audit/events, /api/v1/audit/events/export, /api/v1/audit/events/verify) and GET /api/v1/users (GET /api/v1/users/names is readable), plus /metrics and the MCP endpoint (read tools only), and the self-service routes: signing out, your own password, sessions and API keys (see below) |
operator |
read plus POST /api/v1/backups, POST /api/v1/backups/{id}/retry, POST /api/v1/jobs/{id}/run, POST /api/v1/jobs/{id}/cancel, POST /api/v1/restore into a safe clone on the backup's own connection, and cancelling backups and restores that are not in place (POST /api/v1/backups/{id}/cancel, POST /api/v1/restores/{id}/cancel), POST /api/v1/backups/{id}/verify, .../pin and POST /api/v1/jobs/{id}/restore-test, plus the MCP action tools |
admin |
Everything: deletions, in-place and cross-connection restores, jobs, connections, storage targets, notifications, settings, users (including the user list and roles), other users' passwords, sessions and API keys, and the audit logs |
Six self-service routes need only read in the table, and the auth service applies the finer rule: POST /api/v1/auth/logout ends the request's own session; PUT /api/v1/users/{id}/password changes your own password from a session with the current password, another user's with admin; DELETE /api/v1/auth/sessions/{id} ends your own user's sessions from a session, any session with admin, and no session with an API key below admin; GET /api/v1/api-keys lists only the keys you created unless you are admin; POST /api/v1/api-keys needs a session or an admin key and a scope no higher than yours; DELETE /api/v1/api-keys/{id} revokes the keys your user created from a session, only itself with an API key below admin, and any key with admin (an unknown ID is 404 for everyone).
The recovery kit (POST /api/v1/recovery-kit) is refused to every API key, admin included: it needs a signed-in administrator who confirms their password. An in-place restore ("safe_clone": false or a target_database) and a restore into another connection than the backup's (target_connection_id) need admin even though the route itself needs operator, and so does cancelling a running in-place restore (it may leave the target partially restored). Keys created before scopes existed (and a key imported from MONGORESCUE_API_KEY) are admin keys.
Failed logins return a generic 401. After 5 failures for the same (client IP, username), further attempts for that pair get 429 Too Many Requests with Retry-After; the lockout starts at 30 seconds and doubles up to 15 minutes. Once an IP has 20 recent failures, each further username from it is locked after a single failure, but the correct password of a user that is not locked always works, so clients sharing one address (NAT, a proxy) cannot lock each other out. Only one attempt per (IP, username) is checked at a time, and at most four per IP for usernames that already failed; concurrent extras get 429 with Retry-After: 2. Password comparisons share a global pool (twice the number of CPUs): a login waits up to 5 seconds for a free slot rather than being refused. Wrong setup codes are throttled only after 100 per IP within 15 minutes, and the correct code is always accepted. Passwords longer than 72 bytes are rejected without a password check.
POST /api/v1/setup and POST /api/v1/auth/login require Content-Type: application/json (415 otherwise) and, when the request has an Origin header, it must be this server's host or a configured CORS origin (403 otherwise). This blocks cross-site form posts (login CSRF).
Sessions end after the security.session_idle_timeout without requests (default 12 hours) or the security.session_absolute_timeout after login (default 7 days), on logout, when they are revoked from the session list (DELETE /api/v1/auth/sessions/{id}, the dashboard's user menu → Sessions), and when the user's password changes (other sessions) or the user is deleted. Deleting a user also revokes the API keys they created.
Single sign-on¶
With the oidc settings on, people sign in through an OpenID Connect provider (recipes, design); API keys, the CLI and MCP are unchanged. The desktop app has no single sign-on. The browser routes live outside /api/ and need no credentials:
GET /auth/oidc/start?return_to=/pathanswers404while single sign-on is off. Otherwise it sets themr_oidcflow cookie (state, nonce and PKCE verifier, sealed with a subkey ofsecret.key;HttpOnly,Path=/auth/oidc/,Max-Age=600,SameSite=Lax,Securelike the session cookie) and redirects to the provider (authorization code flow, S256 PKCE, query response mode).return_tomust be a path on this server; anything else becomes/. Starts are throttled per client address (30 a minute) and not audited.GET /auth/oidc/callback?code=…&state=…clears the flow cookie, checks the state (single use, at most 10 minutes old), exchanges the code with the verifier, verifies the ID token (signature RS256 or ES256,iss,aud,azp, nonce,exp,nbf,iatwith 60 seconds of leeway), applies the domain filter and the group mappings, finds the user by issuer and subject or creates one, and redirects toreturn_towith a session cookie. Failures redirect to/?oidc_error=<code>:state_mismatch,idp_error,token_invalid,domain_not_allowed,no_role(also an unknown identity whileauto_create_usersis off),account_conflict(a user with that name exists: users are never linked by name or email),disabledorthrottled. Tokens, the code and claims never appear in responses, logs or the audit log; access and refresh tokens are discarded.
A group mapping carries connections like a user: all_connections, or exactly connection_ids (an empty list is none; a mapping saved before connection access, with neither field, has every connection). The connections come with the role: only the mappings that grant the user's role, the highest of their matching mappings, count. A user's connections are the union of those mappings' lists, or every connection when one of them has all_connections; a mapping of a lower role never widens them. For example, with G1 → operator on connection A and G2 → viewer on every connection, a member of G1 and G2 is an operator on A only (not a viewer, nor anything, on the others); a member of G2 alone is a viewer on every connection. When no mapping matches and oidc.default_role applies, and for administrators (admin mappings cannot be limited), it is every connection. The connections are recomputed with the role at every sign-in, take effect at once (sessions do not end), and cannot be changed by hand while mappings exist (409). Widening someone's connections is never an admin grant, so the two-person rule does not hold it.
While group mappings exist, the role of a single sign-on user is recomputed at every sign-in (a change ends their sessions, like PUT /api/v1/users/{id}/role) and changing it by hand is refused with 409; without mappings, oidc.default_role is only the role of new users and a role set by hand stays. Single sign-on users have no password: the password form never signs them in (it takes as long as for an unknown name) and changing or confirming their password answers 400, so the recovery kit needs a local administrator. While single sign-on is on, the last local administrator can be neither demoted nor deleted (409); with oidc.local_login admins_only, a local user without the admin role gets 403 from POST /api/v1/auth/login, only after the password matched.
Endpoints¶
| Method | Endpoint | Description | Success | Errors |
|---|---|---|---|---|
GET |
/api/v1/health |
Health check (status, version, time, scheduler, scheduler_last_tick, draining, and in HA mode role and ha); see health |
200 | 503 stale scheduler |
GET |
/api/v1/setup/status |
{"setup_required": bool} |
200 | |
POST |
/api/v1/setup |
{setup_code, username, password} → first user and session: {user, csrf_token} |
201 | 400, 403 wrong code, 409 already set up, 429 |
POST |
/api/v1/auth/login |
{username, password} → {user, csrf_token} |
200 | 401, 403 foreign origin or password sign-in limited to local admins, 415, 429 |
GET |
/api/v1/auth/methods |
Sign-in methods: {local: "all"\|"admins_only", oidc: {enabled, display_name, available}} (public) |
200 | |
POST |
/api/v1/auth/logout |
Revoke the current session → {logged_out, end_session_url} (end_session_url only for single sign-on users with oidc.rp_logout) |
200 | |
GET |
/api/v1/auth/me |
{user, csrf_token, auth: "session"\|"api_key", role, scope, key_scope, key_expires_at, connection_ids} (scope is the effective scope; key_scope only for API keys, key_expires_at only for keys with an expiry; connection_ids only for a caller limited to some connections); signed out: {user: null, csrf_token: "", auth: "", role: "", scope: ""} |
200 | |
GET |
/api/v1/auth/sessions |
Your own live sessions, most recently active first: [{id, user_id, username, created_at, last_seen_at, expires_at, current}]; ?all=true lists every user's (admin only). Never carries tokens or their hashes; an API key sees the sessions of the user who created it |
200 | 400, 403 |
DELETE |
/api/v1/auth/sessions/{id} |
Revoke a session → {revoked_id, current}: your own user's from a session of any role, any user's with admin; API keys below admin may revoke none. Revoking your own current session also clears the cookie |
200 | 403, 404 |
GET / POST |
/api/v1/users |
List users [{id, username, role, auth_provider, connection_ids, created_at, last_login_at}] (auth_provider: local or oidc; connection_ids absent for every connection) / create a local user {username, password, role, connection_ids} (role: viewer (default), operator or admin; connection_ids optional, not for admins); admin only |
200 / 201 | 400 (also an unknown connection or a limited admin), 403, 409 username taken |
GET |
/api/v1/users/names |
[{id, username}] of every user, to show who created or pinned something; read |
200 | |
PUT |
/api/v1/users/{id}/connections |
{all_connections, connection_ids} → the user: every connection, or exactly the list ([]: none); IDs of deleted connections the user had are dropped; applies to their sessions and API keys from the next request; admin only, no approval under the two-person rule |
200 | 400 neither field, unknown connection or a limit on an administrator, 403, 404, 409 a single sign-on user whose group mappings decide |
PUT |
/api/v1/users/{id}/role |
{role} → the user; ends their sessions (their API keys are capped by the new role); admin only |
200 | 400 unknown role or yourself, 403, 404, 409 last admin, last local admin while single sign-on is on, or a single sign-on user whose role the group mappings decide |
DELETE |
/api/v1/users/{id} |
Delete a user and revoke their sessions and API keys; admin only | 200 | 400 yourself, 403, 404, 409 last user, last admin, or last local admin while single sign-on is on |
PUT |
/api/v1/users/{id}/password |
{current_password, new_password}: your own from a session with the current password, another user's (no current password) with admin |
200 | 400 (also for single sign-on users, who have no password), 403 wrong current password, an API key for your own, or not admin, 404 |
GET / POST |
/api/v1/api-keys |
List keys (admins see all, others their own) with effective_scope, effective_connection_ids and expires_at / create {name, scope, all_connections, connection_ids, expires_at \| expires_in_days} (scope: read (default), operator or admin, at most your own; the access as for users, within your own, every connection with admin; the expiry optional unless a maximum lifetime is set) → {api_key, key} (plaintext only here) |
200 / 201 | 400 (also a limited admin key, an expiry in the past, missing or beyond the maximum lifetime), 403 scope above yours or a non-admin key, 404 a connection you cannot see |
DELETE |
/api/v1/api-keys/{id} |
Revoke a key: the keys your user created from a session of any role, only itself with an API key below admin, any key with admin | 200 | 403, 404 |
GET |
/api/v1/audit |
Recent API key activity (MCP calls and REST requests), newest first (?limit= 1-1000, default 200); admin only (details) |
200 | 400, 403 |
GET |
/api/v1/audit/events |
The audit log of every action, newest first, with filters and pages (details); admin only | 200 | 400, 403 |
GET |
/api/v1/audit/events/export |
The audit log as JSON Lines, oldest first, same filters; admin only | 200 | 400, 403 |
GET |
/api/v1/audit/events/verify |
Verify the audit log's hash chain and report the first broken entry; admin only | 200 | 403 |
GET / POST |
/api/v1/connections |
List / create {name, uri, description} |
200 / 201 | 400 |
GET / PUT |
/api/v1/connections/{id} |
Get / update a connection | 200 | 400, 404 |
DELETE |
/api/v1/connections/{id} |
Delete a connection | 200 | 404, 409 used by jobs |
POST |
/api/v1/connections/{id}/test |
Test a saved connection → {ok, server_version, latency_ms, error} |
200 | 404 |
POST |
/api/v1/connections/test |
Test an unsaved {uri} (with optional TLS fields and connection_id) |
200 | 400 |
GET |
/api/v1/connections/{id}/databases |
[{name, size_bytes, empty}]; admin, config, local only with ?system=true |
200 | 404, 502 unreachable |
GET |
/api/v1/connections/{id}/databases/{db}/collections |
[{name, type}] |
200 | 404, 502 |
GET |
/api/v1/stats |
Dashboard KPIs over every record: counts, failed_backups_24h, total_restores, last_backup, job_last_backups (details) |
200 | |
GET |
/api/v1/stats/history |
Outcomes and stored size per day, each job's recent runs, the next 24 hours' scheduled runs and failed verifications (?days= 1-366, default 30; ?tz_offset= minutes east of UTC) (details) |
200 | 400 |
GET |
/api/v1/schedule/preview |
Whether ?cron= is a valid schedule and its next ?n= (1-10, default 3) activations, as the scheduler computes them (details) |
200 | 400 |
GET |
/api/v1/readiness |
One row per database a job backs up: last good, verified and restore-tested backups, RPO, estimated RTO, escrowed keys and an ok / warn / fail status (details) |
200 | |
GET |
/api/v1/pitr/streams |
Every PITR stream with its status (details) | 200 | 503 |
POST |
/api/v1/pitr/streams |
Admin. Create the stream of a replica set connection | 201 | 400, 409 |
GET |
/api/v1/pitr/streams/{id} |
State, windows, lag, headroom, chains and base backups of a stream | 200 | 404 |
PATCH |
/api/v1/pitr/streams/{id} |
Admin. Change a stream: enable, disable, schedule, retention | 200 | 400, 404 |
DELETE |
/api/v1/pitr/streams/{id} |
Admin. Delete a disabled stream (see below) | 200 | 404, 409 |
GET |
/api/v1/pitr/streams/{id}/chunks |
Oplog chunk metadata, newest first (?limit= 1-500, default 100; ?offset=) |
200 | 400, 404 |
POST |
/api/v1/pitr/streams/{id}/base |
Operator. Take a base backup now | 202 | 404, 409 busy |
GET |
/api/v1/settings |
All settings, secrets masked: {general, security, encryption, integrity, metadata_backup, restart_required, warnings, pending_changes} |
200 | |
PUT |
/api/v1/settings |
Partial update, e.g. {"general": {...}}; returns the full settings, the pending_changes and, when a change waits for a second administrator, approvals_requested (delete protection) |
200 | 400, 409 two-person rule with fewer than two admins |
POST |
/api/v1/settings/encryption/generate-key |
New X25519 key pair {identity, recipient} (not stored) |
200 | |
POST |
/api/v1/settings/warnings/{id}/dismiss |
Dismiss a persistent warning for good; returns the remaining {warnings} |
200, 404 | |
POST |
/api/v1/settings/monitoring/test |
Send one test ping: {heartbeat_url} (optional; empty, "******" or the masked stored URL ping the stored monitoring.heartbeat_url) → {ok, host}; admin only, at most 5 per minute per user or API key |
200 | 400 invalid or masked URL, nothing configured; 429 too many test pings (Retry-After); 502 the monitor was unreachable or did not answer 2xx (a fixed reason such as connection refused, timeout, TLS error or DNS error, with the host only, never an address); 503 |
POST |
/api/v1/settings/oidc/test |
Fetch the provider's discovery document and keys: {issuer} (optional; the stored issuer when omitted) → {issuer, authorization_endpoint, token_endpoint, jwks_uri, end_session_endpoint, signing_algorithms, usable_algorithms, pkce_methods, keys, key_types}, never secrets; admin only |
200 | 400 invalid issuer, 404 desktop app, 502 provider unreachable or unusable |
GET |
/api/v1/metadata-backup |
Status of the metadata backups: last snapshot {target_id, target_name, key, created_at, size_bytes, encrypted}, last error, next run |
200 | 503 |
POST |
/api/v1/metadata-backup/run |
Take a metadata snapshot now, in the background (admin) | 202 | 409 already running, 503 |
GET |
/api/v1/recovery-kit |
When the last recovery kit was downloaded and whether it is still current | 200 | 503 |
POST |
/api/v1/recovery-kit |
Download the recovery kit {passphrase, current_password} as an age-encrypted tar (admin, signed-in users only) (details) |
200 | 400, 403, 429 |
GET / POST |
/api/v1/storage-targets |
List / create {name, type, local \| s3} (the first target becomes the default) |
200 / 201 | 400 |
GET / PUT |
/api/v1/storage-targets/{id} |
Get / update a target | 200 | 400, 404, 409 changed meanwhile, location locked, or new credentials of a target in use failed the connection test |
DELETE |
/api/v1/storage-targets/{id} |
Delete a target nothing references; with the two-person rule 202 with an approval request |
200, 202 | 404, 409 default or in use (by a job or any backup record but purged ones) |
POST |
/api/v1/storage-targets/{id}/test |
Test a saved target → {ok, latency_ms, error} |
200 | 404 |
POST |
/api/v1/storage-targets/test |
Test an unsaved target (plus id when editing, for its masked secret) |
200 | 400 |
POST |
/api/v1/storage-targets/{id}/default |
Make the target the default | 200 | 404 |
POST |
/api/v1/storage-targets/{id}/rotate-credentials |
Probe new S3 credentials {access_key_id, secret_access_key} (write, read, list, delete) and swap them in (key rotation) |
200 | 400, 404, 409, 422 |
GET |
/api/v1/security/key-rotation |
secret_key: {from_env, fingerprint, previous_key_kept} (key rotation) |
200 | |
POST |
/api/v1/security/rotate-secret-key |
Rotate secret.key {current_password} (admin, signed-in users only; everyone signs in again) |
200, 202 | 403, 409, 429 |
POST |
/api/v1/encryption/rotate |
Rotate the backup encryption key {passphrase?, reencrypt?} |
200 | 400, 409 |
GET |
/api/v1/encryption/reencryption |
The latest re-encryption job (null when none ran) |
200 | |
GET |
/api/v1/jobs |
One page of the scheduled jobs (sort name, next_run, last_status or created_at, order asc or desc; limit 1-500, default 100; after the previous page's meta.next); optional filters q, enabled, connection_id, database, schedule, last_status (details) |
200 | 400 |
POST |
/api/v1/jobs |
Create or update a job (connection_id and database or database_selection required, several databases; storage_target_id and parallelism optional; the cron expression is validated). A shorter retention of an existing job is answered as pending_retention (or approval) and applies later (delete protection); warnings names a retention shorter than the object lock of the job's storage target |
201 | 400 |
GET |
/api/v1/jobs/{id} |
Get a job with its next three activations (next_runs, UTC), its effective RPO (effective_rpo_minutes, rpo_default; see RPO) and a pending retention shortening (pending_retention) |
200 | 404 |
PUT |
/api/v1/jobs/{id} |
Update a job and reschedule it at once (details); a shorter retention is answered as pending_retention (or approval) and applies after the grace period |
200 | 400, 404, 409 changed meanwhile |
DELETE |
/api/v1/jobs/{id} |
Delete a job | 200 | 404 |
POST |
/api/v1/jobs/{id}/run |
Run a job now: the body is the backup record, or for a job with several databases the run (status: "running"), whose databases are resolved and backed up in the background |
202 | 400, 404, 409 |
POST |
/api/v1/jobs/{id}/cancel |
Stop the job's current run: the running database and those still waiting (details) | 200, 202 still stopping | 404, 409 not running |
GET |
/api/v1/jobs/{id}/runs |
The job's runs, newest first, each with the outcome of every database (?limit= ≤ 200) |
200 | 404 |
GET |
/api/v1/jobs/{id}/databases/preview |
The databases the job's selection backs up now, the excluded ones with reasons and the new ones ({included, excluded, missing, new_since_last_run, warnings}; query parameters preview another selection) (details) |
200 | 400, 404, 502 |
GET |
/api/v1/jobs/databases/preview |
The same for an unsaved job (connection_id, mode, databases, include, exclude, auto_include_new) |
200 | 400, 404, 502 |
GET |
/api/v1/backups |
List backups, newest first; filters, sorting and pagination (details) | 200 | 400 |
GET |
/api/v1/backups/databases |
Distinct database names of all backups, sorted (for filters) | 200 | |
POST |
/api/v1/backups |
Start a backup {connection_id, database, collections \| exclude_collections, storage_target_id, gzip, include_users_and_roles} (users and roles), or one run of several with databases and parallelism instead of database (details) |
202 | 400, 409 |
DELETE |
/api/v1/backups/{id} |
Delete a backup softly, with an optional ?reason= → {deleted_id, status: "deleted", deleted_at, purge_after, archive_deleted: false}. The archive stays until purge_after (the delete grace period) and the deletion can be undone until then; with the two-person rule 202 with an approval request (delete protection) |
200, 202 | 404, 409 pinned, running or already deleted |
POST |
/api/v1/backups/{id}/undelete |
Undo the deletion during the grace period: the backup gets back its earlier status (admin) | 200 | 403, 404, 409 not deleted or already purged |
POST |
/api/v1/backups/bulk |
Run one action on many backups (delete, verify, pin, unpin, cancel), with a dry run (details) |
200 | 400, 403, 409 confirm_count, 422 too many |
POST |
/api/v1/restores/bulk |
Delete restore history records or cancel restores in bulk (details) | 200 | 400, 403, 409, 422 |
POST |
/api/v1/jobs/bulk |
Enable, disable, run or delete jobs in bulk (details) | 200 | 400, 403, 409, 422 |
GET |
/api/v1/bulk/actions |
The available bulk actions and whether the caller may run each | 200 | |
POST |
/api/v1/backups/{id}/verify |
Re-read the archive and compare it with its checksum, in the background; poll the backup for verified_at (verification) |
202 | 404, 409 not completed or already running |
POST |
/api/v1/backups/{id}/pin |
Pin (legal hold) with an optional {note}: retention and deletion skip it |
200 | 400, 404 |
POST |
/api/v1/backups/{id}/unpin |
Lift the pin (admin: it makes the backup deletable again); with the two-person rule 202 with an approval request |
200, 202 | 403, 404 |
GET |
/api/v1/approvals |
Requests of the two-person rule, newest first (?status= pending, approved, failed, rejected or expired); admin |
200 | 400, 403 |
GET |
/api/v1/approvals/{id} |
One request; admin | 200 | 403, 404 |
POST |
/api/v1/approvals/{id}/approve |
Approve and run the request: an administrator signed in to the dashboard, not the requester; API keys never | 200 | 403 API key, requester or not admin, 404, 409 decided or expired |
POST |
/api/v1/approvals/{id}/reject |
Reject the request, with an optional {reason} (admin, API keys and the requester included) |
200 | 403, 404, 409 decided or expired |
GET |
/api/v1/pending-changes |
Lowered protections that take effect later: [{id, kind: "retention"\|"delete_grace_days", job_id, retention_days, retention_count, delete_grace_days, requested_by, approved_by, created_at, effective_at}] |
200 | |
DELETE |
/api/v1/pending-changes/{id} |
Cancel a pending change; the current protection stays (admin) | 200 | 403, 404 |
GET |
/api/v1/jobs/{id}/retention/preview |
Backups the retention policy would delete now and why (?retention_days=, ?retention_count= preview other values) |
200 | 400, 404 |
GET |
/api/v1/jobs/{id}/retention/log |
Backups the job's retention deleted, newest first (?limit= ≤ 200) |
200 | 404 |
POST |
/api/v1/jobs/{id}/restore-test |
Run a restore test of the job's latest backup now, in the background → {job_id, backup_id} |
202 | 404, 409 no backup or already running, 503 |
GET |
/api/v1/jobs/{id}/restore-tests |
Restore test results, newest first (?limit= ≤ 100) |
200 | 404 |
GET |
/api/v1/integrity |
Integrity sweep status, the latest scan of every storage target, the next scheduled scan and the integrity work running | 200 | |
POST |
/api/v1/integrity/sweep |
Start an integrity sweep now (admin) | 202 | 409 already running |
GET / POST |
/api/v1/storage-targets/{id}/scan |
Latest / a new storage scan: orphan and missing archives (POST admin) |
200 | 404 |
POST |
/api/v1/storage-targets/{id}/import |
Create (or revive the failed, pruned or purged record of) the orphan archive {key}, hashed in the background (admin); the archive of a deleted backup waiting for its purge is not an orphan |
202 | 400 no valid database in the key, 404, 409 not an orphan or already importing |
POST |
/api/v1/backups/{id}/retry |
Retry a failed backup with its parameters; the new record's retry_of is {id} (details) |
202 | 404, 409 not failed or already running, 422 connection or target gone |
GET |
/api/v1/backups/{id}/collections |
Collections stored in the backup, read from its archive header (details) | 200 | 404, 422 key missing |
POST |
/api/v1/backups/{id}/cancel |
Cancel a running backup (details) | 200, 202 still stopping | 404, 409 not running or finishing |
GET |
/api/v1/backups/{id}/log |
The backup's run log as text/plain: the last N lines with ?tail=N (1-10000), otherwise the whole file as a download (details) |
200 | 400, 404 |
POST |
/api/v1/restore |
Restore (safe clone by default; optional selected_collections, target_connection_id (admin), verify, restore_users_and_roles (in place only, users and roles)) |
202 | 400, 403, 404, 409, 422 |
GET |
/api/v1/restores |
Restore audit history, newest first; filters, sorting and pagination (details) | 200 | 400 |
GET |
/api/v1/restores/databases |
Distinct target databases of all restores, sorted | 200 | |
POST |
/api/v1/restores/{id}/cancel |
Cancel a running restore; an in-place one needs admin (details) | 200, 202 still stopping | 403, 404, 409 not running or finishing |
POST |
/api/v1/restores/{id}/drop-clones |
Drop the clones a restore kept after a failed post-restore command; admin (details) | 200 | 400, 403, 404 |
GET |
/api/v1/restores/{id}/log |
The restore's run log (like the backup log) | 200 | 400, 404 |
GET |
/api/v1/runs/active |
Live progress of every running backup and restore (details) | 200 | |
GET / POST |
/api/v1/notifications/channels |
List / create notification channels | 200 / 201 | 400 |
PUT / DELETE |
/api/v1/notifications/channels/{id} |
Update / delete a channel | 200 | 400, 404 |
POST |
/api/v1/notifications/channels/{id}/test |
Send a test notification | 200 | 404 |
GET / POST |
/api/v1/notifications/rules |
List / create notification rules | 200 / 201 | 400 |
PUT / DELETE |
/api/v1/notifications/rules/{id} |
Update / delete a rule | 200 | 400, 404 |
GET |
/metrics |
Prometheus metrics | 200 | 401 |
POST |
/mcp |
MCP Streamable HTTP endpoint (JSON-RPC; stateless, so GET and DELETE answer 405); API keys only |
200 | 401, 403 disabled or foreign origin |
Every protected endpoint also answers 401 without valid credentials, 403 for a cookie request with a missing or wrong X-CSRF-Token and 403 for an API key whose scope is too small.
Database and collection names sent to create or update a job, start a backup, or name an in-place restore target and selected collections must follow MongoDB's cross-platform rules: a database name has 1 to 63 bytes, none of / \ . " $, a space or a control character, and does not start with -; a collection name is not empty and has no $ or control character. Other names are refused with 400 (invalid namespace). Scheduled runs of existing jobs, retries and restores of existing backups are never refused for their names. Backups are always stored under a key the server derives from the database name; clients cannot choose it.
Connections¶
Connection strings are validated, encrypted at rest and only ever returned redacted (mongodb://user:******@host/...). To keep the stored password when editing, send the URI back exactly as it was returned; any other value containing ****** is rejected. A connection test succeeds or fails with HTTP 200 (ok: false plus a redacted error) and times out after 10 seconds.
A connection may also set read_preference (primary, primaryPreferred, secondary, secondaryPreferred, nearest; empty keeps the URI's own) with read_preference_tags (a list of tag sets such as [{"dc": "east", "use": "backup"}, {}]), and max_concurrent_backups (0 = unlimited, at most 64); an update without them keeps them. A test (POST /api/v1/connections/{id}/test, or POST /api/v1/connections/test with read_preference and read_preference_tags next to uri) also reports read_member (host, state: primary, secondary, standalone, mongos or other, set_name) with the read_preference it used, or read_member_error when no member matches. See Read preferences, throttling and backup windows.
A connection may also carry TLS material: tls_ca_pem, tls_client_cert_pem, tls_client_key_pem, tls_client_key_password, tls_allow_invalid_hostnames and tls_insecure (which needs "tls_insecure_confirm": true when it is turned on). The key and its password are sealed and returned as ******; send ****** back to keep them, omit a field to keep it and send "" to remove it. POST /api/v1/connections/test takes the same fields, and with connection_id it applies them to the stored connection's. The material is checked on save (400 with the field at fault), and a URI that sets its own TLS file or verification options is refused alongside it. See connections.md.
Administrators may also set post_restore_commands, the MongoDB commands every safe-clone restore into the connection runs against its clones before it completes (post-restore commands); an update without the field keeps them and [] removes them. Only administrators see them: GET answers other callers without the field. They are stored sealed with secret.key, like the URI. Every PUT is audited with post_restore_commands_changed and, when they changed, post_restore_commands_from and post_restore_commands_to (the counts, never the contents). While the two-person rule is on, a PUT that removes or changes any existing command (which could bring erased data back after a restore) saves the other fields, keeps the commands and answers 202 with an approval request (reduce_post_restore_commands, the new list held sealed); adding commands needs no approval.
Jobs and manual backups name a connection_id. Backup records keep connection_id and a connection_name snapshot. A restore goes to the backup's connection unless target_connection_id names another one, which restores across servers and needs admin; restore records always carry source_connection_id, source_connection_name, target_connection_id and target_connection_name, the names as they were when the restore started.
Settings¶
GET /api/v1/settings returns every setting grouped as general, security and encryption (descriptions), plus restart_required (always empty: every change applies to the next operation or request), pending_changes (lowered protections that take effect later, see delete protection) and warnings, the persistent notices the dashboard shows as a banner: [{id, message, setting}]. The warnings are encryption_off_after_upgrade (see encryption.md), metadata_backup_unencrypted (metadata backups are on but encryption is off; it cannot be dismissed) and recovery_kit_missing (no recovery kit was downloaded since secret.key, the encryption keys or the storage targets last changed; dismissing it hides it until they change again). Durations are Go duration strings ("6h0m0s"), secrets (encryption.identity, encryption.passphrase) are "******" when set and "" when not, and encryption.retired_keys lists {kind, recipient, retired_at} without key material.
PUT /api/v1/settings takes one or more groups with only the fields to change and answers with the full settings. Unknown fields, invalid values and malformed durations are rejected with 400 and nothing is changed. A lower security.delete_grace_days, turning security.require_locked_copies off and turning security.require_second_approver off are not applied by the request: the first two become pending changes (or, with the two-person rule, approval requests), the last always an approval request; see delete protection. Sending "******" for a secret keeps the stored value; "" removes it. A replaced or removed identity or passphrase is moved to retired_keys, so backups encrypted with it stay restorable.
curl -X PUT http://localhost:8080/api/v1/settings -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d '{"general": {"backup_timeout": "3h", "default_retention_days": 14}}'
The oidc group configures single sign-on (fields); client_secret is stored encrypted and shown as "******". Turning enabled on, or local_login to admins_only, needs a local administrator, and turning single sign-on on (or changing the issuer while it is on) fetches the provider's discovery document and keys first; a failure answers 400 and changes nothing. API key expiry adds api_keys_expiring (keys that expire within security.api_key_expiry_warning_days or have expired, each in api_keys: [{id, name, prefix, expires_at}]; a non-admin sees only their own keys) and api_key_never_expires (a key imported from MONGORESCUE_API_KEY while a maximum lifetime is set); neither can be dismissed. Single sign-on adds the warning oidc_role_kept (a sign-in would have demoted the last administrator, or an administrator while the two-person rule is on, so the stored role was kept; under the rule the demotion waits as an approval request).
The audit group holds retention_days (default 365, 30 to 36500), webhook_url and webhook_secret (audit.md). Both are stored encrypted; webhook_url is shown only up to its host ("https://siem.example.com/******", a bare origin unchanged) and webhook_secret as "******". Sending either back as shown keeps the stored value, "" removes it. A webhook_url with another host needs webhook_secret again (400 when it is masked or omitted), like oidc.client_secret when oidc.issuer changes.
The monitoring group holds heartbeat_url and heartbeat_interval (default 5m, 1m to 1h; see monitoring.md). heartbeat_url is stored encrypted and shown only up to its host ("https://hc-ping.com/******"); sending it back as shown, or "******", keeps it, "" turns the heartbeat off. A masked value with another host is refused with 400: the full URL must be entered again. Test the URL with POST /api/v1/settings/monitoring/test.
To encrypt new backups with a fresh key pair, call POST /api/v1/settings/encryption/generate-key, store the returned identity safely, and send {"encryption": {"enabled": true, "mode": "x25519", "recipients": ["<recipient>"], "identity": "<identity>"}}. The passphrase of passphrase mode needs at least 16 characters.
Storage targets¶
A target is {id, name, type: "local"|"s3", is_default, local: {path}, s3: {endpoint, region, bucket, prefix, access_key_id, secret_access_key, use_path_style, part_size_mb, object_lock, retention_days, legal_hold_on_pin}, created_at, updated_at, last_test_at, last_test_ok, last_test_error, hints}. object_lock is none, governance or compliance with retention_days 1 to 3650 and optional legal_hold_on_pin (immutable backups); saving or testing a locked target is refused (400, or a test with ok: false) unless the bucket has Object Lock and versioning enabled. An update that lowers the lock (a weaker mode, none, a shorter retention or the legal hold turned off) keeps the current lock and answers pending_object_lock (applied after the delete grace period) or, with the two-person rule, approval; raising it applies at once. hints (responses only) lists {level: "info"|"warn", code, message}: no_object_lock, governance_bypass or local_not_immutable. Create and update bodies contain only the sub-object of their type; the default is changed only with POST /api/v1/storage-targets/{id}/default. The secret access key is returned as "******"; sending it back keeps the stored key only while endpoint, bucket and access_key_id are unchanged. part_size_mb is the multipart part size, 5 to 512 MiB (default 16 when omitted); it caps one archive at part size × 10,000 (see S3 multipart limits). Tests answer 200 with ok: false and an error when the probe fails. local.path must be absolute and must not be /, the data directory or inside it. Updates are refused with 409 when the target changed meanwhile, when they would move a target that backup records reference (type, path, endpoint, bucket or prefix; every record but purged ones counts, deleted backups waiting for their purge included), and when new credentials, region or path style of such a target fail a connection test; create a new target instead of moving one.
Every signed-in user, and every API key with the admin scope, has full rights, including settings, storage targets and the connection and storage test endpoints, which connect to hosts named in the request. Roles for users are on the roadmap.
Jobs and manual backups take an optional storage_target_id (the default target when omitted). Backup records carry storage_target_id and a storage_target_name snapshot; restores, deletions and retention use the record's target. Deleting the default target, or a target still used by a job or referenced by any backup record but purged ones, answers 409 with the reason.
Metadata backups and the recovery kit¶
The metadata_backup settings group (enabled, interval, target_id, retention_count; see configuration.md) schedules snapshots of mongorescue.db. POST /api/v1/metadata-backup/run takes one now (202, 409 while one runs); GET /api/v1/metadata-backup reports {enabled, prefix, running, last_run_at, last_trigger, last_error, retention_error, last, next_run_at}; prefix is _mongorescue/metadata/<install_id>/, this installation's own prefix (derived from secret.key), and last carries install_id. A failed snapshot publishes metadata_backup.failed. The runbook is in production.md.
POST /api/v1/recovery-kit returns the recovery kit as application/octet-stream (Content-Disposition: attachment; filename=mongorescue-recovery-kit-<date>.tar.age, Cache-Control: no-store). The body is {"passphrase": "...", "current_password": "..."}:
- only a signed-in user may call it, with the session cookie and
X-CSRF-Token; API keys get403whatever their scope; current_passwordmust be the user's password (403otherwise; wrong passwords count against the same throttle as password changes, then429withRetry-After);passphraseseals the kit with age scrypt and needs at least 12 characters (400). It is not stored.include_previous_key(optional) addssecret.key.previous, the key the lastsecret.keyrotation replaced, which opens metadata snapshots taken before it (409when there is none).plaintext_secret_key(optional) only matters with key wrapping on: the kit then carriessecret.key.wrapped(andsecret.key.previous.wrapped) with the KMS key URI inrecovery.json(secret_key.wrapped,secret_key.key_wrap), and recovering needs access to the KMS key;trueputs the plaintext keys into the kit instead. The audit entry of such a download recordsplaintext: true. Under the two-person rule the first such request answers202with an approval request (plaintext_recovery_kit); once another administrator approved it, the requester repeats the request with"approval_id": "<id>"within an hour, once (403for a request that is not approved, not theirs, too old or already used). The response always carriesCache-Control: no-store.409whilesecret.key.wrappeddoes not hold the running key (a rotation being installed).
The kit is a tar archive with README.txt (the recovery steps), secret.key, recovery.json (encryption settings, every storage target with its credentials, the snapshot prefix metadata_prefix and the latest metadata snapshot) and identities.txt (the age private keys, only when the server holds one; in recipient-only mode the README points to your own key file instead). Passphrases are never included. Open it with age -d -o kit.tar mongorescue-recovery-kit-<date>.tar.age && tar -xf kit.tar. Every download and refused attempt is written to the audit log (POST /api/v1/recovery-kit, the user, the result), never the passphrase, the password or the content. GET /api/v1/recovery-kit returns {downloaded_at, up_to_date, min_passphrase_length, key_wrapped}.
Key rotation¶
See the runbook in encryption.md.
POST /api/v1/security/rotate-secret-keyneeds a signed-in administrator, the session cookie,X-CSRF-Tokenand{"current_password": "..."}(API keys403, a wrong password403, then429). With the two-person rule it answers202with the approval request; the rotation runs when a second administrator approves it. A key fromMONGORESCUE_SECRET_KEYanswers409, as does a rotation already running or an earlier one a restart must settle. On success it answers{old_fingerprint, new_fingerprint, resealed, sessions_revoked, skipped, rotated_at, sign_in_again: true}; every session, the caller's included, has ended.skippednames (table|id|field) stored secrets that could not be opened (damaged or planted, see unreadable records): they are left as they were, do not block the rotation, are listed in the event and the audit entry (skipped), and must be entered again.POST /api/v1/encryption/rotate(admin) answers{mode, old_fingerprint, new_fingerprint, recipients, reencryption}. X25519 keys are named by their public keys; passphrases by"passphrase". With"reencrypt": truethe answer carries the job{id, status, total, done, skipped, failed, errors, ...}, also returned byGET /api/v1/encryption/reencryption.POST /api/v1/storage-targets/{id}/rotate-credentials(admin) answers{target, steps: [{name, ok, error}], old_access_key_id, new_access_key_id}. A failed probe answers422with the same document indataand changes nothing; a local target, missing fields or unchanged credentials answer400.
When the database was re-sealed but the new key file could not be installed, the endpoint answers 500 with that explanation: the audit entry already carries the fingerprints, and the next start installs secret.key.next and publishes the event then (also for a rotation a crash interrupted after its commit; the actor and approval come from the rotation marker, the audit log gets a keyrotation.completed system entry). Each successful rotation publishes security.key_rotated (action: secret_key, encryption or storage_credentials; detail: the old and new fingerprints; approval_id) and annotates its audit entry with key_rotated, old_fingerprint and new_fingerprint.
Restores¶
Restore into a safe clone (the default, so safe_clone may be omitted):
curl -s -X POST http://localhost:8080/api/v1/restore \
-H "Authorization: Bearer $MONGORESCUE_KEY" -H 'Content-Type: application/json' \
-d '{"backup_id": "bkp_shop_20260924_030000_3f9a1c2e"}'
A safe clone is a new database named <db>_rescue_<YYYYMMDD_HHMMSS>_<id>: the source database, the start of the restore in UTC and a random 4-character lowercase hex ID (from a cryptographic random source), for example shop_rescue_20261002_120000_3f9a. The ID keeps two restores of one database started in the same second apart. A name may not exceed MongoDB's 63 bytes, so the source part of a database longer than 35 bytes is shortened (at a character boundary). Anything matching _rescue_ still matches every clone. The restore record's target_database names the clone; the preflight of POST /api/v1/restore checks that very name.
Restoring in place (into the source database, or into target_database) must be confirmed explicitly with {"safe_clone": false, "confirm_in_place": true}; any other in-place request is rejected with 400 Bad Request before mongorestore starts. In-place restores are always verified first (verify and the policy apply to safe clones only). A missing decryption key is rejected up front with 422 Unprocessable Entity; a checksum mismatch or failed decryption found during verification marks the restore record as failed, and mongorestore is never started. Two restores into the same target database cannot run at once (409); a dry run ("dry_run": true) writes nothing, so it takes no lock on the target and runs alongside them.
A backup with copies can be restored from any target that holds it: source_target_id names the primary target or the target of a done copy (anything else is 400). Without it the restore reads the primary, and falls back to the first healthy copy when the primary archive is recorded missing, failed its last verification, or is missing or has another size on its target. The restore record then has source_target_id, source_target_name and source_fallback (why it read a copy), and the preflight adds a source check: pass when the chosen archive is there, warn with the reason after a fallback, fail when the archive is missing and no healthy copy exists.
curl -s -X POST http://localhost:8080/api/v1/restore \
-H "Authorization: Bearer $MONGORESCUE_KEY" -H 'Content-Type: application/json' \
-d '{"backup_id": "bkp_shop_20260924_030000_3f9a1c2e", "source_target_id": "stg_offsite"}'
Restore preflight¶
POST /api/v1/restores/preflight (operator scope) takes the body of a restore request and answers the go/no-go summary without starting anything:
{
"ok": false,
"checks": [
{"id": "connection", "status": "pass", "message": "connected to prod (MongoDB 7.0.14)"},
{"id": "encryption", "status": "pass", "message": "the backup is not encrypted"},
{"id": "server_version", "status": "warn", "message": "the backup's server version is unknown (backups of earlier releases do not record it); the target runs MongoDB 7.0.14"},
{"id": "target_database", "status": "pass", "message": "restores into the new database shop_rescue_<YYYYMMDD_HHMMSS>_<id>; existing data is untouched"},
{"id": "privileges", "status": "pass", "message": "the user of connection prod may restore into shop_rescue_<YYYYMMDD_HHMMSS>_<id>"},
{"id": "disk_space", "status": "fail", "message": "the target has 120.0 MiB free, less than the 2.1 GiB archive"},
{"id": "collections", "status": "pass", "message": "a safe clone restores into a new database; no existing collection is replaced"},
{"id": "users_and_roles", "status": "pass", "message": "users and roles are not restored"}
]
}
status is pass, warn or fail; ok is false when a check failed. A safe clone is named when its restore starts, so this endpoint names it by its pattern (shop_rescue_<YYYYMMDD_HHMMSS>_<id>, also in post_restore); the checks stored on a started restore name its clone. The checks, in this order:
| ID | Checks |
|---|---|
connection |
The target server answers (fail otherwise; the server checks below are then warn "not checked"). |
encryption |
An encrypted backup has a decryption key configured. |
server_version |
The target's MongoDB version against the version recorded on the backup (server_version, read with buildInfo while the manifest is captured; backups of earlier releases have none and warn "unknown"). Any difference only warns (mongorestore may still work across versions); this check never fails. |
target_database |
Whether the target database exists; for a safe clone the clone name must be free. |
privileges |
The connection's user holds createCollection, createIndex and insert on the target database (connectionStatus), or on each restored collection through collection-level grants, plus dropCollection for an in-place restore with drop_target. It fails only when the actions are certainly missing (built-in roles only, every privilege understood); with custom roles, partial grants or an unreadable connectionStatus it warns. Missing user administration actions for restore_users_and_roles only warn. |
disk_space |
The archive size against the free space of the target server's data filesystem, from dbStats (fsTotalSize - fsUsedSize) or, for a server on the same host, from the file system of its dbPath. Less free space than the archive fails only when dbStats reported it and the restore is not in place with drop_target (which frees the space of what it replaces); the local value (a loopback address may be an SSH tunnel or a Docker host network) only warns. Less than twice (uncompressed) or four times (compressed or unknown) the archive warns; unknown free space warns. |
collections |
For an in-place restore, the existing collections the restore writes into (from the selection, the backup's manifest or its collection filter, against listCollections): dropped and replaced with drop_target, otherwise documents are added. Listed as a warning. |
users_and_roles |
restore_users_and_roles against the backup (fails where POST /api/v1/restore answers 400); a valid request warns that the database's users and roles are replaced. |
post_restore |
Only when the target connection has post-restore commands: the commands the restore runs, per clone (delete on users in shop_rescue_20261002_120000_3f9a), also listed in post_restore of the result with status: "planned". Passes for a safe clone and a dry run (which runs none), warns for an in-place restore (which runs none), fails when a stored command is no longer valid. Point-in-time preflights list them too. |
Only these can fail, and so refuse a restore: connection (the target is unreachable), encryption (no decryption key, which POST /api/v1/restore already answered with 422), target_database (the safe clone name exists), privileges (write actions certainly missing), disk_space (free space reported by dbStats smaller than the archive), users_and_roles (a request POST /api/v1/restore already answered with 400) and post_restore (a stored command that is no longer valid). Everything else warns. The server checks share one connection to the target and are bounded by 15 seconds.
A preflight applies the scope rules of the restore it checks: an in-place or cross-connection preflight needs admin. confirm_in_place is not needed to ask (it is still needed to start the restore). Dry runs need neither privileges nor space.
POST /api/v1/restore runs the same checks before it starts and stores them on the restore record (preflight). A failed check refuses the restore with 409 Conflict, the checks in data and the failed ones in error; send "force": true to restore anyway (the record then has "forced": true). Warnings never block, and dry runs are never refused. The dashboard runs the preflight while the restore dialog's options change and shows Restore anyway when a check failed.
Restore verification¶
"verify_restore": true compares the restored database with the manifest the backup captured (per collection: the document count range and the indexes) once the restore completed, like an automated restore test does with a temporary database. It is off when omitted, so API clients of earlier releases are unchanged; the dashboard turns it on by default. Only the restored collections are compared: the selection of a selective restore, and for an in-place restore only the collections in the backup (others in the target are ignored). An in-place restore without drop_target adds documents to the ones already there, so a larger count is a note, not a mismatch. The result is stored on the record:
"verification": {
"status": "failed",
"mismatches": ["collection orders: 9 documents restored, 10 expected"],
"notes": [],
"collections": 2,
"checked_at": "2026-10-02T12:00:41Z"
}
status is passed, failed or skipped (a dry run, a backup without a manifest, a target that could not be inspected; notes says why). A PITR chain test compares exact counts and adds unverified, the number of collections whose count could not be checked (with a note each), unverified_reason (the first reason) and unverified_old_base (the newer base predates exact counts); such a pass is not clean: readiness warns with pitr_chain_test_unverified. A failed verification keeps the restore completed, adds a warning to it and publishes restore.verification_failed (selectable in notification rules): the data is applied, but it does not match what the backup recorded. The dashboard shows the result in the restore details.
Post-restore commands¶
A connection's post_restore_commands (admin, POST/PUT /api/v1/connections, or Post-restore commands in the dashboard's connection form) are MongoDB commands that every safe-clone restore into the connection (a restore, a point-in-time restore or a restore test) runs against the databases it created, before it is reported complete. The use case is re-applying erasures to restored data (privacy.md):
"post_restore_commands": [
{"database": "*", "command": {"delete": "users", "deletes": [{"q": {"_id": {"$in": [1042, 1077]}}, "limit": 0}]}},
{"database": "shop", "command": {"update": "orders", "updates": [{"q": {"customer_id": 1042}, "u": {"$unset": {"address": ""}}, "multi": true}]}}
]
databaseis a source database: the command runs in that database's clone, and not at all when the restore did not restore it."*"runs it in every clone the restore created. A command never runs in the database it names, in a source database, or inadmin,configorlocal: every target must be a clone the restore created, or nothing runs and the restore fails.commandis a command document in (extended) JSON, sent withrunCommand; its first key is the command. Allowed:delete,update,findAndModify,dropIndexes,collModanddrop, on one collection (not asystem.*one). Refused anywhere in the document:$where,$function,$accumulator(server-side JavaScript),$out,$merge,$lookup,$graphLookup,$unionWith(other collections or databases) and$db; no top-level key may start with$and no key may repeat in one object. Commands such aseval,applyOps,aggregate,renameCollectionanddropDatabaseare refused with the reason. Upserts are refused (upsertother thanfalsein anupdateentry or afindAndModify): the commands only remove or change restored data, never insert. Integers beyond ±2^53 must be written as{"$numberLong": "…"}, so no client rounds an_id. At most 100 commands of 512 KiB together.- The commands run in order, per command in every clone it applies to. Dropping a collection or an index that does not exist succeeds, so a list can be re-applied any number of times.
- A restore runs its commands over one connection to the target, each bounded by
general.post_restore_command_timeout(default60s, 1s to 24h); a deferred run (afterverify_restore) is bounded bygeneral.restore_timeoutas well. - A command that fails, times out or cannot run fails the restore (
error_messagenames the command, its collection and clone, and the server's error code and code name only: server messages can quote document values, such as the key of a duplicate key error) and stops the rest. The clones are kept for inspection, not dropped: they hold the restored data without every command applied (erasures included), so do not use them. Until they are dropped, every settings response carries apost_restore_clones_keptwarning per restore (restore_id,databases) and the dashboard shows them with a Drop clones button;POST /api/v1/restores/{id}/drop-clones(admin) drops exactly the recorded clones of that restore (never its source or another database), recordsclones_dropped_atandclones_dropped_by, and is audited. - In-place restores and dry runs run none: the record's
post_restoresays so, and an in-place restore carries a warning to re-apply them by hand. Withverify_restore, the clone is compared with the manifest first, then the commands run. Restore tests compare first too, then run the commands as a rehearsal (a failure fails the test; the temporary database is dropped as always). PITR chain tests run none.
The restore record shows what ran, with names and counts only, never the command documents or the documents they touched:
"post_restore": {
"status": "completed",
"commands": [
{"index": 0, "database": "shop_rescue_20261002_120000_3f9a", "source_database": "shop", "command": "delete", "collection": "users",
"status": "ok", "n": 2, "duration_ms": 14},
{"index": 1, "database": "shop_rescue_20261002_120000_3f9a", "source_database": "shop", "command": "update", "collection": "orders",
"status": "ok", "n": 5, "modified": 5, "duration_ms": 9}
]
}
status is completed, failed (with note and clones_kept) or skipped (dry runs and in-place restores, with note); each command is ok, failed (with a redacted error; write errors give their codes only, since their messages can quote document values), not_run (after a failure) or planned (dry runs). n counts the documents a delete removed or an update or findAndModify matched, modified and upserted what an update changed. Every command that ran is also written to the audit log as SYSTEM restore.post_restore_command, and its counts to the run log.
Selective restores¶
selected_collections restores only the named collections of the backup (one --nsInclude each, with * and \ escaped, so a name always means exactly that collection); omitted or empty, the whole database is restored. The restore record repeats the selection in selected_collections. With drop_target, mongorestore drops each collection right before restoring it, so only the selected collections are dropped in the target; its other collections are kept. A view is restored from its definition and reads from its source collection (view_on), which is not restored with it unless it is selected too.
curl -s -X POST http://localhost:8080/api/v1/restore \
-H "Authorization: Bearer $MONGORESCUE_KEY" -H 'Content-Type: application/json' \
-d '{"backup_id": "bkp_shop_20260924_030000_3f9a1c2e", "selected_collections": ["orders", "customers"]}'
GET /api/v1/backups/{id}/collections (read scope) lists what the backup holds, to choose from:
{
"backup_id": "bkp_shop_20260924_030000_3f9a1c2e",
"database": "shop",
"source": "archive",
"collections": [
{"name": "big_orders", "type": "view", "view_on": "orders"},
{"name": "metrics", "type": "timeseries"},
{"name": "orders", "type": "collection"}
]
}
The list is read from the prelude of the mongodump archive only: the artifact is streamed from its storage target, decrypted and decompressed on the fly, and the download stops where the prelude ends, so the data of the backup is never transferred. At most 64 MiB of decompressed prelude are read, within 30 seconds, and at most four archives at a time; lists of completed backups are cached in memory per backup. type is collection, view or timeseries (system collections are listed too; the dashboard hides them). size_bytes appears when the archive records a size (current mongodump versions do not); document counts are not part of the archive header.
When the archive cannot be read (an artifact that is missing, damaged or not a mongodump archive, a timeout) or the backup failed or was pruned (its archive is not read then), source is record, warning gives the reason and collections is the backup's own collection filter (empty for a whole-database backup). An encrypted backup without its key is answered the same way when the record has a filter, with the key hint in warning; without one, the response is 422 with the same hint as a restore.
Users and roles¶
Backups do not include the users and roles defined on a database unless asked to. A job (POST /api/v1/jobs, PUT /api/v1/jobs/{id}) or an on-demand backup (POST /api/v1/backups) with "include_users_and_roles": true (default false, returned by GET /api/v1/jobs/{id}) runs mongodump --db=<db> --dumpDbUsersAndRoles, once per database for a job with several databases. The backup record then says "users_and_roles": true; a retry of a failed backup keeps the option. mongodump refuses --collection with this flag, so a collection filter of such a backup is passed as exclusions of the other collections. Without a database (database empty, which mongodump would dump whole), the option is ignored, since mongodump only dumps users and roles together with --db. The admin database is the exception: it stores every user and role of the server as its own collections (admin.system.users, admin.system.roles), so its backups contain them anyway, and a single-database job or backup of admin with include_users_and_roles is refused with 400. List, all and pattern selections never include admin.
A restore with "restore_users_and_roles": true runs mongorestore --db=<db> --restoreDbUsersAndRoles and records "users_and_roles": true on the restore. mongorestore restores the users and roles under the name of the database they were dumped from, so the option is allowed only for an in-place restore into the backup's own database ("safe_clone": false, "confirm_in_place": true, no target_database or the source database's name). Combined with a safe clone or a target_database that renames the restore, it is rejected with 400; it is rejected with 400 as well when the backup was taken without users and roles, or is a backup of admin. The users and roles of the target database are replaced by the ones in the backup: users and roles created since the backup are removed, in full even when selected_collections restores only some collections. With target_connection_id, an in-place restore on another server restores them there, into the database of the same name. Restoring them needs a connection whose user may manage users and roles (for example userAdminAnyDatabase or root).
curl -s -X POST http://localhost:8080/api/v1/restore \
-H "Authorization: Bearer $MONGORESCUE_KEY" -H 'Content-Type: application/json' \
-d '{"backup_id": "bkp_shop_20260924_030000_3f9a1c2e", "safe_clone": false, "confirm_in_place": true, "restore_users_and_roles": true}'
The MCP tools do not take these options: restore_to_safe_clone only restores into a safe clone, and jobs are created through the REST API or the dashboard (get_job and the job resources show include_users_and_roles).
Updating a job¶
PUT /api/v1/jobs/{id} (admin scope, like creating a job) replaces a job's name, cron_expression, database, collections, exclude_collections, connection_id and storage_target_id. retention_days, retention_count, gzip, include_users_and_roles (users and roles), rpo_minutes (RPO), enabled, verify_after_backup ("" follows the integrity.verify_after_backup setting, "on", "off") and restore_test ({enabled, frequency, every_n, connection_id}, see verification.md) are optional: an omitted field keeps the job's current value, so {"enabled": false, ...} pauses a job without touching its retention, and last_restore_test is managed by the server and never taken from requests. Pausing with "paused_until": "<RFC 3339 time>" resumes the job on its own at that time (the scheduler checks every minute; the time must be in the future, else 400); pausing without it pauses until the job is resumed, an edit that keeps the job paused keeps its paused_until, and resuming ("enabled": true) clears it. A paused job's run in progress continues; stop it with cancel. The job is validated exactly like a new one: the cron expression must parse (five fields or a descriptor such as @daily or @every 6h; an empty one means @daily), database and a known connection_id are required, retention must not be negative, and storage_target_id must name a target (empty means the default target).
The new schedule takes effect immediately, without a restart: the job's cron entry is replaced, or removed for a disabled job, and next_run is recomputed. The id, created_at, last_run and the job's backups (GET /api/v1/backups?job_id={id}) are kept. The response is the updated job. Concurrent updates are stored and scheduled in the same order, and a backup that finishes while the job is being edited only records its run times, so it never reverts the edit.
To avoid overwriting someone else's change, send the job's current updated_at (from GET /api/v1/jobs/{id}) in the body: the update is refused with 409 Conflict if the job was changed since. Without updated_at the last write wins.
| Status | When |
|---|---|
400 Bad Request |
Invalid JSON, cron expression, database, retention, rpo_minutes, connection or storage target |
404 Not Found |
No job {id} |
409 Conflict |
updated_at was sent and the job was changed since |
curl -X PUT http://localhost:8080/api/v1/jobs/job_shop_1727146800_3f9a1c2e \
-H "X-API-Key: $MONGORESCUE_ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"name":"shop hourly","cron_expression":"@hourly","database":"shop","connection_id":"conn_prod","retention_count":24}'
In the dashboard, clicking a job row (or Details in its ⋯ menu) opens the job's details: its schedule in words with the next three runs, retention, compression, encryption, state, and the last 20 runs with their success rate. Edit opens the job form prefilled, and Pause / Resume (also in the row's ⋯ menu) pauses the schedule, until resumed or until a date and time, or resumes it; a paused job shows a Paused badge. Stop current run stops the job's current run: its running backup and, for a job with several databases, every database still waiting. Edits and pauses send updated_at, so a job changed elsewhere in the meantime is reported instead of overwritten.
read_preference (with read_preference_tags), max_upload_mbps, num_parallel_collections and backup_window are optional too: an omitted field keeps the job's value, "read_preference": "" uses the connection's again and "backup_window": {} removes the window (see Read preferences, throttling and backup windows).
database_selection and parallelism are optional in an update: when database_selection is omitted, a database makes the job a single-database job of it, and an update with neither keeps the job's selection (see Jobs with several databases). restore_test.databases (rotate or all) chooses which databases of a multi-database run the restore test covers.
Jobs with several databases¶
A job backs up one database or several. Its database_selection says which:
{
"mode": "pattern",
"databases": ["billing"],
"include": ["prod_*"],
"exclude": ["prod_tmp?"],
"auto_include_new": false
}
mode |
Backs up | Fields |
|---|---|---|
single |
One database, as jobs always did (database holds it too) |
databases: exactly one name |
list |
The named databases | databases: at least one name |
all |
Every database of the connection except those exclude matches |
exclude, databases (always added), auto_include_new |
pattern |
The databases an include pattern matches and no exclude pattern matches |
include (at least one), exclude, databases (always added), auto_include_new |
admin,configandlocalare never backed up bylist,allorpatternjobs, and naming one in alistis refused with400.- Patterns use
*(any run of characters, also none) and?(exactly one character); every other character matches itself and matching is case-sensitive, like MongoDB database names. They follow the rules for database names (1 to 63 bytes, none of/ \ . " $, a space or a control character, not starting with-), with*and?as the only wildcards. A pattern that matches no database is allowed; the preview and the run report it as a warning. - The job's own collection filters (
collections,exclude_collections) only apply tosinglejobs; a multi-database job that sends them is refused with400.listandalljobs filter collections per database instead, see Collection filters per database. parallelism(1 to 4, default 1) is how many databases a run backs up at the same time. Each database keeps its own run lock: a database another backup is running for (a manual backup, another job) waits until it finished, up to 30 minutes, and is recorded as failed in this run after that (the others still run).- Clients that predate selections keep working: a job created or updated with
databaseand nodatabase_selectionis asinglejob of that database, an update that sends neither keeps the job's selection, and responses still carrydatabasefor single-database jobs (""for the others). Every job stored by an earlier release was migrated to asingleselection (migration 0013).
New databases. For all and pattern jobs the server records the databases the selection matched when the job was saved (and when its selection or connection changes) as known_databases (managed by the server, never taken from requests). With auto_include_new off (the default), a run backs up only the known databases plus the named ones; databases created later that match are not backed up but listed as new_databases in the run, in the run's notification ("3 new databases not included") and in the dashboard, which offers Add to job (it adds the name to databases). With auto_include_new on, a run backs them up too, adds them to known_databases and publishes a job.databases_added event the first time each one appears.
Runs. Every database of a run gets its own backup record and archive, exactly as a single-database backup: the same storage key layout (<db>/<yyyy>/<mm>/<id>.archive.gz), verification, manifest, log and cancellation, so restores, retention, pins and verification work unchanged. The records of one run share a run_id (GET /api/v1/backups?run_id=…), and GET /api/v1/jobs/{id}/runs lists the runs: {id, job_id, trigger, status, started_at, completed_at, duration_seconds, databases: [{database, backup_id, status, error}], new_databases, added_databases, warnings, error}. A run is ok when every database succeeded, partial when some failed, failed when none succeeded and cancelled when it was stopped without failures. A database named in a list (or in databases) that no longer exists is recorded as failed with "error": "database not found"; the others still run. A run whose connection cannot list its databases fails as a whole (error), and so does a run that matches no existing database. POST /api/v1/jobs/{id}/run of a multi-database job answers 202 at once with the run ({id, job_id, trigger, status: "running", started_at, databases: []}); its databases are resolved in the background and planning failures are recorded in the run (poll GET /api/v1/jobs/{id}/runs), while a run of the job still going answers 409. The run is stored after every database, so a crash keeps the outcome of the databases that finished: the next start rebuilds each database's status from its backup record (completed ones stay completed, the others fail as interrupted). Single-database jobs record a run too.
Notifications send one message per run, not one per database: the run's backup.succeeded, backup.failed (also for a partial run, whose message names the failed databases) or backup.cancelled event carries run_id and a run summary (status, databases, succeeded, failed, cancelled, failed_databases, new_databases); webhooks receive both fields. The per-database events still feed the metrics (per job and per database), and every run is counted in mongorescue_job_runs_total (metrics).
Stopping a run. POST /api/v1/jobs/{id}/cancel (operator) stops the job's current run: the running database and every database still waiting are cancelled ({job_id, run_ids, cancelled, backups}; 200 once they stopped, 202 while some still stop, 404 unknown job, 409 no run of it is active). Cancelling any one backup of a run (POST /api/v1/backups/{id}/cancel, MCP cancel_run, the desktop app's quit) stops the whole run as well. Databases already backed up keep their backups. A scheduled run that is still going when the next one is due is skipped; starting one on demand meanwhile answers 409. Retention and the restore tests of a scheduled run happen after its run slot is released, so they never delay the next run. The known databases a run records are merged into the job as it is stored when the run ends: when the job's selection or connection was changed meanwhile, only the databases it backed up that the new selection still matches are added, and a newer value is never overwritten.
Retention stays per job and database: the job's retention_days and retention_count apply to each of its databases separately, and so do the protections. The newest completed backup of every database is kept (so a database whose backup failed today keeps its last good one), and so is the newest verified one of every database. A scheduled run prunes only the databases it backed up successfully. GET /api/v1/jobs/{id}/retention/preview adds databases, the per-database breakdown (database, considered, delete, protected, kept, last_good).
Restore tests of a multi-database job test one database per run, taking turns in name order (restore_test.databases: "rotate", the default), or every database of the run ("all").
Collection filters per database¶
A list or all job can back up only some collections of some of its databases. An entry of databases may be an object with a filter instead of a name, and the filters are stored and returned in collection_filters, one per database, sorted by name:
{
"mode": "list",
"databases": ["a", {"name": "b", "exclude_collections": ["logs", "tmp"]}, {"name": "c", "collections": ["orders"]}]
}
is stored, and returned, as
{
"mode": "list",
"databases": ["a", "b", "c"],
"collection_filters": [{"name": "b", "exclude_collections": ["logs", "tmp"]}, {"name": "c", "collections": ["orders"]}]
}
collection_filtersmay also be sent directly; a database must not have a filter both there and in itsdatabasesentry.- A database's filter has either
collectionsorexclude_collections, never both (400). - Each database's filter works exactly like the collection filter of a single-database backup:
collectionsbacks up only those collections (several of them become exclusions of every other collection, and a collection that does not exist fails that database's backup),exclude_collectionsbacks up everything else. Names follow the collection name rules (no$, no control characters), at most 1000 per list. Withinclude_users_and_rolesacollectionsfilter becomes exclusions, as for one database. *(any run of characters) and?(one character) are wildcards, in every collection filter (one database or several, ad hoc or in a job):tmp_*excludes every collection starting withtmp_. A pattern is never taken for a collection name. The backup expands it against the database's collections when it runs: an exclude pattern that matches nothing excludes nothing, an include pattern that matches nothing fails that database's backup, and patterns never match system collections (system.*). Expanding needs the server's collection lister; a server without one refuses filters with patterns with400(collection filter cannot be applied), when the backup is requested or the job saved.- The backup record names the filter that was applied, patterns expanded:
collections(the included collections),exclude_collections(the excluded ones) andfiltered: true. Retrying a failed backup (POST /api/v1/backups/{id}/retry) reuses that record's own filter, so a database of a multi-database run keeps its filter. - A
listjob's filters must name databases of itsdatabases, and an object entry ofdatabasesadds the database to the list. In analljob an object entry only attaches its filter: it does not add the database todatabases(the databases always backed up) and does not change which databases the job matches or the known databases. Its filters must name databases the job backs up: notadmin,configorlocal, and none itsexcludepatterns skip (400, "filter for an excluded database") unless they are also named indatabases. A filter on a database that does not exist yet applies once it does. patternjobs have no per-database filters (400), and databases analljob discovers later are backed up whole until a filter is added for them.- The manifest, verification and restore tests of a filtered backup cover only the collections it backed up (the expanded filter on the record), as for a filtered single-database backup. A filtered backup counts towards the database's RPO; see production.md for what it does not cover.
- Jobs stored before filters existed have no
collection_filtersand are unchanged; adatabasesarray of names stays valid everywhere. The filters live in the job's JSON data, so no migration is needed.
Previewing a selection¶
GET /api/v1/jobs/{id}/databases/preview (read scope) resolves the job's selection against the live server exactly as its next run would, without changing anything:
{
"job_id": "job_pattern_1727146800_3f9a1c2e",
"selection": {"mode": "pattern", "include": ["prod_*"], "auto_include_new": false},
"included": ["prod_eu", "prod_us"],
"excluded": [{"name": "admin", "reason": "system"}, {"name": "prod_new", "reason": "new"}, {"name": "staging", "reason": "not_matched"}],
"missing": [],
"new_since_last_run": ["prod_new"],
"warnings": []
}
Reasons are system, excluded (with the matching pattern), not_matched, not_selected, new and not_found. Query parameters preview another selection while the job is edited: mode, repeated databases, include and exclude, auto_include_new, connection_id and storage_target_id (the target whose largest archive the warnings compare each database's expected size with, as backups do); a changed selection is previewed as saving it would record it (everything it matches counts as known). GET /api/v1/jobs/databases/preview previews a selection for a job that does not exist yet (connection_id and mode required). Answers: 400 invalid selection, 404 unknown job or connection, 502 the databases cannot be listed (a list selection is then previewed as named, with a warning).
curl -X POST http://localhost:8080/api/v1/jobs \
-H "X-API-Key: $MONGORESCUE_ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"name":"all prod","cron_expression":"0 2 * * *","connection_id":"conn_prod",
"database_selection":{"mode":"pattern","include":["prod_*"],"exclude":["prod_tmp*"]},"parallelism":2}'
Backing up several databases now¶
POST /api/v1/backups with databases instead of database backs up several databases of one connection in one run, as the dashboard's Back up now does with Selected or All:
curl -s -X POST http://localhost:8080/api/v1/backups \
-H "X-API-Key: $MONGORESCUE_KEY" -H "Content-Type: application/json" \
-d '{"connection_id":"conn_prod","databases":["shop","crm","billing"],"parallelism":2}'
databasesnames 1 to 200 databases, each a valid database name, none twice. It cannot be combined withdatabase(400).parallelismis how many of them are backed up at once: 0 to 4, where 0 or omitted means 1, as for jobs. Every database keeps its own run lock.- An entry of
databasesis a name or an object{"name", "collections" | "exclude_collections"}with a collection filter of its own for that database, such as["shop", {"name": "crm", "exclude_collections": ["logs", "tmp"]}, {"name": "billing", "collections": ["invoices"]}]. Entries without a filter behave exactly as before. - The top-level
collectionsandexclude_collectionsapply only to a run of one database (and not together with a filter in its entry); with several databases they are refused with400; give each database its own filter instead. include_users_and_rolesapplies to each database, as for a job with several databases;adminis backed up without it (its dumps already contain every user and role). A run ofadminalone with the option is refused with400.storage_target_idandgzipapply to every database.
Each database gets its own backup record and archive, with "trigger": "manual" (mcp from MCP) and the same run_id. The databases are backed up exactly like those of a job with several databases: they show up as queued at once, cancelling any one of them (POST /api/v1/backups/{id}/cancel) stops the whole run, and notifications get one summary for the run instead of one per database. The run belongs to no job, so it is not listed under GET /api/v1/jobs/{id}/runs; list its backups with GET /api/v1/backups?run_id=....
The response is 202 with the run:
{
"run_id": "run_20261005_101500_3f9a1c2e",
"backups": [
{"id": "bkp_shop_20261005_101500_a1b2c3d4", "database": "shop", "status": "in_progress", "run_id": "run_20261005_101500_3f9a1c2e", "...": "..."},
{"id": "bkp_billing_20261005_101500_e5f6a7b8", "database": "billing", "status": "in_progress", "run_id": "run_20261005_101500_3f9a1c2e", "...": "..."}
],
"busy": [
{"database": "crm", "busy": true, "error": "another backup of database crm is already running"}
]
}
backups are the in-progress records, in the order they run. A database another backup is running when the request arrives is not backed up: it comes back in busy (an empty array when none is) and the others start. A skipped database is neither a success nor a failure: it does not change the run's outcome (a run whose other databases all succeed is ok), its failure counts or metrics, and the summary notification lists it as "Skipped (already running)" (run.skipped_databases in webhooks). When every database is busy the request is refused with 409. A request with database is answered exactly as before: the one backup record, without run_id; it ignores parallelism, as it always did.
Retrying a failed backup¶
POST /api/v1/backups/{id}/retry (operator scope, like starting a backup) starts a new manual backup with the parameters of the failed backup {id}: the same connection, database, collections and storage target (the default target for records written without one). When the backup belongs to a job that still exists, the job's exclude_collections and gzip are used; otherwise compression follows the failed backup's storage key. Encryption follows the current settings. The retry is started exactly like POST /api/v1/backups (same concurrency limit per database, events and notifications) and the response is the new in-progress record with "trigger": "manual" and "retry_of": "{id}".
The failed record is never changed or deleted by a retry: its status, error_message, started_at and completed_at stay in the list, and every attempt links to the one it retries through retry_of (omitted for backups that are not retries). A failed retry can be retried again, forming a chain.
| Status | When |
|---|---|
404 Not Found |
No backup {id} |
409 Conflict |
The backup has not failed (only failed backups can be retried), or a backup of the same database is already running |
422 Unprocessable Entity |
The backup's connection or storage target no longer exists, or the backup has no connection recorded |
curl -X POST http://localhost:8080/api/v1/backups/bkp_shop_20260924_030000_3f9a1c2e/retry \
-H "X-API-Key: $MONGORESCUE_OPERATOR_KEY"
In the dashboard, a failed backup that has not been retried yet shows a Retry button, and Details opens the full error message (including mongodump's stderr, which the server stores in the redacted error message), the absolute start and end times, the duration, the trigger, connection, database, storage target and the retry chain.
Listing backups and restores¶
GET /api/v1/backups and GET /api/v1/restores take optional query parameters. All given filters must match; empty values are ignored.
| Parameter | Backups | Restores | Meaning |
|---|---|---|---|
id |
yes | yes | Records with exactly these IDs, comma-separated (at most 200) |
status |
pending, in_progress, completed, failed, cancelled, pruned, missing, deleted, purged |
pending, in_progress, completed, failed, cancelled |
Records in this state. Without status (and without id), deleted and purged backups are hidden |
deleted |
true |
Only deleted backups waiting for their purge (like status=deleted; the dashboard's Deleted view) |
|
database |
Backed-up database | Target database | Exact, case-sensitive match |
connection_id |
yes | Backups taken from this connection | |
job_id |
yes | Backups of this scheduled job | |
trigger |
scheduled, on_demand, manual, mcp |
How the backup was started (records older than triggers count as scheduled when they belong to a job, else manual) |
|
retry_of |
yes | Retries of this backup | |
run_id |
yes | Backups of this job run (one per database) | |
backup_id |
yes | Restores of this backup | |
from, to |
yes | yes | started_at range in RFC 3339 (2026-10-01T00:00:00Z); from is inclusive, to exclusive |
q |
ID or database | ID, source or target database | Substring, ASCII case-insensitive; % and _ are literal (at most 256 characters) |
sort |
started_at, duration, size, database, status |
started_at, duration, database (target), status |
A column for ascending order, - and a column for descending (sort=-size); ties keep the newest first. desc (newest first, the default) and asc still sort by start time |
limit |
yes | yes | Page size, 1 to 200 |
offset |
yes | yes | Matches to skip (default 0) |
Without limit the response is unchanged from earlier releases: data is the array of every matching record. With limit, meta describes the page:
{
"success": true,
"data": [{"id": "bkp_shop_20260924_030000_3f9a1c2e", "status": "failed", "...": "...",
"retried_by": {"id": "bkp_shop_20260924_031000_9b1d2c3e", "started_at": "2026-09-24T03:10:00Z"}}],
"meta": {"total": 312, "limit": 25, "offset": 0}
}
Breaking (API) in 0.9.0: these parameters are now validated. Earlier releases ignored every query parameter of
GET /api/v1/restoresand every one ofGET /api/v1/backupsexceptdatabaseandjob_id; a malformedstatus,trigger,from,to,sort,limit,offset,id(more than 200 IDs) or an over-longq,database,connection_id,job_id,retry_of,backup_idor ID now answers400instead of being ignored. Unknown parameter names are still ignored.
total counts every match, so a client can show "1–25 of 312"; an offset past the end returns an empty data with the same total. A malformed or out-of-range value (limit=0, limit=500, sort=random, sort=id, sort=size on restores, from=yesterday, to before from, an unknown status or trigger) answers 400 with a message naming the parameter. Backup items carry retried_by, the newest backup whose retry_of is this one, when there is one; it is computed for each listed row, so retry links work on any page.
curl "http://localhost:8080/api/v1/backups?status=failed&database=shop&from=2026-09-01T00:00:00Z&limit=25&offset=25" \
-H "X-API-Key: $MONGORESCUE_READ_KEY"
The sort columns are a fixed list mapped to constant SQL; anything else, a different case or a column the resource has not answers 400 listing the accepted columns.
curl "http://localhost:8080/api/v1/backups?status=completed&sort=-size&limit=25" -H "X-API-Key: $MONGORESCUE_READ_KEY"
GET /api/v1/stats covers every record regardless of any list filter and is computed with SQL aggregates (it does not read every record): total_backups, completed_backups, failed_backups, failed_backups_24h, total_bytes, active_backups and active_restores (pending or in progress), total_restores, active_jobs, last_backup ({id, database, job_id, status, started_at, error_message} of the newest backup) and job_last_backups (each job ID mapped to its newest backup), plus the default storage target. The figures are computed once for every caller asking with the same connection access, and again as soon as a job, backup, run or restore is written (at most 10 seconds later otherwise). When some figures cannot be read (they then count as zero), the response adds degraded: true and a degraded_reason. For callers with the effective admin scope (administrators and their admin keys) it also lists corrupt_records: stored rows that every list skips because they cannot be read, as {table, id, error} (the error never quotes stored data). See troubleshooting.md.
Listing jobs¶
GET /api/v1/jobs returns one page of the jobs in the order of sort and order (by name, ascending, by default), ties broken by ID, so the order is stable. Optional filters keep only the matching jobs, with the same matcher as the jobs bulk filter, so selecting "all matching" there selects exactly the listed jobs. All given filters must match.
Changed (API) after 0.26.0: the list is paged by cursor. Without
limitthe answer is the first 100 jobs (earlier releases returned every job), withmeta.nextwhen more follow. Clients that read every job followmeta.nextuntil it is absent, as the CLI, the MCP server and the dashboard do.
| Parameter | Meaning |
|---|---|
limit |
Page size, 1 to 500 (default 100) |
sort |
name (default), next_run (paused jobs and jobs without a next run last, in both directions), last_status (the outcome of the last run: failed, partial, cancelled, in_progress, completed, within each the newest first by when that backup or run started; jobs that never ran last) or created_at |
order |
asc (default) or desc |
after |
The meta.next of the previous page in the same sort and order (opaque; anything else, also a cursor of another order: 400). Without it the list starts at the first job |
{
"success": true,
"data": [{"id": "job_a1b2c3", "name": "nightly shop", "...": "..."}],
"meta": {"total": 10000, "limit": 100, "next": "ajE6eyJzIjoibmFtZSIsImsiOnsiayI6Im5pZ2h0bHkgc2hvcCIsImlkIjoiam9iX2ExYjJjMyJ9fQ"}
}
total counts every matching job; next is present while jobs follow the page and is the after of the next one. Paging is by key (the order, its direction and the sort value and ID of the page's last job), not by position: a job created or deleted between two requests never makes the next page repeat or skip another job, and a cursor stays valid when its job is deleted. A cursor works with any filter, but pages of one filter are only consistent with that filter's cursors. total leaves out stored jobs that cannot be read, as the pages do. Every filter and order is answered in SQL from the jobs table: the pages from the indexes of the order (with and without the connection and state filters), q from a lower-cased search text of each job and database, schedule and last_status from columns kept on each job, which every job write updates (last_status follows every backup and run; an all or pattern job is matched against its patterns). A caller limited to some connections, one of whose jobs has its newest backup on another connection (a job that moved), gets last_status filters and the last_status order computed from the backups it may see instead.
curl "http://localhost:8080/api/v1/jobs?enabled=true&limit=500" -H "X-API-Key: $MONGORESCUE_READ_KEY"
curl "http://localhost:8080/api/v1/jobs?enabled=true&limit=500&after=$NEXT" -H "X-API-Key: $MONGORESCUE_READ_KEY"
Filters:
| Parameter | Meaning |
|---|---|
q |
ID, name, database or a multi-database job's selection contains this text (case-insensitive, at most 256 characters) |
enabled |
true scheduled jobs, false paused ones (anything else: 400) |
connection_id |
Jobs of this connection |
database |
Jobs covering this database: a single-database job's database, or one a multi-database job lists, knows or matches with a pattern |
schedule |
Frequency of the cron expression: hourly (more than once a day, also @every under 24 h), daily, weekly (some weekdays), monthly (some days of the month) or other |
last_status |
Outcome of the job's last run: completed, failed, partial, in_progress, cancelled or never. A single-database job's is its newest backup's (a pruned or missing archive still counts as completed); a multi-database job's is its newest run's (ok, partial, failed, cancelled, running), never one database's backup |
An unknown schedule or last_status value, an over-long text, a limit outside 1-500, an unknown sort or order, or an after that is not a meta.next of the same order answers 400.
Overview history and schedule preview¶
GET /api/v1/stats/history (read scope) feeds the dashboard's Overview tab. Like /api/v1/stats it is computed with SQL aggregates and never reads every record:
daily: one entry per day of the window, oldest first, empty days included:{date, completed, failed, cancelled, bytes, stored_bytes}.days(1-366, default 30) counts back from today.tz(an IANA zone such asEurope/Istanbul) makes the days start at your midnights, also across daylight saving changes. Whentzis missing or unknown to the server,tz_offset(minutes east of UTC, -840 to 840, e.g.180for UTC+3) is used as a fixed offset instead.time_zonein the answer names the zone used.bytessums the day's completed backups that are still on record, andstored_bytesis the running total sincestored_bytes_before(completed backups on record that started earlier). Both describe the archives kept today, so backups that retention has deleted no longer count.jobs: each existing job with backups or an enabled schedule mapped to{runs, last_success_at, interval_seconds}: its 12 newest backups, oldest first ({id, status, started_at, duration_seconds}), when its newest completed backup started, and the gap between its next two scheduled runs.- For a job with several databases,
runslists its 12 newest runs, each once however many databases it covered ({id, status, run_status, databases, succeeded, started_at, duration_seconds};statusiscompletedforok,failedforpartialandfailedruns), andlast_success_atis its stalest database's newest success (empty while one never succeeded), withstalest_databaseand the per-databasedatabases. The Overview's attention list then reads "job X: db Y has no successful backup since …" and flags a partial last run. upcoming: the enabled jobs' scheduled runs in the next 24 hours, in order ({job_id, at}), at most 96 per job and 500 in total (upcoming_truncatedsays the caps cut the list).verification_issues: the 20 newest completed backups of the window whose verification failed ({id, job_id, database, started_at, verification},mismatchorerror), andverification_issues_totalfor the window.
Every query is answered from an index on backups, never by scanning the table, and record JSON is read only for the rows the index selected (a job's newest runs, the window's completed backups). The per-day outcomes and the stored size read covering indexes only; the runs and last successes of jobs with several databases are read in one bounded query per 500 jobs. The answer is computed once for every caller asking with the same days, zone and connection access within 10 seconds (also when they ask at the same moment), and computed again earlier after a backup, job run, verification, retention, bulk action or job change, so generated_at may be a few seconds old.
- server_time_zone: {name, offset_minutes}, the time zone cron expressions are evaluated in.
GET /api/v1/schedule/preview?cron=0%202%20*%20*%20*&n=3 (read scope) parses a cron expression with the scheduler's own parser and returns {valid, error, next_runs, server_time_zone, default_rpo_minutes}: the expression's hours are read in the server's time zone, and next_runs are given in UTC. default_rpo_minutes is the recovery point objective a job with this schedule has when it sets none. An expression the scheduler rejects is answered with 200 and "valid": false plus the reason; a missing or longer than 256 characters cron, or n outside 1-10, is a 400. The dashboard's cron builder uses it for its next runs line and the job form's RPO hint.
Health¶
GET /api/v1/health (public) answers 200 with {status: "healthy", version, time, scheduler, scheduler_last_tick}. scheduler is ok while the scheduler records its liveness tick (every 30 seconds) and not_started before it starts; scheduler_last_tick is the time of the last tick. When the last tick is older than three intervals (90 seconds: the scheduler is deadlocked or stopped; the tick runs on a monotonic timer, so wall clock steps do not count, see monitoring.md), the answer is 503 with success: false and scheduler: "stale" (status keeps its value "healthy"; read the HTTP status or scheduler), so the Docker HEALTHCHECK, a load balancer or an uptime monitor see the failure. While new runs are refused (a graceful shutdown waiting for running backups, or the desktop app waiting before it quits) the answer adds draining: true and stays 200. The fields of earlier releases are unchanged. The tick time is also exported as mongorescue_scheduler_last_tick_timestamp_seconds (metrics).
In active-passive HA mode the answer adds role (active or standby) and ha: {role, cluster, epoch, holder, lease_healthy}, where holder names the instance holding the lease as last seen. A standby answers 200 with scheduler: "not_started". It serves no data and accepts no sign-in: every other API request, signing in and out, single sign-on and /mcp answer 503 with a Retry-After header and the error this instance is the standby of HA cluster …; it serves no data and accepts no sign-ins: use the active instance (data: {role, cluster}). GET /metrics on a standby needs no credentials and exposes only the process, Go runtime, build and HA series. While an instance changes role (a takeover or a step-down) or starts, the health check answers 200 with role: "switching" (ha.role too), /metrics serves the standby series, and every other request answers 503 with Retry-After: 1 and the error this instance of HA cluster … is changing role; retry in a second (data: {role, cluster}); the port stays open (details).
Job heartbeats¶
A job's optional heartbeat_url (POST /api/v1/jobs, PUT /api/v1/jobs/{id}) is pinged at <url>/start when a run starts, <url> when it succeeds and <url>/fail when it fails or is partial; a run cancelled by a user or interrupted by a shutdown sends nothing after its start (monitoring.md). It must be an http or https URL without user info (else 400), is stored encrypted and is returned only up to its host ("https://hc-ping.com/******") by every endpoint and MCP tool that returns jobs. Sending the masked value (or "******") back keeps the stored URL, "" removes it, and a masked value with another host is a 400. PUT /api/v1/jobs/{id} keeps omitted fields, heartbeat_url included. POST /api/v1/jobs with an existing id is an upsert that replaces every field of the job: a body without heartbeat_url clears it (send the masked value back to keep it). A stored URL that can no longer be decrypted (a damaged or swapped ciphertext) does not stop the job: it runs without its heartbeat, and the URL is listed among the unreadable records (jobs.heartbeat_url) until it is entered again.
Read preferences, throttling and backup windows¶
These fields keep large backups off a busy primary and within the hours you choose; production.md explains when to use them.
| Field | On | Values |
|---|---|---|
read_preference |
connection, job | primary, primaryPreferred, secondary, secondaryPreferred, nearest, or "": a job's empty value uses its connection's, a connection's the URI's own |
read_preference_tags |
connection, job | Tag sets tried in order, [{"dc": "east"}, {}] ({} matches any member); at most 10 sets of 10 tags, no , or : in names and values, not with primary |
max_concurrent_backups |
connection | Backups that may read from the connection at once (0 = unlimited, at most 64); further ones wait with phase: "waiting" |
max_upload_mbps |
job | Upload cap in megabits per second (0 uses the general.max_upload_mbps setting, at most 100000) |
num_parallel_collections |
job | mongodump --numParallelCollections, 1 to 16 (0 = mongodump's default of 4) |
backup_window |
job | {"timezone": "Europe/Istanbul", "days": ["sat", "sun"], "start": "22:00", "end": "02:00", "cancel_at_window_end": false}: timezone an IANA name ("" = UTC), days the days the window opens (mon to sun, none = every day), start HH:MM, end HH:MM or 24:00 and different from start (not after it: the next day) |
Invalid values answer 400 naming the field. The read preference is added to the connection string passed to mongodump through its private config file (never --uri) and to the manifest capture. Backup records gain read_preference (as used, with its tags) and source_member (host, state, set_name: the member hello reported under that preference before the dump started). With secondary, a backup fails before mongodump starts when the selected member is not a secondary (read preference secondary needs a replica set secondary, but the connection reaches only ...); the other modes go on and record what they found.
A scheduled run outside its job's window does not start, and it is not a failure. Consecutive skips with no run in between share one job run with "status": "skipped", "skip_reason": "outside window" and skipped_runs (how many activations it stands for, from started_at to completed_at), so an hourly job with a four-hour window records one skip a day; it is listed by GET /api/v1/jobs/{id}/runs, passed over as the job's last status and left out of the overview history before its limit. Only the first skip of each such gap publishes backup.skipped (status: "skipped", detail: "skipped (outside window)"), and only to notification rules that name it: it is opt-in. With cancel_at_window_end, a scheduled run still going when the window closes is cancelled by system with the reason "the backup window ended". POST /api/v1/jobs/{id}/run and the MCP run_job tool ignore the window. GET /api/v1/jobs/{id} adds window_open for a job with a window and lists in next_runs only the runs the window allows; its effective_rpo_minutes uses the largest gap between them.
Copies on further storage targets¶
Jobs (POST /api/v1/jobs, PUT /api/v1/jobs/{id}) and manual backups (POST /api/v1/backups) take copy_targets (up to 3 storage target IDs other than the primary) and copy_mode (async, the default, or sync). An update without copy_targets keeps them, and [] removes them. An unknown or invisible target is 404/400, the primary or more than three targets are 400. Every backup record lists its copies in copies (target_id, target_name, storage_key, status, sha256_ok, version_id, retain_until, error, copied_at, attempts, next_attempt_at, verification, verified_at), and copy_mode. A retried backup keeps the copy targets of the failed one. POST /api/v1/backups/{id}/copies/retry (admin) queues every failed copy of a completed backup again with its attempts reset, including copies the queue gave up on (backup.copy_exhausted). It answers the updated backup, or 400 when the backup is not completed or has no failed copy. MCP's start_backup takes the same fields, and restore_backup takes source_target_id. See configuration.md for the copy queue, retries and the purge.
curl -s -X PUT http://localhost:8080/api/v1/jobs/job_shop \
-H "Authorization: Bearer $MONGORESCUE_KEY" -H 'Content-Type: application/json' \
-d '{"name": "shop", "database": "shop", "connection_id": "conn_prod", "copy_targets": ["stg_offsite"], "copy_mode": "async"}'
Cross-region disaster recovery¶
- Locked copies. Jobs take
require_locked_copies(boolean). With it, a job whose copy target has no S3 Object Lock is refused with400("the job requires locked copies").POST /api/v1/jobswithout the field takessecurity.require_locked_copies;PUTwithout it keeps the job's value. Whilesecurity.require_locked_copiesis on, no job can opt out: a new job withfalse, or a job turning it off, is400. With the setting off, a job turning it off keeps it on and answerspending_locked_copies(or, with the two-person rule,locked_copies_approvalfirst): it is turned off after the grace period (pending change kindjob_locked_copies). Turningsecurity.require_locked_copiesoff throughPUT /api/v1/settingsbecomes a pending change of kinddisable_locked_copies(after the grace period), or, with the two-person rule, an approval request (disable_locked_copies) first. - Regions. Storage targets have
region(at most 64 printable characters) andregion_detected.POST/PUT /api/v1/storage-targetstakeregion(an operator's label); aPUTwithout it keeps the stored one,""clears it. An AWS S3 target (no endpoint) without a region (here or ins3.region, ors3.regionisauto) takes its bucket'sGetBucketLocationwhen the key may read it (region_detected: true), detected again when the endpoint, bucket ors3.regionchanges. On other endpoints only a label counts as a region. - DR drills. A job's
restore_testtakessource_target_id: one of the job'scopy_targets(else400). Its restore tests then read the copy there; a test result hassource_target_idandsource_target_name. A scheduled drill is skipped while no backup has a completed copy on that target; a manual one (POST /api/v1/jobs/{id}/restore-test) is refused like a job without a backup. - PITR. Streams take
copy_targets(see below), and a point-in-time restore takessource_target_id.
Readiness rows of jobs with copy targets carry dr (see recovery readiness).
Recovery point objectives¶
A job's rpo_minutes (POST /api/v1/jobs, PUT /api/v1/jobs/{id}, returned by GET /api/v1/jobs/{id} and the MCP get_job tool) is its recovery point objective: how old, in minutes, the newest successful backup of each of its databases may be. 0 or absent means the default from the schedule: twice its interval plus an hour, at least six hours. The interval is the largest gap between consecutive runs over 8 days (a year for monthly and rarer schedules), read in the server's time zone like the scheduler does, so it does not change with the day of the week: @hourly gets 6 hours, @every 6h 13 hours, @daily 49 hours, 0 9,17 * * * 33 hours (the 16-hour night), 0 9 * * 1-5 145 hours (Friday to Monday) and @monthly about 63 days; a schedule that cannot be read counts as daily. A job with a backup window counts only the runs the window allows. When set it must be between 15 (15 minutes) and 129600 (90 days), else 400. An update without rpo_minutes keeps it; 0 restores the default. GET /api/v1/jobs/{id} adds effective_rpo_minutes (the objective that applies) and rpo_default.
Every 5 minutes, right after a job's backup finished and right after a job is created, edited, paused, resumed or deleted, the server checks every database of every enabled job: the age of its newest successful backup against the job's objective. A database without one counts from when it joined the job: for a database a run included automatically later, the time it was first counted among the job's known databases (job_database_joins, migration 0018), else the job's last update or creation, so a database added to an old job is not reported at once. When the objective is missed, job.rpo_missed is published once (job_id, database, the age and the objective in detail); when it heals, job.rpo_recovered (its detail says whether a recent enough backup came in or the objective was changed). Breaches are kept in the metadata database (rpo_breaches, migration 0018), so a restart does not report a breach again; an event the event bus drops (a full queue) is not recorded, so the next check publishes it again. Pausing or deleting the job, or removing the database from its selection, drops the breach without an event; resuming a job that is still past its objective reports it again right away. Both events are selectable in notification rules, and the age and objective are exported as metrics.
Recovery readiness¶
GET /api/v1/readiness (read scope) answers "could we recover each database, and how fast?". The report is computed once for every caller asking with the same connection access and shared until a job, backup, run, restore, restore test or RPO breach is written, an event is sent or 5 seconds pass, so generated_at may be a few seconds old. It returns {generated_at, keys_escrowed, key_wrapping, summary: {ok, warn, fail}, rows, streams, storage_hints} (key_wrapping: {active, provider}, whether secret.key is wrapped by a KMS; storage_hints: the hints of every storage target with target_id and target_name; they never change a row's status) with one row per connection and database a job backs up (paused jobs included), failing rows first:
| Field | Meaning |
|---|---|
connection_id, connection_name, database |
The database |
jobs |
[{id, name, enabled, target_seconds, default, age_seconds, no_backup, met, breached_since}]: each job backing it up, with its objective, the age of its newest successful backup of the database and whether the objective is met |
last_good_backup |
{id, at, job_id}: the newest completed backup by any of them; a filtered one adds filtered: true and its collections or exclude_collections (it still counts towards the RPO) |
last_verified_backup |
The newest completed backup whose archive passed verification |
last_restore_test |
{id, job_id, at, status, duration_seconds}: the newest restore test of the database (ok, mismatch or error) |
rpo |
{target_seconds, age_seconds, met}: the strictest objective of the enabled jobs, the age of last_good_backup, and whether every enabled job meets its objective (met is absent when every job is paused) |
rto |
{seconds, source, id, measured_at}: the estimated recovery time, the duration of the newest successful restore test (source: "restore_test"), else of the newest completed real restore of the whole database ("restore"; dry runs and selective restores do not count); absent when neither exists |
keys_escrowed, encrypted |
Whether a recovery kit was downloaded for the current secret.key, encryption keys and storage targets, and whether last_good_backup is encrypted |
copies, copy_targets |
How many copies of last_good_backup are done, of how many planned (copy_targets is absent without copy targets) |
dr |
Only when a job of the row has copy targets: {level, cross_region, separate_credentials, locked_copy, primary_regions, copy_regions, last_drill, last_good_drill, drill_stale, drill_cross_region}. level is cross_region when a copy target in another region has Object Lock and a DR drill from such a target passed in the last 30 days, cross_region_unproven when a copy target is in another region but that is not proven, same_region otherwise. last_drill and last_good_drill are restore test references with source_target_id |
status, reasons |
fail for rpo_missed, restore_test_failed (the newest test failed) or verification_failed (the newest good backup failed verification); warn for no_backup, paused, not_verified, no_restore_test, keys_not_escrowed (an encrypted backup without an escrowed key), copy_missing (copies of the newest good backup still not done six hours after it finished), dr_same_region (no copy target in a known region other than the primary's), dr_same_credentials (every copy target reachable with the primary's access key, default credentials or Cloudflare account) dr_drill_stale (no DR drill passed in the last 30 days) or dr_unlocked_copy (a job requiring locked copies, itself or through security.require_locked_copies, has a copy target without Object Lock; dr.unlocked_copies lists them); ok otherwise |
streams lists the PITR streams with their state, status and reasons; rto estimates a whole-instance point-in-time restore to the newest point of the stream's open window: {seconds, base_bytes, oplog_bytes, oplog_entries, source, samples, base_bytes_per_second, replay_bytes_per_second, replay_entries_per_second, replay_model}, where source is measured (the stream's five newest completed restores and chain tests: the base at their rate, the replay from a fit of their replay times to entries and bytes, replay_model entries_and_bytes, entries or bytes), chain_test or default (the replay takes the longer of its bytes at 4 MiB/s and its entries at 2,000 per second) (details); it is absent without an open window.
The report reads each kind of record (jobs, newest and verified backups, restore tests, restores, breaches, join times) with one query, whatever the number of jobs. The dashboard's Overview shows it as the Recovery readiness table, and its Attention needed list reports the jobs whose objective is missed from the same data.
Point-in-time recovery streams¶
A PITR stream collects the oplog of one replica set connection into encrypted chunks and takes base backups of the whole instance. Reading streams needs the read scope; creating, changing and deleting them needs admin; POST /api/v1/pitr/streams/{id}/base needs operator. The answers carry chunk metadata only, never oplog contents.
POST /api/v1/pitr/streams and PATCH /api/v1/pitr/streams/{id} take:
| Field | Default | Meaning |
|---|---|---|
connection_id |
(required) | The replica set connection; one stream per connection, and it cannot change |
target_id |
the default target | Storage target of chunks and base backups |
enabled |
true |
Run the collector |
base_cron |
0 2 * * * |
Schedule of base backups (five fields or a descriptor such as @daily) |
base_keep_count, base_keep_days |
7, 14 |
Keep this many bases, or bases this many days old; pinned bases and the newest eligible one are always kept |
oplog_max_days |
0 (unset) |
Delete chunks older than this many days even when that shortens the window (privacy) |
chunk_seconds |
60 |
Chunk interval, 15-900 seconds |
base_on_gap |
true |
Take a base backup as soon as a gap breaks the chain |
read_preference |
secondaryPreferred |
Read preference of oplog reads |
chain_test_cron |
empty (off) | Schedule of chain tests; "" turns them off |
chain_test_connection_id |
empty (the stream's connection) | Connection chain tests restore into; another server spares production the load of a full restore |
copy_targets |
none | Up to 3 further storage targets the stream's oplog chunks and base backups are copied to (not target_id); [] removes them. While security.require_locked_copies is on, each must have S3 Object Lock (else 400), and a base backup whose copy target has none is refused |
With copy_targets, base backups get copies like a job's backups, and the copy queue copies every live oplog chunk to each copy target, checks it against the chunk's SHA-256, and purges the copy once the chunk is purged (and the copy's Object Lock ended). A point-in-time restore (POST /api/v1/restores with pitr) takes source_target_id: a copy target where the base and every chunk of the range have a completed copy (a copy chain), else 400 naming the first gap. Without it the restore reads the primary, and reads the first complete copy chain instead when the primary base or a chunk is missing or cannot be read; the restore record then has source_target_id and source_fallback.
Creating or enabling a stream is refused with 400 unless the connection is a replica set, its user may read local.oplog.rs (backup, read on local or a custom role) and backup encryption is on with age keys: the oplog keeps deleted data, so PITR data is always encrypted.
A database row's rpo.source is pitr when the stream's durable lag is the better recovery point: the oplog up to it is captured and can be restored to a point in time.
GET /api/v1/pitr/streams/{id} returns {stream, state, running, live, lag_seconds, headroom_seconds, durable_rpo_seconds, windows, chains, bases, chain_breaks}. windows lists [{chain_id, open, start, end, start_time, end_time, bases}], one per chain with an eligible base: a gap or a divergence splits them. state.status is running, failed (with last_error) or stopped.
DELETE refuses an enabled stream (409). For a disabled stream with chunks it ends the open chain, deletes every chunk with the delete grace period and answers 409 until the purge removed them; delete it again then. Base backups stay as backups.
POST /api/v1/pitr/streams/{id}/chain-test (admin) starts a chain test now and answers 202 with its restore (pitr.chain_test: true); 409 when the stream has no two eligible bases in one window, the newer with a manifest, or when its preflight fails (with the checks, like a restore).
Point-in-time restores¶
POST /api/v1/restore and POST /api/v1/restores/preflight restore to a point in time when the body has pitr instead of backup_id:
{"pitr": {"stream_id": "str_…", "at": "2026-10-05T14:30:00Z"}, "databases": ["shop"]}
{"pitr": {"stream_id": "conn_…", "ts": {"t": 1791210600, "i": 3}}}
| Field | Meaning |
|---|---|
pitr.stream_id |
The PITR stream, or the ID of its connection |
pitr.at |
RFC 3339 time: every write up to and including that second (the primary's clock) is restored; the replay stops at (at+1):0 |
pitr.ts |
Exact oplog position instead of at: every write before it is restored |
databases |
Restore only these databases (default: every database but admin, config and local, which are never restored) |
target_connection_id |
Another connection to restore into (default: the stream's) |
force |
Start although a preflight check failed |
pitr.in_place |
Replace the original databases with the restored state (in place); admin only |
pitr.confirm_in_place |
With in_place: the databases it replaces, comma-separated, exactly those of databases |
pitr.keep_clone |
With in_place: keep the clones after a successful swap |
Every restored database goes into a new database <db>_rescue_<YYYYMMDD_HHMMSS>_<id>. Point-in-time restores and their preflight need the operator scope (403 for read keys); a restore into another connection than the stream's (target_connection_id) needs admin, like for backups. The in-place options of a backup restore (safe_clone: false, confirm_in_place, target_database) are refused with 400 (use pitr.in_place), as are backup_id, dry_run, drop_target, selected_collections and restore_users_and_roles, and databases without pitr. A target outside every window, behind a chain break or a chunk that failed verification is refused with 422; a missing key for the encryption of the base or a chunk with 422 too (ErrKeyRequired); a server without point-in-time support answers 503.
The restore record has pitr: {stream_id, chain_id, base_id, target_time, limit, chunks, oplog_bytes, base_bytes, databases, clone_suffix, clones, ops_replayed, ops_applied, ops_unverified, chain_test, base_seconds, replay_seconds}: the plan chosen, the clone databases (recorded before they are written; clean-up drops exactly these), the operations the oplog filter wrote and mongorestore applied, and how long the base and the replay took (set on a completed restore; they give the measured rates). A mismatch of the two fails the restore and keeps the clones; ops_unverified: true says mongorestore printed no count. The clone suffix is _rescue_<YYYYMMDD_HHMMSS>_<id> with a random id. A database in databases the base does not hold, or a MongoRescue clone (_rescue_ in its name), is refused with 400. source_database is * (or the selected databases) and target_database is *<clone_suffix> (or the clones).
The preflight checks pitr_chain, connection, server_version, target_database, privileges (the replay roles), disk_space (base plus oplog) and tools_version (mongorestore 100.12 or newer); a plan that does not reach the target fails pitr_chain instead of the call. Its result adds pitr: {base_id, base_started_at, base_consistent_at, base_bytes, oplog_bytes, chunks, unverified_chunks, target_time, limit, clone_suffix, estimated_seconds, estimate_from}; estimate_from is measured when the stream's recent restores and chain tests gave the rates, chain_test when only a chain test of an earlier release did, default otherwise.
In-place point-in-time restores¶
{"pitr": {"stream_id": "str_…", "at": "2026-10-05T14:30:00Z", "in_place": true, "confirm_in_place": "shop,crm"}, "databases": ["shop", "crm"]}
"in_place": true makes the restore end with the original databases holding their state at the target (procedure, downtime, space and recovery): it restores into clones and verifies them as above, takes a safety backup of each original (a backup with "label": "pre-pitr-in-place" and restore_id), pauses the stream (its chain ends), copies each clone over its original (mongodump | mongorestore --drop, then drops the collections the clone does not have), drops the clones unless keep_clone, and enables the stream again with a new chain and a new base backup.
- It needs the admin scope (403 otherwise, also for the preflight), a non-empty
databases(400) andconfirm_in_placenaming exactly those databases, in any order (400,in-place point-in-time restore not confirmed). It is refused with 400 withtarget_connection_idnaming another connection than the stream's, with 409 while a backup or restore of one of the databases, a base backup of the connection, or a whole-instance restore into it, runs, and with 409 (in-place PITR is not available in HA mode yet, also for the preflight) while active-passive high availability is on. - With the two-person rule on it answers
202withapproval_requiredand an approval request of actionpitr_in_place; the approval starts the same request. - MCP never sends it:
pitr_restorealways restores into safe clones. - The record has
in_place: trueandpitr.in_place: {databases, stage, safety_backup_ids, stream_was_enabled, stream_paused, stream_resumed, swap_started, swapped, keep_clone, new_base_id};stageisrestoring,safety_backup,pausing,swapping,resumingordone, and the run's phase issafety_backupandswappingduring those steps. Once the clones are complete the run can no longer be cancelled. - A failure before the swap leaves the originals untouched and drops the clones. A failure during the swap keeps the clones and the safety backups and names them, and which databases were swapped, in
error_message. - The preflight adds the check
in_place(fails while one of those runs is going) and runs the connection's post-restore commands against the clones before the swap. - Events:
security.destructive_action(action: "pitr_in_place") when it starts, the usualrestore.*andrestore.in_place_pitr(status: "succeeded"or"failed",databases,safety_backup_ids) when it ends.
Bulk actions¶
POST /api/v1/backups/bulk, POST /api/v1/restores/bulk and POST /api/v1/jobs/bulk run one action on many records. They share one body:
{"action": "delete", "filter": {"status": "failed", "database": "shop", "from": "2026-09-01T00:00:00Z"}, "dry_run": true}
{"action": "delete", "ids": ["bkp_a", "bkp_b", "…the dry run's actionable_ids"], "confirm_count": 312}
{"action": "pin", "filter": {"job_id": "job_shop"}, "note": "audit 2026", "confirm_count": 40}
- Exactly one of
ids(duplicates count once; each at most 256 bytes) andfilterselects the items. Destructive actions (delete) take a filter only withdry_run: true: their real run must name the exact IDs, the dry run'sactionable_ids, and a filter is refused with400, because a filter evaluated later could match other records than the ones that were shown.filtertakes the names and formats of the list endpoints' query parameters (backups:status,database,connection_id,job_id,trigger,retry_of,from,to,q; restores:status,database,backup_id,from,to,q; jobs:database,connection_id,q,enabled,schedule,last_status, as in listing jobs) and selects every match; paging does not apply. A selection may hold at most 10,000 items (422otherwise); the body may be about 5 MiB, enough for 10,000 IDs of the maximum length. Unknown fields, also insidefilter, and filter fields that do not apply to the resource are refused with400, so a misspelt filter can never widen the selection. dry_run: truechanges nothing and answers{matched, actionable, skipped: [{id, reason, params, detail}], total_size_bytes, actionable_ids}.reasonis a stable code (see the table below) that clients translate;paramsfill it in (jobforlast_good_backupandlast_verified,statusforin_progressandnot_running) anddetailsays the same in English.- A real run plans the selection again. When more than 10 items are affected it needs
confirm_countequal to the dry run'sactionable; a setconfirm_countmust always match. Otherwise it is refused with409and nothing changes, so a stale client can never act on more than it showed. Items are then processed one at a time through the same use cases as the single-item routes (logs, events and the shared-archive rule included). Every deletion of a backup (single, bulk, and retention) holds a lock per job (per connection and database for backups without a job), and bulk deletes decide the protections again inside it on the stored record, right before deleting it; an item that became protected meanwhile (pinned, newly the last good or last verified backup) is added toskippedinstead. The record and, when no other record names it any more, its archive are deleted under a lock per archive, so records sharing an archive never strand it. The answer addssucceeded,failedandresults: [{id, ok, error, warning, detail}]in order. Items are not started once the request ends; an item that started is finished. - One
bulk.completedevent (not selectable in notification rules) summarises every real run, andmongorescue_bulk_operations_total{resource,action}andmongorescue_bulk_items_total{resource,action,outcome}count them. Every real run also writes an audit entry (toolbulk backups delete and so on, with the actor, the counts and a SHA-256 of the processed IDs, sorted and one per line), whether it was started from a session or with an API key.
GET /api/v1/bulk/actions (read) lists the available actions as {resource, name, scope, destructive, allowed}; allowed says whether the caller has the scope. The route needs operator; each action needs the scope of its single-item route:
| Resource | Action | Scope | Skipped (reason) |
|---|---|---|---|
| backups | delete (soft, optional reason) |
admin | in_progress, pinned, already_deleted, last_good_backup (the newest completed backup of a job), last_verified (the job's newest verified scheduled backup, which retention keeps), not_found |
| backups | verify |
operator | not_verifiable (not completed or no checksum) |
| backups | pin (optional note) |
operator | already_pinned |
| backups | unpin |
admin | not_pinned |
| backups | cancel |
operator | not_running |
| restores | delete (history records only; restored databases are not touched) |
admin | in_progress |
| restores | cancel (in-place restores need admin per item) |
operator | not_running |
| jobs | enable, disable |
admin | already_enabled, already_disabled |
| jobs | run_now |
operator | — (a database that is already being backed up fails the item) |
| jobs | delete (their backups are kept) |
admin | — |
Every action also skips IDs that name no record (not_found). A bulk delete never removes a job's last good backup, a pinned backup or the backup retention keeps as the last verified one; delete those one at a time (after unpinning) if you really mean to. Deleting backups in bulk is soft like a single delete: every result's detail says until when the backup is recoverable (recoverable until …), the archives stay for the grace period, and each deletion can be undone. With the two-person rule a real run of delete or unpin answers 202 with one approval request for all its actionable IDs; the approved run re-checks every item (delete protection).
The dashboard selects rows with checkboxes (shift-click for a range, the header box for the page, then Select all N matching the filters, which sends the filter), shows the dry run with the skipped items grouped by reason, asks to type the count for destructive actions on more than 10 items, and runs the confirmed IDs in batches of 25 with a progress bar.
Delete protection¶
Deletes are soft, lowering a protection waits for the delete grace period, and with the two-person rule destructive actions wait for a second administrator. The threat model is in security.md.
DELETE /api/v1/backups/{id}marks the backupdeletedand answers{deleted_id, status, deleted_at, purge_after, archive_deleted: false}; the archive stays untilpurge_after. Backup records gaindeleted_at,deleted_by,delete_approved_by,delete_reason,purge_after,status_before_deleteand, once purged,purged_at. A deleted backup cannot be restored (409, also for the preflight), verified, pinned or deleted again (409);POST /api/v1/backups/{id}/undelete(admin) gives it back its earlier status.- Every ten minutes the scheduler purges the deleted backups whose grace period ended: the archive is removed (unless another live record names it) and the record becomes
purged. The grace period ismax(purge_after, deleted_at + security.delete_grace_days), so raising it protects earlier deletions. Retention deletes softly too (deleted_by: retention). On a target with S3 Object Lock a backup whose archive is still locked staysdeleteduntil itsretain_untiltoo, and the purge then deletes every version of the archive. A failed backup whose uploaded artifact is locked carriesarchive_cleanup_pendinguntil the purge has deleted it. Backup records carrystorage_version_id,retain_until,object_lock_modeandlegal_hold; pinning on a target withlegal_hold_on_pinsets an S3 legal hold and unpinning lifts it (502when the hold cannot be changed; nothing changes then). - A shorter job retention (
PUT /api/v1/jobs/{id},POST /api/v1/jobs) and a lowersecurity.delete_grace_daysare stored as pending changes and applied at theireffective_at, the end of the current grace period. The answers carrypending_retentionandpending_changes;GET /api/v1/pending-changeslists them andDELETE /api/v1/pending-changes/{id}(admin) cancels one, keeping the current protection. - With
security.require_second_approver, deleting backups (single or bulk), unpinning (single or bulk), deleting a storage target, shortening retention, lowering the grace period and turning the rule off answer202 Accepted:
{"success": true, "message": "a second administrator must approve this action: delete backup bkp_shop_20260924_030000_3f9a1c2e (db shop)",
"data": {"approval_required": true, "approval": {"id": "apr_5c2e9b1f0a7d3e46", "action": "delete_backup", "status": "pending",
"summary": "delete backup bkp_shop_20260924_030000_3f9a1c2e (db shop)", "subject": "bkp_shop_20260924_030000_3f9a1c2e",
"requested_by": "API key ci", "requested_by_user_id": "usr_1a2b3c4d", "requested_via": "api_key",
"created_at": "2026-10-05T09:00:00Z", "expires_at": "2026-10-08T09:00:00Z"}}}
Another administrator approves it with POST /api/v1/approvals/{id}/approve from a dashboard session (API keys, the requester, non-admins and administrators who got their role after the request get 403), which runs the action and answers the request with status: "approved" and a result, or status: "failed" and an error; POST /api/v1/approvals/{id}/reject rejects it. Requests expire after 72 hours (409 afterwards). Actions: delete_backup, bulk_delete_backups, unpin_backup, bulk_unpin_backups, delete_storage_target, shorten_retention (bound to the job's subject_created_at), lower_delete_grace, shorten_metadata_retention, restore_drop_target (POST /api/v1/restore in place with drop_target), pitr_in_place (an in-place point-in-time restore; subject is the stream), disable_second_approver, grant_admin_role, grant_admin_api_key, demote_admin, delete_admin, reset_password and oidc_admin_mapping. Turning the rule on with fewer than two administrators answers 409; turning it off while fewer than two remain is a pending change (disable_second_approver) instead. An API key without a creator gets 403 for every request while the rule is on.
- With the rule on, POST /api/v1/users with role: "admin" answers 202 with the user (created as a viewer) and the request; PUT /api/v1/users/{id}/role to or from admin and DELETE /api/v1/users/{id} of an administrator answer 202; POST /api/v1/api-keys with scope: "admin" answers 202 with the key (scope operator until approved) and its plaintext key; PUT /api/v1/users/{id}/password for another user needs a session (403 for API keys) and answers 202. A PUT /api/v1/settings whose oidc change can grant admin returns the request in approvals_requested and leaves the oidc group unchanged (400 when it carries a new client_secret).
- Creating or moving a storage target onto the location of another (the same or a nested directory, or the same endpoint and bucket with an equal or nested prefix) answers 409. Failed and cancelled backups block a target's deletion only while their object may exist.
- The audit log entries of these requests carry the targets protection (soft_deleted, undeleted, approval_requested, approved, rejected, pending_change_cancelled), approval_id, purge_after and effective_at; purges and applied pending changes are recorded as SYSTEM backup.purge and SYSTEM protection.apply_pending. The events security.destructive_action and security.approval_requested can be selected by notification rules.
Cancelling a run¶
POST /api/v1/backups/{id}/cancel and POST /api/v1/restores/{id}/cancel (operator scope) stop a running backup or restore, whether it was started through the API, the dashboard, MCP or the scheduler. They wait up to 5 seconds for the run to stop and answer 200 OK with the final record ("status": "cancelled"); a run that takes longer to stop is answered with 202 Accepted and its in-progress record (progress.cancelling is true): poll it until the status is cancelled. A run cancelled while it is still queued never starts its tool. mongodump or mongorestore is terminated with its process group (SIGTERM, then SIGKILL), so no tool process outlives the run.
- A cancelled backup deletes its partial artifact (an S3 multipart upload is aborted, a local temporary file removed) and records neither size nor checksum.
- A cancelled safe-clone restore drops the partially restored
<db>_rescue_<timestamp>_<id>database; the message says so (or asks to drop it by hand if that failed). - A cancelled in-place restore cannot be undone: the target may be left partially restored. The record's
warninganderror_messagesay so loudly. Cancelling one needsadmin, like starting it. - A run cancelled before its tool started leaves the target untouched.
The record gets "status": "cancelled", cancelled_by (the username, API key <name>, ... via MCP, or system for the desktop app's force quit) and cancelled_at. Cancelled runs are not failures: they are not counted in failed_backups or the last 24 hours' failures, emit backup.cancelled / restore.cancelled instead of the failure events (notification rules may subscribe to them) and are counted with status="cancelled" in the metrics.
| Status | When |
|---|---|
200 OK |
The run stopped; the body is its final record |
202 Accepted |
The cancellation was requested and the run is still stopping |
403 Forbidden |
The key may not cancel this run (an in-place restore needs admin) |
404 Not Found |
No backup or restore {id} |
409 Conflict |
The run is not running (it finished, or it is not active in this process), or it is already finishing: its tool completed and only the outcome is being recorded |
Once mongodump or mongorestore has completed successfully, the run is past the point of no return: a cancellation that arrives then is refused with 409, and one that raced with the tool's exit does not undo the finished backup or restore. If a check after the tool fails (checksum mismatch, documents that failed to insert) while a cancellation was requested, the run is recorded as cancelled, and a safe clone is dropped.
Run logs¶
Every backup and restore writes a log file, <datadir>/logs/<id>.log: the complete output of mongodump / mongorestore and MongoRescue's own timestamped [mongorescue] phase lines (start, tool arguments without the connection, phases, outcome, cancellation). Each line is redacted before it is written: connection strings and secrets are masked, document values in duplicate-key errors are replaced, and tool lines are cut to 300 bytes. A log never exceeds 5 MiB: the first 1 MiB and the newest output are kept around a … N bytes truncated … marker, and the run streams its log to disk without holding it in memory.
GET /api/v1/backups/{id}/log and GET /api/v1/restores/{id}/log (read scope) return the log as text/plain. With ?tail=N they return the last N lines (1-10000), also while the run is still writing them; without it, the whole file with Content-Disposition: attachment. Runs from older releases have no log (404). Logs are deleted with their backup record, when retention prunes the backup, and after general.log_retention_days (default 30; 0 keeps them).
Live progress¶
GET /api/v1/runs/active (read scope) returns the progress of every running backup and restore, oldest first; running records in GET /api/v1/backups and GET /api/v1/restores carry the same object as progress:
{"id": "bkp_shop_20261001_100000_3f9a1c2e", "kind": "backup", "job_id": "job_shop", "database": "shop",
"phase": "dumping", "percent": 41.2, "bytes": 52428800, "documents": 41200,
"current_collection": "shop.orders", "collections_done": 3, "collections_total": 5,
"bytes_per_second": 10485760, "started_at": "...", "updated_at": "...",
"phases": {"queued": "...", "started": "..."}}
Bytes count what was streamed to storage (backups) or read from it (restores, with total_bytes the archive size). Documents, collections and the backup percentage come from the tools' progress lines (the percentage covers the collections the tool has started so far); a restore's percentage is the share of the archive read. phase is queued, waiting (a backup waiting for a slot of its connection's max_concurrent_backups), dumping, verifying, restoring, safety_backup and swapping (an in-place point-in-time restore), finishing or cancelling. Every record also stores the timestamps of its phases (queued, started, dump_done, upload_done, verify_done, restore_done, finished); records from older releases have queued and finished only. The dashboard polls every 2 seconds while something runs, shows a progress bar on running rows and, in the details dialog, the progress, a phase timeline and a Log tab that follows the log of a running run.
Asynchronous operations¶
Every backup record has a trigger: scheduled (a cron run of job_id), on_demand (POST /api/v1/jobs/{id}/run), manual (POST /api/v1/backups) or mcp (an assistant's start_backup or run_job). It is set by the server, never taken from the request, and retention only prunes a job's scheduled backups (configuration.md).
POST /api/v1/backups, POST /api/v1/backups/{id}/retry, POST /api/v1/jobs/{id}/run and POST /api/v1/restore return 202 Accepted as soon as the operation has started. The response body is the new backup or restore record with "status": "in_progress"; poll GET /api/v1/backups or GET /api/v1/restores until it becomes completed, failed or cancelled. Operations keep running if the client disconnects; cancel them to stop them. Verifications (POST /api/v1/backups/{id}/verify), restore tests, sweeps and orphan imports run in the background the same way; their results land on the backup record (verified_at, verification, last_restore_test) or in GET /api/v1/jobs/{id}/restore-tests and GET /api/v1/integrity.
Backup records carry the trust fields verified_at, verification (ok, mismatch, error) and verification_error; pinned, pin_note, pinned_at, pinned_by; has_manifest; imported and imported_at; missing_since for status: "missing"; and last_restore_test. See verification.md.
| Status | Meaning |
|---|---|
202 Accepted |
The operation started |
400 Bad Request |
Invalid request, for example a missing connection_id, an unknown storage_target_id or an unconfirmed in-place restore |
401 Unauthorized |
Not signed in and no valid API key |
403 Forbidden |
The API key's scope does not allow the operation (for example a read key starting a backup, or an operator key restoring in place) |
409 Conflict |
A backup of the same database on the same connection, or a restore into the same target, is already running |
422 Unprocessable Entity |
The backup is encrypted and no decryption key is configured |
503 Service Unavailable |
The server is shutting down, or the desktop app is quitting once its running backups and restores finish (MongoRescue is shutting down) |
Audit log¶
GET /api/v1/audit/events (admin) returns the hash-chained audit log of every action: each mutating REST request from a session or an API key, sign-ins and failed sign-ins, sign-out, setup, the recovery kit and audit log downloads, MCP tool calls and system actions. Entries name the actor, the route pattern, the path parameters, the HTTP status and outcome, the client address and the user agent, never request bodies. The format, the hash chain, retention and forwarding are described in audit.md.
| Parameter | Meaning |
|---|---|
actor |
User ID, user name, API key ID or API key name (exact) |
actor_kind |
user, api_key, system or anonymous |
action |
Part of the action, case-insensitive (jobs, DELETE, MCP start_backup) |
result |
ok, error, denied or rate_limited |
since, until |
RFC 3339 times; since inclusive, until exclusive |
limit |
Page size, 1-1000 (default 100) |
before_id |
Return entries older than this ID: the next_before_id of the previous page |
{"events": [{"id": 1842, "time": "2026-10-02T12:30:45.123456789Z", "actor_kind": "api_key", "actor_user_id": "",
"actor_name": "", "actor_key_id": "key_9f8e7d6c5b4a3928", "actor_key_name": "ci", "action": "DELETE /api/v1/backups/{id}",
"targets": {"id": "bkp_shop_20260901_030000_1a2b3c4d"}, "status": 403, "outcome": "denied", "client_ip": "192.0.2.10",
"user_agent": "curl/8.7.1", "count": 1, "hash": "…"}],
"next_before_id": 1842,
"forwarding": {"enabled": true, "queued": 0, "sent": 1840, "failed": 2, "dropped": 0}}
GET /api/v1/audit/events/export takes the same filters (no paging) and streams every matching entry as one JSON object per line, oldest first (application/x-ndjson, as a mongorescue-audit-<time>.jsonl attachment). GET /api/v1/audit/events/verify walks the chain from its anchor and answers {ok, checked, anchor: {last_id, last_hash, last_time, pruned_at}, head_id, head_hash, broken_id, reason, warning, retention_days, verified_at}; ok is false and broken_id names the first entry that was changed, removed or reordered when the chain is broken, and warning is set when the anchor moved further than the retention explains (audit.md).
curl -s "http://localhost:8080/api/v1/audit/events/export?since=2026-10-01T00:00:00Z" -H "Authorization: Bearer $KEY" > audit.jsonl
curl -s http://localhost:8080/api/v1/audit/events/verify -H "Authorization: Bearer $KEY"
API key activity¶
GET /api/v1/audit (admin) returns the most recent activity of API keys, newest first: MCP tool calls, resource reads (tool is resources/read, the URI in arguments.resource) and prompt requests (prompts/get), and REST requests to /api/... authenticated by an API key. Browser sessions and /metrics scrapes are not audited.
{"id": 42, "time": "2026-09-25T10:15:03Z", "api_key_id": "key_1a2b3c4d5e6f7a8b", "api_key_name": "claude-desktop",
"transport": "stdio", "tool": "restore_to_safe_clone", "arguments": {"backup_id": "bkp_shop_20260924_030000_3f9a1c2e", "verify": true},
"result": "ok", "duration_ms": 18, "count": 1}
A REST entry has transport rest, the route pattern as tool (never the raw path, and (no route) when none matched), the path parameters as arguments (request bodies and queries are not stored) and the response status in http_status:
{"id": 43, "time": "2026-09-25T10:16:10Z", "api_key_id": "key_9f8e7d6c5b4a3928", "api_key_name": "ci",
"transport": "rest", "tool": "DELETE /api/v1/backups/{id}", "arguments": {"id": "bkp_shop_20260901_030000_1a2b3c4d"},
"result": "denied", "error": "Forbidden", "duration_ms": 0, "http_status": 403, "count": 1}
result is ok, error (the call ran and failed; error holds the message the assistant saw, or the status text for REST), denied (scope too small; 403 for REST) or rate_limited (429). Argument values of secret-looking keys and credentials inside strings are masked before they are stored. The newest 10,000 entries are kept. Refused and polling clients cannot flush the log: repeated denied calls of one key and tool, repeated rate_limited calls of one key, and repeated REST reads or failed REST requests of one key, route and status are merged into one entry per 10 seconds whose count is the number of calls it stands for (1 for every other entry). A merged entry keeps the distinct values of each argument, up to 20 (for example "arguments": {"id": ["bkp_a", "bkp_b"]}), and sets "_more_values": true when it dropped some.