# Hourly backups

The hourly backups add-on of a site: its restore points, a backup now, rolling the site back to an hour, and downloading one.

## Hourly backups of a site

`GET /sites/{site_id}/hourly-backups` · scope `backups:read`

The same as the hourly part of the site's Backups tab: whether hourly backups are on for this site (and paid until when), when the next one runs, every restore point kept, any roll-back in progress and the downloads being prepared. Switching the add-on on is a purchase made in the Backups tab (`manage_url`); it is never bought over the API.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/hourly-backups" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/hourly-backups", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/hourly-backups", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "active": true,
    "available": true,
    "price_per_month": "4.00",
    "retention": "every hour for 48 hours, then daily",
    "paid_until": "2026-10-25",
    "next_run": "2026-09-26T18:00:00Z",
    "last_ok": "2026-09-26T17:00:12Z",
    "last_error": "",
    "restoring": false,
    "restore_state": "",
    "count": 1,
    "backups": [
      {
        "id": 9120,
        "taken_at": "2026-09-26T17:00:12Z",
        "kind": "hourly",
        "bytes": 48211093,
        "database": true
      }
    ],
    "downloads": [
      {
        "id": 311,
        "backup_id": 9120,
        "state": "ready",
        "bytes": 48211093,
        "error": ""
      }
    ],
    "manage_url": "https://app.pbn.ltd/site/123#backups"
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## Take an hourly backup now

`POST /sites/{site_id}/hourly-backups/now` · scope `backups:write`

Takes a restore point now, as the "Back up now" button (once per site every 10 minutes). It appears in GET /sites/{site_id}/hourly-backups within a minute or two.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/hourly-backups/now" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/hourly-backups/now", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/hourly-backups/now", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`202`

```json
{
  "data": {
    "queued": true,
    "message": "Taking a backup now."
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## Roll the site back to a restore point

`POST /sites/{site_id}/hourly-backups/{backup_id}/restore` · scope `backups:write` · **destructive**

Rolls the site back to exactly how it was at that restore point, as the tab's "Restore this backup" dialog. A backup of how the site looks now is taken first. One roll-back per site at a time; you get an e-mail when it is done.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `backup_id` | path | integer | yes | The restore point id (see GET /sites/{site_id}/hourly-backups). |
| `what` | body | string (one of: both, files, db) | no | Files and database (default), files only, or database only. Default: `both`. |
| `confirm` | body | boolean | yes | Must be true: the site is rolled back to that hour. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/hourly-backups/88001/restore" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/hourly-backups/88001/restore", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/hourly-backups/88001/restore", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`202`

```json
{
  "data": {
    "queued": true,
    "restore_id": 77,
    "message": "Rolling the site back now."
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable`, `validation_failed`

## Prepare a download of a restore point

`POST /sites/{site_id}/hourly-backups/{backup_id}/download` · scope `backups:read`

Builds a .tar.gz of that restore point (files and database), as the tab's Download button. Poll GET /sites/{site_id}/hourly-backups until the download reads "ready", then fetch it with GET /sites/{site_id}/hourly-backups/downloads/{download_id}. Kept for 24 hours.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `backup_id` | path | integer | yes | The restore point id (see GET /sites/{site_id}/hourly-backups). |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/hourly-backups/88001/download" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/hourly-backups/88001/download", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/hourly-backups/88001/download", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`202`

```json
{
  "data": {
    "download_id": 311,
    "state": "pending",
    "message": "Preparing the download."
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## Download a prepared restore point

`GET /sites/{site_id}/hourly-backups/downloads/{download_id}` · scope `backups:read`

The .tar.gz archive, once the download reads "ready". Not offered to AI assistants (it is a file, not an answer).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `download_id` | path | integer | yes | The download id from the prepare call. |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/hourly-backups/downloads/download_id" \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -o invoice.pdf
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/hourly-backups/downloads/download_id", headers=headers, timeout=120)
r.raise_for_status()
open("invoice.pdf", "wb").write(r.content)
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/hourly-backups/downloads/download_id", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
const pdf = Buffer.from(await res.arrayBuffer());  // Node 18+
```

### Response

`200` with the file (`application/gzip`).

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`
