curl --request POST \
--url https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "nightly",
"interval_minutes": 1440,
"retention_count": 7,
"description": "Nightly snapshots of the database volume",
"retention_days": 30,
"enabled": true,
"tags": {
"environment": "production",
"team": "backend"
}
}
'import requests
url = "https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies"
payload = {
"name": "nightly",
"interval_minutes": 1440,
"retention_count": 7,
"description": "Nightly snapshots of the database volume",
"retention_days": 30,
"enabled": True,
"tags": {
"environment": "production",
"team": "backend"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'nightly',
interval_minutes: 1440,
retention_count: 7,
description: 'Nightly snapshots of the database volume',
retention_days: 30,
enabled: true,
tags: {environment: 'production', team: 'backend'}
})
};
fetch('https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'nightly',
'interval_minutes' => 1440,
'retention_count' => 7,
'description' => 'Nightly snapshots of the database volume',
'retention_days' => 30,
'enabled' => true,
'tags' => [
'environment' => 'production',
'team' => 'backend'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies"
payload := strings.NewReader("{\n \"name\": \"nightly\",\n \"interval_minutes\": 1440,\n \"retention_count\": 7,\n \"description\": \"Nightly snapshots of the database volume\",\n \"retention_days\": 30,\n \"enabled\": true,\n \"tags\": {\n \"environment\": \"production\",\n \"team\": \"backend\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"nightly\",\n \"interval_minutes\": 1440,\n \"retention_count\": 7,\n \"description\": \"Nightly snapshots of the database volume\",\n \"retention_days\": 30,\n \"enabled\": true,\n \"tags\": {\n \"environment\": \"production\",\n \"team\": \"backend\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"nightly\",\n \"interval_minutes\": 1440,\n \"retention_count\": 7,\n \"description\": \"Nightly snapshots of the database volume\",\n \"retention_days\": 30,\n \"enabled\": true,\n \"tags\": {\n \"environment\": \"production\",\n \"team\": \"backend\"\n }\n}"
response = http.request(request)
puts response.read_body{
"snapshot_policy": {
"faults": [
{
"code": "BACKUP_FAILED",
"severity": "error",
"message": "Backup upload failed.",
"details": {},
"first_at": "2023-11-07T05:31:56Z",
"last_at": "2023-11-07T05:31:56Z",
"occurrences": 1
}
],
"id": "3c9f1b2e-6d4a-4e8b-9f1c-2a3b4c5d6e7f",
"crn": "crn:storage:sa-saopaulo-1:my-account:volume/data-vol-01/snapshot-policy/nightly",
"name": "nightly",
"description": "Nightly snapshots of the database volume",
"enabled": true,
"interval_minutes": 1440,
"retention_count": 7,
"retention_days": 30,
"next_run_at": "2026-01-16T00:00:00Z",
"last_run_at": "2026-01-15T00:00:00Z",
"tags": {
"environment": "production",
"team": "backend"
},
"created_at": "2026-01-15T09:30:00Z",
"updated_at": "2026-01-15T09:30:00Z"
}
}{
"error": {
"code": "INVALID_INPUT",
"message": "Invalid request parameters",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "ACCESS_DENIED",
"message": "You don't have permission to perform this action",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "NOT_FOUND",
"message": "Resource not found",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "INVALID_INPUT",
"message": "Invalid request parameters",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"params": {
"instances": 2,
"pools": 0
}
}
}{
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used with a different request payload",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}Create snapshot policy
Attach a snapshot schedule to a volume. A volume supports up to 16 independent policies, each with its own interval and retention. Overlapping runs create separate snapshots and each counts against quota.
The first snapshot lands one interval_minutes from now —
attaching a schedule is not itself a request for a snapshot. Use
POST /v1/snapshots for one now.
What retention can delete, since a schedule is also an automatic deleter: only the snapshots this policy itself took. A snapshot taken by hand is never reaped, and neither is one something depends on — a snapshot a volume was created from, including a restore still running, is skipped and looked at again later. Pausing the policy stops the deleting as well as the taking, and deleting the policy keeps its existing snapshots. Work already claimed before a pause or deletion can finish; no new retention deletions are claimed after that change. Other policies continue independently.
curl --request POST \
--url https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "nightly",
"interval_minutes": 1440,
"retention_count": 7,
"description": "Nightly snapshots of the database volume",
"retention_days": 30,
"enabled": true,
"tags": {
"environment": "production",
"team": "backend"
}
}
'import requests
url = "https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies"
payload = {
"name": "nightly",
"interval_minutes": 1440,
"retention_count": 7,
"description": "Nightly snapshots of the database volume",
"retention_days": 30,
"enabled": True,
"tags": {
"environment": "production",
"team": "backend"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'nightly',
interval_minutes: 1440,
retention_count: 7,
description: 'Nightly snapshots of the database volume',
retention_days: 30,
enabled: true,
tags: {environment: 'production', team: 'backend'}
})
};
fetch('https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'nightly',
'interval_minutes' => 1440,
'retention_count' => 7,
'description' => 'Nightly snapshots of the database volume',
'retention_days' => 30,
'enabled' => true,
'tags' => [
'environment' => 'production',
'team' => 'backend'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies"
payload := strings.NewReader("{\n \"name\": \"nightly\",\n \"interval_minutes\": 1440,\n \"retention_count\": 7,\n \"description\": \"Nightly snapshots of the database volume\",\n \"retention_days\": 30,\n \"enabled\": true,\n \"tags\": {\n \"environment\": \"production\",\n \"team\": \"backend\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"nightly\",\n \"interval_minutes\": 1440,\n \"retention_count\": 7,\n \"description\": \"Nightly snapshots of the database volume\",\n \"retention_days\": 30,\n \"enabled\": true,\n \"tags\": {\n \"environment\": \"production\",\n \"team\": \"backend\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://storage.{region}.basaltic.sh/v1/volumes/{volume_id}/snapshot-policies")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"nightly\",\n \"interval_minutes\": 1440,\n \"retention_count\": 7,\n \"description\": \"Nightly snapshots of the database volume\",\n \"retention_days\": 30,\n \"enabled\": true,\n \"tags\": {\n \"environment\": \"production\",\n \"team\": \"backend\"\n }\n}"
response = http.request(request)
puts response.read_body{
"snapshot_policy": {
"faults": [
{
"code": "BACKUP_FAILED",
"severity": "error",
"message": "Backup upload failed.",
"details": {},
"first_at": "2023-11-07T05:31:56Z",
"last_at": "2023-11-07T05:31:56Z",
"occurrences": 1
}
],
"id": "3c9f1b2e-6d4a-4e8b-9f1c-2a3b4c5d6e7f",
"crn": "crn:storage:sa-saopaulo-1:my-account:volume/data-vol-01/snapshot-policy/nightly",
"name": "nightly",
"description": "Nightly snapshots of the database volume",
"enabled": true,
"interval_minutes": 1440,
"retention_count": 7,
"retention_days": 30,
"next_run_at": "2026-01-16T00:00:00Z",
"last_run_at": "2026-01-15T00:00:00Z",
"tags": {
"environment": "production",
"team": "backend"
},
"created_at": "2026-01-15T09:30:00Z",
"updated_at": "2026-01-15T09:30:00Z"
}
}{
"error": {
"code": "INVALID_INPUT",
"message": "Invalid request parameters",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "ACCESS_DENIED",
"message": "You don't have permission to perform this action",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "NOT_FOUND",
"message": "Resource not found",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "INVALID_INPUT",
"message": "Invalid request parameters",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"params": {
"instances": 2,
"pools": 0
}
}
}{
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used with a different request payload",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}storage:CreateSnapshotPolicy. See STORAGE permissions for the full list, what each one covers, and an example policy.Authorizations
An OAuth 2.0 bearer token, sent as Authorization: Bearer <token>.
This is the recommended way to authenticate.
Get one by exchanging a service account's access key pair at
POST /v1/oauth/token with grant_type=client_credentials. It is the
standard client-credentials grant, so any OAuth-aware library will
obtain and refresh it for you.
curl -s -u "$KEY_ID:$SECRET" -d grant_type=client_credentials \ https://iam.basaltic.sh/v1/oauth/token
Tokens last an hour by default. The same access key pair is separately your AWS SigV4 credential for the S3-compatible object endpoint, which speaks nothing else.
Headers
Optional client-generated key that makes a create replay-safe. Retrying a request with the same key returns the original outcome verbatim instead of creating a duplicate resource. Reusing a key with a different request body is rejected (422); a request whose key is still being processed returns 409. Records are honored for 24 hours. Use a UUID or similarly unique token.
255Path Parameters
Volume ID
Body
Unique within the volume — it names the policy in its CRN.
Scheduled snapshots are named <policy>-<UTC timestamp>. Resource names must not start with the literal crn: prefix or be UUIDs (canonical, compact, braced, or urn:uuid: forms, in either case).
1 - 128"nightly"
Minutes between snapshots — a minimum gap, not an exact cadence. A
periodic pass takes whatever has come due and re-bases each policy's
next run off the moment it ran, so a snapshot lands at or after
interval_minutes and never before, and can land a minute or two
later when the pass is busy. A window the pass misses costs one
snapshot rather than producing a catch-up burst afterwards.
The floor is one minute, because that pass is what evaluates the
schedule and nothing finer can be honoured; the ceiling is 30 days.
Sub-hourly intervals multiply snapshot churn and count against
the snapshots quota, so pick the largest interval that meets your
recovery point objective.
1 <= x <= 432001440
How many of this policy's snapshots to keep. When a fire takes the count past this, the oldest go first.
1 <= x <= 2567
"Nightly snapshots of the database volume"
Optional age bound, applied on top of retention_count: a
snapshot outside EITHER window is reaped. 0 means no age bound.
The single newest snapshot is exempt from the age bound, so a
volume that could not be snapshotted for longer than the window
never loses its whole history.
0 <= x <= 365030
Defaults to true. Set false to attach a paused schedule. Pausing stops the whole policy — no snapshots are taken and none are deleted, because a paused schedule that kept reaping would delete history while you were looking at it.
true
Show child attributes
Show child attributes
{ "environment": "production", "team": "backend" }
Response
Snapshot policy created
A schedule attached to one volume: take a snapshot every
interval_minutes, then keep at most retention_count of the
snapshots this policy created.
Retention only ever deletes snapshots the policy itself created
(those carrying its snapshot_policy reference) — a snapshot taken by
hand is never reaped. It also never deletes a snapshot something
depends on: a snapshot a volume was created from, including a
restore still in progress, is skipped and re-examined later.
Show child attributes
Show child attributes

