PBN.LTD API docs
View as Markdown

Website e-mail sending

Send a website's e-mail through your own provider (Resend, SendGrid, Brevo, Mailgun, Amazon SES, Postmark or any SMTP server) instead of our mail servers, and test it.

Providers you can send through

GET/api/v1/mail-sending/providers

Scope sites:read

The providers a site can send through and what each needs: a key only (Resend, SendGrid, Postmark), a login and key (Brevo, Mailgun, Amazon SES, which also need a region), or a server, port, user name and password (provider "smtp").

Example

curl -s "https://app.pbn.ltd/api/v1/mail-sending/providers" \
  -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/mail-sending/providers", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/mail-sending/providers", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": [
    {
      "key": "resend",
      "name": "Resend",
      "host": "smtp.resend.com",
      "ports": [
        465,
        587
      ],
      "default_port": 465,
      "asks_user": false,
      "asks_host": false,
      "user_label": "",
      "secret_label": "API key",
      "regions": null,
      "docs": "https://resend.com/docs/send-with-smtp"
    }
  ]
}

Errors: rate_limited, scope_missing, unauthorized

Where a site's e-mail goes out

GET/api/v1/sites/{site_id}/mail-sending

Scope sites:read

"route": "ours" = our mail servers (the default); "own" = the customer's own provider, with its state ("pending" while being set up on the server, "active", "error" + state_note). The key is never returned, only key_hint.

Parameters

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).

Example

curl -s "https://app.pbn.ltd/api/v1/sites/123/mail-sending" \
  -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/mail-sending", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/mail-sending", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "route": "own",
    "provider": "resend",
    "provider_name": "Resend",
    "region": null,
    "host": "smtp.resend.com",
    "port": 465,
    "username": null,
    "key_hint": "...9f2a",
    "from_address": "[email protected]",
    "force_from": true,
    "enabled": true,
    "locked_by_staff": false,
    "state": "active",
    "state_note": "",
    "confirmed_at": "2026-09-26T16:00:00Z",
    "last_test": {
      "at": "2026-09-26T16:01:00Z",
      "ok": true,
      "to": "[email protected]",
      "message": "Sent. Resend accepted the test e-mail (250 OK)."
    },
    "last_send": null,
    "updated_at": "2026-09-26T16:00:00Z"
  }
}

Errors: not_found, rate_limited, scope_missing, unauthorized

Send through my own provider

PUT/api/v1/sites/{site_id}/mail-sending

Scope sites:write

Stores the settings (the key encrypted) and switches the site's e-mail to that provider within a minute. Check "state", then POST .../mail-sending/test.

Parameters

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
providerbodystring (one of: resend, sendgrid, brevo, mailgun, ses, postmark, smtp)yesSee GET /mail-sending/providers.
secretbodystringnoThe API key / SMTP key / SMTP password. Required the first time and when the provider or server changes; leave out to keep the stored one.
usernamebodystringnoThe SMTP login (Brevo, Mailgun, Amazon SES, smtp).
regionbodystringnoAmazon SES: e.g. eu-west-1. Mailgun: us or eu.
hostbodystringnoprovider "smtp" only: the SMTP server name.
portbodyinteger (one of: 465, 587)no465 (TLS) or 587 (STARTTLS).
from_addressbodystringnoThe address the website sends as (default noreply@<site domain>). Must be verified at the provider.
force_frombodybooleannoAlways send as from_address (recommended: providers refuse unverified senders). Default: True.

Example

curl -s -X PUT "https://app.pbn.ltd/api/v1/sites/123/mail-sending" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.put("https://app.pbn.ltd/api/v1/sites/123/mail-sending", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/mail-sending", {
  method: "PUT",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "route": "own",
    "provider": "resend",
    "provider_name": "Resend",
    "region": null,
    "host": "smtp.resend.com",
    "port": 465,
    "username": null,
    "key_hint": "...9f2a",
    "from_address": "[email protected]",
    "force_from": true,
    "enabled": true,
    "locked_by_staff": false,
    "state": "active",
    "state_note": "",
    "confirmed_at": "2026-09-26T16:00:00Z",
    "last_test": {
      "at": "2026-09-26T16:01:00Z",
      "ok": true,
      "to": "[email protected]",
      "message": "Sent. Resend accepted the test e-mail (250 OK)."
    },
    "last_send": null,
    "updated_at": "2026-09-26T16:00:00Z"
  }
}

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

Send through our mail servers again

DELETE/api/v1/sites/{site_id}/mail-sending

Scope sites:write · destructive

Switches the site back to our mail servers within a minute. The settings are kept unless forget=true.

Parameters

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
forgetquerybooleannoAlso delete the stored key and settings. Default: False.

Example

curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/mail-sending" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/sites/123/mail-sending", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/mail-sending", {
  method: "DELETE",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "route": "ours",
    "provider": null
  }
}

Errors: not_found, rate_limited, scope_missing, unauthorized

Send a test e-mail

POST/api/v1/sites/{site_id}/mail-sending/test

Scope sites:write

Sends one real e-mail the way the website does (from inside the site, through the provider) and returns the provider's answer in plain words. At most 5 tests per site per 10 minutes.

Parameters

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
tobodystringyesWhere to send the test e-mail.

Example

curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/mail-sending/test" \
  -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/mail-sending/test", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/mail-sending/test", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "ok": true,
    "message": "Sent. Resend accepted the test e-mail."
  }
}

Errors: not_found, rate_limited, scope_missing, unauthorized, validation_failed