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/providersScope 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-sendingScope 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
| 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/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-sendingScope 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
site_id | path | integer | yes | The site id (see GET /sites). |
provider | body | string (one of: resend, sendgrid, brevo, mailgun, ses, postmark, smtp) | yes | See GET /mail-sending/providers. |
secret | body | string | no | The API key / SMTP key / SMTP password. Required the first time and when the provider or server changes; leave out to keep the stored one. |
username | body | string | no | The SMTP login (Brevo, Mailgun, Amazon SES, smtp). |
region | body | string | no | Amazon SES: e.g. eu-west-1. Mailgun: us or eu. |
host | body | string | no | provider "smtp" only: the SMTP server name. |
port | body | integer (one of: 465, 587) | no | 465 (TLS) or 587 (STARTTLS). |
from_address | body | string | no | The address the website sends as (default noreply@<site domain>). Must be verified at the provider. |
force_from | body | boolean | no | Always 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-sendingScope sites:write · destructive
Switches the site back to our mail servers within a minute. The settings are kept unless forget=true.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
site_id | path | integer | yes | The site id (see GET /sites). |
forget | query | boolean | no | Also 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/testScope 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
site_id | path | integer | yes | The site id (see GET /sites). |
to | body | string | yes | Where 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