# Backup Storage

https://dev.wisecp.com/es/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

get/api/v1/admin/settings/backup/storage/providers

`Settings/GetBackupStorageProviders` admin

Returns the storage providers installed for backups to be sent to.

Response fields data[] — 5

keystringThe provider key. This is what creating a target takes.

namestringThe provider name.

descriptionstringWhat it does.

oauthboolWhether it needs the user to grant access. When true, writing settings is not enough on its own.

fieldsarrayThe definition of the settings the provider wants.

Errors 1

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage/providers' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage/providers', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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

post/api/v1/admin/settings/backup/storage/validate

`Settings/ValidateBackupStorage` admin

Runs a configuration through a connection test without saving it.

Body 3

typestringreqThe provider to test. Required unless you give a target id.

idintThe id of an existing target. Its settings get used.

configobjectThe provider settings. Secret values stay protected behind the mask.

Response fields data — 2

validatedboolWhether the connection worked.

messagestringWhat the result means.

Errors 3

unknown_provider422The provider is not recognised.

module_not_loadable422The provider could not be loaded.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
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"}}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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

post/api/v1/admin/settings/backup/storage/oauth-url

`Settings/GetBackupOauthUrl` admin

Builds the address where the user grants a cloud provider access.

Body 3

typestringreqThe provider asking for consent.

idintThe id of an existing target.

configobjectThe provider settings.

Response fields data — 2

authorize_urlstringThe address the user gets sent to.

statestringThe signed state value. It has to match when the user comes back.

Errors 4

unknown_provider422The provider is not recognised.

oauth_unsupported422The provider does not use a consent flow.

oauth_url_failed422The address could not be built.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
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"}'
```

```javascript
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;
```

```php
$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);
```

```php
// 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

post/api/v1/admin/settings/backup/storage/bunny-zones

`Settings/GetBunnyZones` admin

Returns the Bunny storage zones the given account key can reach.

Body 2

account_api_keystringreqThe Bunny account key. Sending the mask means a target id is needed too.

idintThe id of an existing target.

Response fields data — 1

zonesarrayThe storage zones on the account.

Errors 3

api_key_required422No account key was given.

fetch_zones_failed422The zone list could not be fetched.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
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"'"}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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

get/api/v1/admin/settings/backup/storage

`Settings/GetBackupStorages` admin

Returns the backup storage targets defined.

Query 1

searchstringSearches by name.

Response fields data[] — 6

idintThe target id.

namestringThe target name.

typestringThe provider type.

statusstringThe target status.

validated_atstring | nullWhen the connection was last tested. Empty means it was never tested.

created_atstring | nullWhen it was created.

Errors 1

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/backup/storage', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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

post/api/v1/admin/settings/backup/storage

`Settings/CreateBackupStorage` admin connects first

Sets up a new storage target, trying to connect before it saves.

Body 3

namestringreqThe target name. Up to 150 characters.

typestringreqThe provider key.

configobjectThe provider settings. A cloud provider also needs the consent token.

Response fields 201 — data

dataobjectThe target created. Same shape as the detail endpoint, with secrets masked.

Errors 5

name_required422The name is empty.

invalid_provider422The provider is not valid.

oauth_required422The cloud provider has no consent yet.

storage_test_failed422The connection could not be made.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
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"}}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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

get/api/v1/admin/settings/backup/storage/{id}

`Settings/GetBackupStorage` admin

Returns one target together with its settings.

Response fields data — 7

idintThe target id.

namestringThe target name.

typestringThe provider type.

statusstringThe target status.

configobjectThe provider settings. Secret values arrive masked.

validated_atstring | nullWhen the connection was last tested.

created_atstring | nullWhen it was created.

Errors 2

not_found404No such target.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/backup/storage/12' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch(`https://panel.example.com/api/v1/admin/settings/backup/storage/${id}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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

patch/api/v1/admin/settings/backup/storage/{id}

`Settings/UpdateBackupStorage` admin

Changes the target name and its settings.

Body 2

namestringThe new name.

configobjectThe provider's settings, written as a whole. Send the full set: a field you leave out is dropped, and a checkbox or number falls back to zero. `*****` keeps an encrypted field's stored value.

Response fields data

dataobjectThe target as it now stands. Same shape as the detail endpoint, with secrets masked.

Errors 5

not_found404No such target.

name_required422The name is empty.

oauth_required422The cloud provider has no consent yet.

storage_test_failed422The connection could not be made.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
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":"*****"}}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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

delete/api/v1/admin/settings/backup/storage/{id}

`Settings/DeleteBackupStorage` admin consent is withdrawn

Removes a storage target.

Response fields data — 2

deletedboolWhether the delete ran.

idintThe id of the target removed.

Errors 3

not_found404No such target.

storage_in_use422A schedule still points at this target.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/settings/backup/storage/12' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// On a cloud target the delete also WITHDRAWS consent: re-adding it needs a fresh grant.
Api::Settings()->DeleteBackupStorage(['id' => $id]);
```

## Pitfalls

> **The mask protects on read and does nothing on create**
> 
> 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.

> **Testing is not saving**
> 
> 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.

> **A cloud provider is not set up with settings alone**
> 
> 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.

> **A target a schedule uses cannot be deleted**
> 
> 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 withdraws the consent too**
> 
> 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

- [Backup Schedules](https://dev.wisecp.com/en/backup-schedules)
- [Backup Diagnostics](https://dev.wisecp.com/en/backup-diagnostics)
- [Adding a Scheduled Job](https://dev.wisecp.com/en/adding-a-scheduled-task)
