# Registrar connections

Connect your domain registrar accounts so we set your sites' nameservers for you: connections, automatic updates, per-site status and history.

## Registrars you can connect

`GET /registrars/providers` · scope `registrars:read`

Every registrar we can set nameservers at, with the steps to create an API key there, the IP addresses to allow (when the registrar needs an allow-list) and the fields to send in `secret_credentials` when you connect it.

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/registrars/providers" \
  -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/registrars/providers", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`200`

```json
{
  "data": [
    {
      "code": "namecheap",
      "name": "Namecheap",
      "ip_allowlist": "required",
      "allow_ips": [
        "116.202.166.25",
        "65.109.76.2"
      ],
      "about": "Uses the Namecheap API. API access must be switched on and our IP addresses whitelisted.",
      "needs": "...",
      "steps": [
        "In Namecheap open Profile > Tools > ..."
      ],
      "fields": [
        {
          "name": "username",
          "label": "Namecheap username",
          "masked": false,
          "required": true
        },
        {
          "name": "api_key",
          "label": "API key",
          "masked": true,
          "required": true
        }
      ]
    }
  ]
}
```

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

## List registrar connections

`GET /registrars/connections` · scope `registrars:read`

Your connected registrar accounts: status, how many domains we can see in each, and whether automatic nameserver updates are on. The keys are never returned - only a masked hint such as "key …a1b2".

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/registrars/connections" \
  -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/registrars/connections", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`200`

```json
{
  "data": [
    {
      "id": 41,
      "registrar": "namecheap",
      "registrar_name": "Namecheap",
      "label": "Main account",
      "hint": "myuser \u00b7 key \u2026a1b2",
      "status": "ok",
      "status_message": "",
      "last_checked": "2026-09-24T09:00:00Z",
      "domains": 57,
      "domains_refreshed_at": "2026-09-24T09:01:00Z",
      "automatic_updates": true,
      "created_at": "2026-09-15T10:00:00Z"
    }
  ]
}
```

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

## One registrar connection

`GET /registrars/connections/{connection_id}` · scope `registrars:read`

One connection, with the domain names we can see in that registrar account (up to 1,000).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `connection_id` | path | integer | yes | The connection id. |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/registrars/connections/connection_id" \
  -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/registrars/connections/connection_id", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`200`

```json
{
  "data": {
    "id": 41,
    "registrar": "namecheap",
    "registrar_name": "Namecheap",
    "label": "Main account",
    "hint": "myuser \u00b7 key \u2026a1b2",
    "status": "ok",
    "status_message": "",
    "last_checked": "2026-09-24T09:00:00Z",
    "domains": 57,
    "domains_refreshed_at": "2026-09-24T09:01:00Z",
    "automatic_updates": true,
    "created_at": "2026-09-15T10:00:00Z",
    "domain_names": [
      "example.com"
    ]
  }
}
```

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

## Connect a registrar account

`POST /registrars/connections` · scope `registrars:write`

Checks the key with the registrar FIRST and only saves a working account (same as the Registrar connections page). We then read the domain list; sites waiting for nameservers on those domains are updated automatically within a minute or two. Some registrars need our IP addresses on an allow-list first (see GET /registrars/providers).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `registrar` | body | string | yes | The registrar code from GET /registrars/providers (e.g. namecheap, godaddy, porkbun, dynadot, namesilo, spaceship, namebright, zinn). |
| `secret_credentials` | body | object | yes | The fields that registrar needs, by name (see `fields` in GET /registrars/providers), e.g. {"username": "...", "api_key": "..."}. Checked with the registrar before anything is saved; stored encrypted; never returned and never written to the API call log. |
| `label` | body | string | no | Your own name for this account (optional, 80 characters). |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/registrars/connections" \
  -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/registrars/connections", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`201`

```json
{
  "data": {
    "id": 41,
    "registrar": "namecheap",
    "registrar_name": "Namecheap",
    "label": "Main account",
    "hint": "myuser \u00b7 key \u2026a1b2",
    "status": "ok",
    "status_message": "",
    "last_checked": "2026-09-24T09:00:00Z",
    "domains": 57,
    "domains_refreshed_at": "2026-09-24T09:01:00Z",
    "automatic_updates": true,
    "created_at": "2026-09-15T10:00:00Z"
  }
}
```

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

## Test a registrar connection

`POST /registrars/connections/{connection_id}/test` · scope `registrars:write`

Asks the registrar whether the stored key still works and records the answer on the connection.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `connection_id` | path | integer | yes | The connection id. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/registrars/connections/connection_id/test" \
  -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/registrars/connections/connection_id/test", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/registrars/connections/connection_id/test", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "id": 41,
    "registrar": "namecheap",
    "registrar_name": "Namecheap",
    "label": "Main account",
    "hint": "myuser \u00b7 key \u2026a1b2",
    "status": "ok",
    "status_message": "",
    "last_checked": "2026-09-24T09:00:00Z",
    "domains": 57,
    "domains_refreshed_at": "2026-09-24T09:01:00Z",
    "automatic_updates": true,
    "created_at": "2026-09-15T10:00:00Z",
    "works": true
  }
}
```

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

## Re-read the domain list

`POST /registrars/connections/{connection_id}/refresh` · scope `registrars:write`

Reads the domain list of this registrar account again (about a minute). Once every 2 minutes.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `connection_id` | path | integer | yes | The connection id. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/registrars/connections/connection_id/refresh" \
  -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/registrars/connections/connection_id/refresh", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/registrars/connections/connection_id/refresh", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`202`

```json
{
  "data": {
    "refreshing": true
  }
}
```

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

## Automatic nameserver updates on or off

`POST /registrars/connections/{connection_id}/automatic` · scope `registrars:write`

Switches automatic nameserver updates for this connection on or off (the page's toggle, but with an explicit value, so repeating the call is safe).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `connection_id` | path | integer | yes | The connection id. |
| `enabled` | body | boolean | yes | true = we set nameservers for waiting sites on this account's domains. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/registrars/connections/connection_id/automatic" \
  -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/registrars/connections/connection_id/automatic", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/registrars/connections/connection_id/automatic", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "id": 41,
    "registrar": "namecheap",
    "registrar_name": "Namecheap",
    "label": "Main account",
    "hint": "myuser \u00b7 key \u2026a1b2",
    "status": "ok",
    "status_message": "",
    "last_checked": "2026-09-24T09:00:00Z",
    "domains": 57,
    "domains_refreshed_at": "2026-09-24T09:01:00Z",
    "automatic_updates": true,
    "created_at": "2026-09-15T10:00:00Z"
  }
}
```

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

