Schedules

Automatic snapshots: when they are taken, and how long they are kept.

Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.

GET/computers/{id}/schedule

Get the snapshot scheduleviewer+

The window automatic snapshots are taken in. There is deliberately no "last run" here: the scheduler’s own bookkeeping reads like backup history and lies in both directions. Snapshots carry real capture times — read GET /snapshots for a freshness check.

A computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot, so an old newest snapshot on one is not a missed window.

This says when they are TAKEN and not how long they survive. GET /retention is the other half.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials. SDK setup first GETs the computer. Python start/stop/suspend/restart also refresh it with another GET; TypeScript refreshes only if the action returns an acknowledgement.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/computers/vm-a1b2c3d4e5f6/schedule' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "vm-a1b2c3d4e5f6"
    c = client.computers.get(id)
    result = c.schedule()
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "vm-a1b2c3d4e5f6";
const c = await client.computers.get(id);
const result = await c.schedule();
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "enabled": true,
  "hour": 3,
  "minute": 30,
  "tz": "Europe/London"
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, PUT, DELETE, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The computer id.

Response

The schedule.

Response fields

enabledalwaysboolean

Whether a capture is taken in this window. A computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot.

houralwaysinteger

0–23, in tz.

minutealwaysinteger

0–59.

tzalwaysstring

IANA zone name. UTC when unset.

PUT/computers/{id}/schedule

Set the snapshot schedulemember+

How long these are kept is your plan’s retention, not a field here — read it at GET /retention, which is what tells you how many of the snapshots this schedule takes will still be there next month.

The PUT replaces the whole window: hour and minute default to 0 and tz to UTC, so send all three again when only toggling enabled. A computer being copied or moved answers 409; send it again once that finishes.

A computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot, so an old newest snapshot on one is not a missed window.

AUTOMATIC CAPTURES COUNT AGAINST YOUR SNAPSHOT STORAGE ALLOWANCE, and can be skipped when it is full. Their bytes count toward the same allowance a manual capture does, so they can bring an account to the point where a manual capture answers 402; and a scheduled capture skipped for storage is not taken. Nothing is deleted to make room for it. It is retried through the rest of that day and is taken once there is room — retention ageing out an older automatic snapshot, or you deleting one. Room is measured across every computer on the account, normally once an hour, and each host is sent its share. A measurement is sent only when every host answers it, and until a complete one reaches a host, that host keeps the share it was last sent. So a skipped capture usually waits up to an hour after room appears, longer while any host cannot be measured, and one can still land shortly after the allowance fills. Each plan’s allowance is sized to hold the history its retention keeps, so a schedule meets it only when manual snapshots or unusually busy disks have used that room.

A NEW ACCOUNT OR A NEWLY PLACED COMPUTER CAN ALSO BE SKIPPED UNTIL THE NEXT COMPLETE MEASUREMENT. Each host’s share of the room left in the allowance is in proportion to the account’s computers on it. Until a complete measurement reaches the host, a new account has no share anywhere, and a computer created on or moved to a host where the account had no computers has none there, so its scheduled capture is skipped even with nothing stored. That is normally within the hour, but a host that cannot be measured, or a share that cannot be delivered, can stretch it to hours or days. A skipped capture is retried through the rest of that day like any other, so a longer wait can cost that day’s capture.

One automatic capture a day, and an edit does not add another. Changing the time or tz after today’s capture has been taken moves the window from tomorrow on; it does not open a second one today, and neither does removing the schedule and setting it again in the same tz.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials. SDK setup first GETs the computer. Python start/stop/suspend/restart also refresh it with another GET; TypeScript refreshes only if the action returns an acknowledgement.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X PUT "${base_url%/}"'/computers/vm-a1b2c3d4e5f6/schedule' \
  -H "Authorization: Bearer $MANDALA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-raw '{"enabled":true,"hour":3,"minute":30,"tz":"Europe/London"}'

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import json, os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "vm-a1b2c3d4e5f6"
    body = json.loads("{\"enabled\":true,\"hour\":3,\"minute\":30,\"tz\":\"Europe/London\"}")
    c = client.computers.get(id)
    result = c.set_schedule(**body)
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "vm-a1b2c3d4e5f6";
const body = {"enabled":true,"hour":3,"minute":30,"tz":"Europe/London"};
const c = await client.computers.get(id);
const result = await c.setSchedule(body);
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "enabled": true,
  "hour": 3,
  "minute": 30,
  "tz": "Europe/London"
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, PUT, DELETE, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The computer id.

Request body

enabledrequiredboolean
hourinteger

0–23, in tz. 0 when left out.

minuteinteger

0–59. 0 when left out.

tzstring

IANA zone name, e.g. Europe/London. Defaults to UTC. A name the host’s zone database does not know is a 400.

Response

The schedule as stored.

Response fields

enabledalwaysboolean

Whether a capture is taken in this window. A computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot.

