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/api/v1/sites/{site_id}/hourly-backupsScope 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
curl -s "https://app.pbn.ltd/api/v1/sites/123/hourly-backups" \
-H "Authorization: Bearer $PBN_API_KEY"
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())
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
{
"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/api/v1/sites/{site_id}/hourly-backups/nowScope 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
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/hourly-backups/now" \
-H "Authorization: Bearer $PBN_API_KEY"
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())
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
{
"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/api/v1/sites/{site_id}/hourly-backups/{backup_id}/restoreScope 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
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/hourly-backups/88001/restore" \
-H "Authorization: Bearer $PBN_API_KEY"
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())
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
{
"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/api/v1/sites/{site_id}/hourly-backups/{backup_id}/downloadScope 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
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/hourly-backups/88001/download" \
-H "Authorization: Bearer $PBN_API_KEY"
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())
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
{
"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/api/v1/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
curl -s "https://app.pbn.ltd/api/v1/sites/123/hourly-backups/downloads/download_id" \
-H "Authorization: Bearer $PBN_API_KEY" \
-o invoice.pdf
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)
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