## Disconnect a registrar account

`DELETE /registrars/connections/{connection_id}` · scope `registrars:write` · **destructive**

Deletes the connection and its stored keys (the nameserver update history is kept). Nothing changes at the registrar. Needs "confirm": true.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `connection_id` | path | integer | yes | The connection id. |
| `confirm` | body | boolean | yes | Must be true: the stored keys are deleted and automatic nameserver updates through this account stop. |

### Example

```bash
curl -s -X DELETE "https://app.pbn.ltd/api/v1/registrars/connections/connection_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/registrars/connections/connection_id", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/registrars/connections/connection_id", {
  method: "DELETE",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "deleted": true,
    "id": 41
  }
}
```

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

## Nameserver update history

`GET /registrars/history` · scope `registrars:read`

Every nameserver change we made (or tried) at your registrars: what the registrar had before (the value to go back to), what we set, the result, and whether the domain registry already shows it.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | integer | no | Only this site. |
| `domain` | query | string | no | Only this domain. |
| `limit` | query | integer | no | How many, newest first. Default: `30`. |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/registrars/history" \
  -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/registrars/history", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`200`

```json
{
  "data": [
    {
      "id": 9001,
      "site_id": 12345,
      "domain": "example.com",
      "registrar": "namecheap",
      "registrar_name": "Namecheap",
      "connection_id": 41,
      "trigger": "autopilot",
      "result": "ok",
      "message": "",
      "old_nameservers": [
        "dns1.registrar-servers.com",
        "dns2.registrar-servers.com"
      ],
      "new_nameservers": [
        "ada.ns.cloudflare.com",
        "bob.ns.cloudflare.com"
      ],
      "registry": "confirmed",
      "at": "2026-09-24T09:05:00Z"
    }
  ]
}
```

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

## Set nameservers for every waiting site now

`POST /registrars/update-waiting` · scope `registrars:write`

Sets the nameservers now for every site waiting for DNS whose domain is in a connected account (the page's "update all" button). Paced to stay inside the registrars' limits, so large accounts take a few minutes; results appear in GET /registrars/history. Once every 15 minutes.

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/registrars/update-waiting" \
  -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/registrars/update-waiting", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`202`

```json
{
  "data": {
    "started": true
  }
}
```

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

## Will this domain get its nameservers set for me?

`GET /registrars/domain-check` · scope `registrars:read`

The question the add-a-site form asks while you type: is this domain in one of your connected accounts (or registered with Zinn Digital), so we point it at the site ourselves?

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `domain` | query | string | yes | The domain (no site needed yet). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/registrars/domain-check" \
  -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/registrars/domain-check", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`200`

```json
{
  "data": {
    "automatic": true,
    "headline": "We will set this domain's nameservers for you.",
    "detail": "example.com is in your connected Namecheap account. ..."
  }
}
```

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

## Automatic nameservers for a site

`GET /sites/{site_id}/registrar` · scope `registrars:read`

What the site page says about automatic nameserver updates for this site: whether we set its nameservers for you, through which registrar, what happened last, and the nameservers it needs.

### 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/registrar" \
  -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/registrar", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`200`

```json
{
  "data": {
    "automatic": true,
    "state": "pending",
    "registrar_name": "Namecheap",
    "headline": "We are setting the nameservers for you - nothing for you to do.",
    "detail": "This domain is in your connected Namecheap account, so we set its nameservers automatically, usually within 10 minutes.",
    "nameservers": [
      "ada.ns.cloudflare.com",
      "bob.ns.cloudflare.com"
    ],
    "waiting_for_dns": true
  }
}
```

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

## Set this site's nameservers at the registrar now

`POST /sites/{site_id}/registrar/update-nameservers` · scope `registrars:write`

The site page's "Update nameservers at my registrar" button: sets the nameservers this site needs at the registrar that holds its domain, now, and returns the history entry. "already" means the registrar already had them - the site goes live as soon as DNS catches up.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `connection_id` | body | integer | no | Use this connection (optional; by default the one holding the domain). |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/registrar/update-nameservers" \
  -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/registrar/update-nameservers", headers=headers, timeout=120)
print(r.status_code, r.json())
```

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

### Response

`200`

```json
{
  "data": {
    "id": 9001,
    "site_id": 12345,
    "domain": "example.com",
    "registrar": "namecheap",
    "registrar_name": "Namecheap",
    "connection_id": 41,
    "trigger": "manual",
    "result": "ok",
    "message": "",
    "old_nameservers": [
      "dns1.registrar-servers.com",
      "dns2.registrar-servers.com"
    ],
    "new_nameservers": [
      "ada.ns.cloudflare.com",
      "bob.ns.cloudflare.com"
    ],
    "registry": "confirmed",
    "at": "2026-09-24T09:05:00Z"
  }
}
```

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