houralwaysinteger

0–23, in tz.

minutealwaysinteger

0–59.

tzalwaysstring

IANA zone name. UTC when unset.

DELETE/computers/{id}/schedule

Remove the snapshot schedulemember+

Stops automatic snapshots. Snapshots already taken are kept — but the ones this schedule took are still automatic ones, so your plan’s retention (GET /retention) goes on ageing them out. Take a copy by hand of anything you mean to keep past it. A computer being copied or moved answers 409; send it again once that finishes.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials. SDK setup first GETs the computer. Python start/stop/suspend/restart also refresh it with another GET; TypeScript refreshes only if the action returns an acknowledgement.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X DELETE "${base_url%/}"'/computers/vm-a1b2c3d4e5f6/schedule' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "vm-a1b2c3d4e5f6"
    c = client.computers.get(id)
    result = c.clear_schedule()
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "vm-a1b2c3d4e5f6";
const c = await client.computers.get(id);
const result = await c.clearSchedule();
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "enabled": false,
  "hour": 0,
  "minute": 0,
  "tz": "UTC"
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, PUT, DELETE, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The computer id.

Response

The cleared schedule.

Response fields

enabledalwaysboolean

Whether a capture is taken in this window. A computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot.

houralwaysinteger

0–23, in tz.

minutealwaysinteger

0–59.

tzalwaysstring

IANA zone name. UTC when unset.

GET/retention

Read snapshot retentionviewer+

How long automatic snapshots are kept. Your plan decides this and there is no write here — which is the answer to the sentence on PUT /computers/{id}/schedule that says retention decides how long these live without saying what it is.

AUTOMATIC CAPTURES COUNT AGAINST YOUR SNAPSHOT STORAGE ALLOWANCE. Their bytes count toward it, so they can bring an account to the point where a manual capture answers 402, and a scheduled capture can be skipped rather than taken when it is full, or on a new account or a newly placed computer until the next complete hourly measurement reaches its host — see PUT /computers/{id}/schedule. Retention is what ages them out, and each plan’s allowance is sized to hold what its retention keeps.

It is a grandfather-father-son window, not an age. What survives is the NEWEST automatic snapshot in each of the last daily DAYS THAT HAVE ONE, the last weekly such ISO weeks and the last monthly such calendar months; every other automatic one goes. So a daily schedule on {"daily": 7, "weekly": 4, "monthly": 12} leaves you 7 days of dailies, then one a week going back a month, then one a month going back a year — at most twenty-three snapshots per computer in a steady state rather than 365, since one capture can be the newest of its day, its week and its month at once.

COUNTED IN PERIODS THAT CONTAIN A SNAPSHOT, NOT IN CALENDAR TIME, and the difference shows up the moment captures stop. Turn a schedule off for a month and come back: the last daily days of history are still there, because those are the last daily days that HAVE a snapshot and not the last daily days on the calendar. Nothing ages out for the passage of time alone, and the most recent automatic capture is kept whatever these numbers say.

PERIOD BOUNDARIES ARE UTC — days, ISO weeks and calendar months are all cut in UTC, whatever tz your schedule runs in. A capture at 23:30 on a Sunday in America/Chicago lands on Monday UTC and so counts toward the following ISO week, which is the one place a schedule set in local time and a window measured in UTC visibly disagree.

A zero turns that tier off, and all three zero means your plan grants no retained automatic history at all — which is what an account with no active subscription reads.

THAT DOES NOT MEAN YOUR EXISTING SNAPSHOTS GO. An account whose subscription lapses keeps a rolling week of its automatic snapshots — deliberately more than the all-zero answer above promises, so that an expired card is not also a lost backup. It is a grace period and not an entitlement: it is not guaranteed, it is not readable here, and it can change. If there is a capture you need to survive any window at all, take one by hand — those are never aged out.

ONLY SNAPSHOTS WITH auto: true ARE TOUCHED. One you took yourself with POST /computers/{id}/snapshots is yours until you delete it, whatever this says, and taking one by hand is how you keep something past the window. The state this ages out is durable — see state on Snapshot.

These numbers are your plan’s, so they change when your subscription does. They belong to the ACCOUNT but are applied PER COMPUTER: the same window is used for every computer you own, and each one keeps its own set. Two computers on {"daily": 7, "weekly": 4, "monthly": 12} keep up to twenty-three snapshots each, not twenty-three between them.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/retention' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    result = client.snapshots.retention()
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const result = await client.snapshots.retention();
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "daily": 7,
  "weekly": 0,
  "monthly": 0
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Response

What your plan keeps.

Response fields

dailyalwaysinteger

How many of the last days keep their newest automatic snapshot.

weeklyalwaysinteger

The same, over ISO weeks.

monthlyalwaysinteger

The same, over calendar months.

Failures

403

A suspended account may still make this read: a 403 here is about the credential or the person holding it, never the account’s suspension.