Backup Storage
The nine endpoints that set up, test and remove the storage targets backups go to.
Overview
A storage target defines where backups get sent. These nine endpoints set targets up, test them and take them away. When backups run is a separate matter.
Providers fall into two groups. Server-type ones are set up with an address and a password. Cloud-type ones wait for the user to grant access in a browser. That path has three steps: get the consent address, send the user, then create the target with the returned token.
A target is actually connected to before it is saved. A wrong password surfaces the moment you write the target, not on the night of the backup.
Reference
Listing the Providers
Returns the storage providers installed for backups to be sent to.
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage/providers' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/providers', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/providers');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A server-type provider is set up with settings; a cloud one needs the USER TO GRANT ACCESS.
$providers = Api::Settings()->GetBackupStorageProviders()['data'];
$simple = array_filter($providers, fn ($p) => ! $p['oauth']);Testing a Connection
Runs a configuration through a connection test without saving it.
curl -X POST 'https://panel.example.com/api/v1/admin/settings/backup/storage/validate' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"FTP","config":{"host":"ftp.example.com","username":"backup","password":"secret"}}'const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/validate', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'FTP',
config: { host: 'ftp.example.com', username: 'backup', password: secret },
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/validate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'type' => 'FTP',
'config' => ['host' => 'ftp.example.com', 'username' => 'backup', 'password' => $secret],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The test SAVES NOTHING: passing it does not mean a target now exists.
$check = Api::Settings()->ValidateBackupStorage([
'type' => 'FTP',
'config' => $config,
])['data'];
if ($check['validated']) Api::Settings()->CreateBackupStorage($payload);Getting the Consent Address
Builds the address where the user grants a cloud provider access.
curl -X POST 'https://panel.example.com/api/v1/admin/settings/backup/storage/oauth-url' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"GoogleDrive"}'const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/oauth-url', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ type: 'GoogleDrive' }),
});
const { data } = await res.json();
window.location.href = data.authorize_url;$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/oauth-url');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['type' => 'GoogleDrive']),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The address alone is not enough: no target can be CREATED before the user comes back.
$url = Api::Settings()->GetBackupOauthUrl(['type' => 'GoogleDrive'])['data']['authorize_url'];Reading the Bunny Zones
Returns the Bunny storage zones the given account key can reach.
curl -X POST 'https://panel.example.com/api/v1/admin/settings/backup/storage/bunny-zones' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"account_api_key":"'"$BUNNY_KEY"'"}'const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/bunny-zones', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ account_api_key: bunnyKey }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/bunny-zones');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['account_api_key' => $bunnyKey]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A stored target returns its key masked; when sending the mask back, give the ID as well.
$zones = Api::Settings()->GetBunnyZones([
'account_api_key' => '*****',
'id' => $storageId,
])['data']['zones'];Listing the Targets
Returns the backup storage targets defined.
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The last test can be OLD: the password may have changed since that day.
$targets = Api::Settings()->GetBackupStorages()['data'];
$stale = array_filter($targets, fn ($t) => $t['validated_at'] === null);Creating a Target
Sets up a new storage target, trying to connect before it saves.
curl -X POST 'https://panel.example.com/api/v1/admin/settings/backup/storage' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"Offsite FTP","type":"FTP","config":{"host":"ftp.example.com","username":"backup","password":"secret","folder_path":"/backups"}}'const res = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Offsite FTP',
type: 'FTP',
config: {
host: 'ftp.example.com',
username: 'backup',
password: secret,
folder_path: '/backups',
},
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Offsite FTP',
'type' => 'FTP',
'config' => [
'host' => 'ftp.example.com',
'username' => 'backup',
'password' => $secret,
'folder_path' => '/backups',
],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The mask is USELESS here: there is no old value to keep, so send the real password.
Api::Settings()->CreateBackupStorage([
'name' => 'Offsite FTP',
'type' => 'FTP',
'config' => ['host' => $host, 'username' => $user, 'password' => $secret],
]);Reading One Target
Returns one target together with its settings.
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage/12' \
-H "Authorization: Bearer $API_KEY"const res = await fetch(`https://panel.example.com/api/v1/admin/settings/backup/storage/${id}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Do not feed what you read straight into a NEW target: the password is masked.
$target = Api::Settings()->GetBackupStorage(['id' => $id])['data'];Updating a Target
Changes the target name and its settings.
***** keeps an encrypted field's stored value.curl -X PATCH 'https://panel.example.com/api/v1/admin/settings/backup/storage/12' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"Offsite FTP (EU)","config":{"folder_path":"/backups-eu","password":"*****"}}'const res = await fetch(`https://panel.example.com/api/v1/admin/settings/backup/storage/${id}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Offsite FTP (EU)',
config: { folder_path: '/backups-eu', password: '*****' },
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Offsite FTP (EU)',
'config' => ['folder_path' => '/backups-eu', 'password' => '*****'],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Change the path and the connection is tested AGAIN; a missing folder fails the update.
Api::Settings()->UpdateBackupStorage([
'id' => $id,
'config' => ['folder_path' => '/backups-eu', 'password' => '*****'],
]);Deleting a Target
Removes a storage target.
curl -X DELETE 'https://panel.example.com/api/v1/admin/settings/backup/storage/12' \
-H "Authorization: Bearer $API_KEY"const res = await fetch(`https://panel.example.com/api/v1/admin/settings/backup/storage/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/settings/backup/storage/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// On a cloud target the delete also WITHDRAWS consent: re-adding it needs a fresh grant.
Api::Settings()->DeleteBackupStorage(['id' => $id]);Pitfalls
The detail and list endpoints return secrets masked. Sending the mask back on an update keeps the old value. On create there is no old value to keep. Feed what you read straight into a new target and the mask itself gets stored as the password. The connection test then fails.
The test endpoint saves nothing and only tries the connection. Passing it does not mean a target exists. The panel shows the two steps back to back, which is where this gets confused. The test succeeds, the create call is forgotten, and no target exists to give a schedule.
For a provider that asks for consent, writing settings is not enough: the user has to grant access in a browser. Without the token the create call fails with consent missing. The consent field in the provider list tells you in advance which providers want that three-step path.
While a schedule points at a target, the delete call is refused. Move the schedule to another target first, or remove it. The guard is deliberate: silently deleting the target would turn the schedule into a job that runs into nothing every night.
Deleting a cloud target also revokes the access grant at the provider. Adding the same account back asks for a fresh round of consent, and the old token is of no use. The revoke is attempted as best it can be. If the provider cannot be reached, the target still goes and the grant may live on at their end.
Related Articles
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.