# Account API

> Read a Reggio Digital account from your own software: websites, backups and visits, support requests, domains and DNS, invoices and plans.

Source: https://reggiodigital.com/developers/account

The account API lets your own software read what a business has with us: its websites, their backups and visits, its support requests, its domains and DNS records, and its invoices and plans. Build a status board, pull invoices into your accounting, or let your own monitoring see when a support request is answered.

Everything on this page only reads. Nothing here can change a website, spend money or move a domain.

## Getting a key

The business makes the key under [API keys](https://reggiodigital.com/settings/api-keys) in their dashboard and picks what it can read. Anyone on the account can make one, but a key can never read more than the person who made it can see: support requests need access to Support, and invoices need access to Billing. The key belongs to the account, so it keeps working when that person leaves.

| Ability | Lets the key read |
| --- | --- |
| `websites:read` | Websites, their backups and their monthly visits. |
| `tickets:read` | Every support request on the account and the replies on each. |
| `domains:read` | Domains, their renewal dates, and the DNS records on each zone. |
| `billing:read` | Invoices, and the plans and add-ons being paid for. |

A key without the ability a call needs gets a `403` with `forbidden`. Call [`GET /me`](https://reggiodigital.com/developers/authentication#checking-a-key) to see what a key has.

An account that has not bought anything yet still gets answers. Its lists are simply empty.

## Lists and pages

Every list comes back in the same shape, newest first:

```json
{
  "data": [],
  "has_more": true,
  "next_cursor": "eyJpZCI6MTE4LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9"
}
```

- `limit` sets how many come back, from 1 to 100. The default is 25.
- When `has_more` is `true`, ask again with `cursor` set to the `next_cursor` you were given, exactly as it came. When it is `false`, you have everything and `next_cursor` is `null`.
- A cursor rather than a page number means a record added while you are paging never shows up twice.

A `limit` out of range, or a cursor we did not give you, gets a `422` with `invalid_request`.

An id that is not on this account, or belongs to another account, gets a `404` with `not_found`. We never say whether it exists somewhere else.

## Websites

Needs `websites:read`.

### Listing websites

`GET /api/v1/websites`

cURL:

```bash
curl https://reggiodigital.com/api/v1/websites?limit=25 \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/websites?limit=25');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/websites?limit=25');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/websites?limit=25", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    {
      "id": 118,
      "name": "Glass Jug Beer Lab",
      "url": "https://glassjugbeer.com",
      "status": "active",
      "is_staging": false,
      "staging_of": null,
      "hosting": "reggio",
      "wordpress_version": "6.8.2",
      "php_version": "8.3",
      "uptime": {
        "status": "up",
        "checked_at": "2026-09-25T13:55:00+00:00",
        "changed_at": "2026-09-02T04:10:00+00:00"
      },
      "created_at": "2025-03-14T16:20:00+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

- `status` is `active`, `suspended` or `maintenance`.
- `hosting` is `reggio` when we host it, `pressable` for a site still on our previous host, and `external` for a site hosted somewhere else that we look after.
- A staging copy has `is_staging` set and `staging_of` holding the id of the live site it was copied from.
- `uptime.status` is `up`, `down` or `unknown` until we have checked it.

### One website

`GET /api/v1/websites/{id}`

cURL:

```bash
curl https://reggiodigital.com/api/v1/websites/118 \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/websites/118');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/websites/118');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/websites/118", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

Answers with one website in the shape above, on its own rather than inside `data`.

### Backups

`GET /api/v1/websites/{id}/backups`

cURL:

```bash
curl https://reggiodigital.com/api/v1/websites/118/backups \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/websites/118/backups');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/websites/118/backups');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/websites/118/backups", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    {
      "id": "a1b2c3d4",
      "type": "full",
      "status": "completed",
      "created_at": "2026-09-25T05:30:00+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

The restore points the dashboard lists, up to the latest 50, newest first. Downloading or restoring a backup stays in the dashboard. A site hosted somewhere else has none.

### Visits

`GET /api/v1/websites/{id}/traffic`

cURL:

```bash
curl https://reggiodigital.com/api/v1/websites/118/traffic \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/websites/118/traffic');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/websites/118/traffic');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/websites/118/traffic", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    { "month": "2026-08", "visits": 18240, "storage_mb": 2140 },
    { "month": "2026-09", "visits": 15321, "storage_mb": 2188 }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Twelve months, oldest first, ending with the current billing month. These are the figures on the website's page in the dashboard. `storage_mb` is the latest reading in that month, or `null` when we have none.

## Support requests

Needs `tickets:read`.

### Listing support requests

`GET /api/v1/tickets`

cURL:

```bash
curl https://reggiodigital.com/api/v1/tickets?status=open \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/tickets?status=open');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/tickets?status=open');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/tickets?status=open", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    {
      "id": 2051,
      "title": "Contact form stopped sending",
      "description": "Nothing has arrived from the contact page since Tuesday.",
      "status": "open",
      "priority": "high",
      "category": "support",
      "website_id": 118,
      "opened_by": "Sam Rivera",
      "created_at": "2026-09-24T14:02:00+00:00",
      "updated_at": "2026-09-25T09:12:00+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

- Add `status` to see only `open`, `pending` or `closed` requests.
- `priority` is `low`, `medium`, `high` or `urgent`.
- `website_id` is `null` when the request is about the account rather than one site.

`GET /api/v1/tickets/{id}` answers with one request in the same shape.

### Replies

`GET /api/v1/tickets/{id}/comments`

cURL:

```bash
curl https://reggiodigital.com/api/v1/tickets/2051/comments \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/tickets/2051/comments');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/tickets/2051/comments');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/tickets/2051/comments", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    {
      "id": 88412,
      "author": "Jordan at Reggio Digital",
      "body": "Found it. The form was sending from an address that is no longer set up. It is fixed and the missed messages have been resent.",
      "created_at": "2026-09-25T09:12:00+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Newest first. Attachments stay in the dashboard for now.

## Domains and DNS

Needs `domains:read`.

### Domains

`GET /api/v1/domains`

cURL:

```bash
curl https://reggiodigital.com/api/v1/domains \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/domains');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/domains');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/domains", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    {
      "id": 64,
      "domain": "glassjugbeer.com",
      "status": "active",
      "registered_at": "2019-05-02T00:00:00+00:00",
      "expires_at": "2027-05-02T00:00:00+00:00",
      "auto_renew": true,
      "locked": true,
      "nameservers": ["ada.ns.cloudflare.com", "rob.ns.cloudflare.com"],
      "website_id": 118
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

The domains registered or transferred to us. `GET /api/v1/domains/{id}` answers with one. A transfer code is never available here; request one from the domain's page in the dashboard.

### DNS zones and records

`GET /api/v1/dns-zones` lists the zones we run DNS for, and `GET /api/v1/dns-zones/{id}` answers with one:

```json
{
  "id": 37,
  "name": "glassjugbeer.com",
  "status": "active",
  "paused": false,
  "name_servers": ["ada.ns.cloudflare.com", "rob.ns.cloudflare.com"],
  "records": 14,
  "activated_at": "2024-01-18T10:00:00+00:00"
}
```

`GET /api/v1/dns-zones/{id}/records` lists the records on a zone:

cURL:

```bash
curl https://reggiodigital.com/api/v1/dns-zones/37/records \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/dns-zones/37/records');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/dns-zones/37/records');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/dns-zones/37/records", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    {
      "id": 5120,
      "type": "A",
      "name": "glassjugbeer.com",
      "content": "203.0.113.10",
      "ttl": 1,
      "priority": null,
      "proxied": true
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

A `ttl` of `1` means automatic. `priority` is set on `MX` and `SRV` records only.

## Billing

Needs `billing:read`.

### Invoices

`GET /api/v1/invoices`

cURL:

```bash
curl https://reggiodigital.com/api/v1/invoices \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/invoices');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/invoices');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/invoices", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    {
      "id": 9031,
      "number": "RD-2026-0412",
      "status": "paid",
      "currency": "USD",
      "subtotal": "95.00",
      "tax": "0.00",
      "discount": "0.00",
      "total": "95.00",
      "amount_paid": "95.00",
      "amount_due": "0.00",
      "description": "Monthly plan",
      "period_start": "2026-09-01T00:00:00+00:00",
      "period_end": "2026-10-01T00:00:00+00:00",
      "due_date": "2026-09-01T00:00:00+00:00",
      "paid_at": "2026-09-01T06:02:00+00:00",
      "created_at": "2026-09-01T06:00:00+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

- Amounts are strings with two decimal places, in the invoice's `currency`, so nothing is lost to rounding.
- `status` is `draft`, `open`, `paid`, `void` or `uncollectible`.
- `GET /api/v1/invoices/{id}` answers with one. The PDF stays in the dashboard for now.

### Plans and add-ons

`GET /api/v1/subscriptions`

cURL:

```bash
curl https://reggiodigital.com/api/v1/subscriptions \
  -H "Authorization: Bearer $REGGIO_API_KEY"
```

Laravel:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.reggio.key'))
    ->acceptJson()
    ->get('https://reggiodigital.com/api/v1/subscriptions');

if ($response->failed()) {
    throw new RuntimeException($response->json('error.message'));
}

$body = $response->json();
```

PHP:

```php
$ch = curl_init('https://reggiodigital.com/api/v1/subscriptions');

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
    ],
]);

$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($body['error']['message']);
}
```

Node.js:

```js
const response = await fetch("https://reggiodigital.com/api/v1/subscriptions", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
  },
});

const body = await response.json();

if (!response.ok) {
  throw new Error(body.error.message);
}
```

```json
{
  "data": [
    {
      "id": 412,
      "label": "Business Hosting (Plan #412)",
      "status": "active",
      "is_plan": true,
      "items": [
        { "name": "Business Hosting", "quantity": 1, "price": "95.00", "interval": "month" },
        { "name": "Transactional Email", "quantity": 1, "price": "15.00", "interval": "month" }
      ],
      "sites": 3,
      "site_limit": 5,
      "trial_ends_at": null,
      "ends_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Everything the account is paying for, the same as the billing page. An account can have more than one plan, so read every entry rather than the first. `is_plan` is `false` for a subscription that only holds add-ons such as email, and `sites` and `site_limit` only mean something when it is `true`. `ends_at` is set when a plan has been cancelled and is running to the end of what was paid for.
