API keys
Create, list, rename and revoke API keys.
Manage the keys used to authenticate with the API and with SMTP. The full token is shown only once, when you create the key: Postal stores only its hash. Learn about the permissions in Authentication. If the plan shrinks and the team has more keys than the new limit, the newest ones are paused (paused_by_plan: true): they stay in the list, but every request made with a paused key returns 403 paused_by_plan (see Plan limits).
#List keys
GET/api-keys
Query parameters
limitintegerFrom 1 to 100. Default: 20.afterstringCursor for the next page.beforestringCursor for the previous page.searchstringSearches the name or the start of the token.permissionstringfull_accessorsending_access.
Errors
- 422
validation_errorA field is invalid. The message says which one.
cURL
curl 'https://api.postal.so/api-keys?permission=sending_access' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/api-keys?permission=sending_access', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/api-keys",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"permission": "sending_access",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('api-keys', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'permission' => 'sending_access',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": false,
"data": [
{
"id": "0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
"name": "Production - checkout",
"permission": "sending_access",
"domain_id": "0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f",
"token_start": "ps_4Fq9xLm",
"uses_count": 1284,
"created_at": "2026-10-02T18:40:00.000000Z",
"last_used_at": "2026-10-24T13:02:11.000000Z",
"paused_by_plan": false
}
]
}
#Create a key
POST/api-keys
Creates the key and returns the full token. Store it now: it cannot be retrieved later. Members who chose to receive security alerts are told by email that the key was created. The plan defines how many keys the team can have and whether it can create restricted keys (sending_access or limited to one domain).
Request body
namestringrequiredA name to identify the key, up to 50 characters.permissionstringfull_access(default): the whole API.sending_access: sending (POST /emails,POST /emails/batch,POST /eventsand SMTP), plus readingGET /emails/{id}(only the emails it sent) andGET /usage.domain_idstringRestricts asending_accesskey to one domain. It cannot be used withfull_access.
Errors
- 422
validation_errorMissing or too long name, invalid permission, a domain of another team, ordomain_idwithfull_access. - 403
plan_limit_reachedThe plan does not allow more keys, or does not include restricted keys (sending_accessor withdomain_id).
cURL
curl -X POST https://api.postal.so/api-keys \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production - checkout",
"permission": "sending_access",
"domain_id": "0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f"
}'
Node.js
const response = await fetch('https://api.postal.so/api-keys', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Production - checkout',
permission: 'sending_access',
domain_id: '0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/api-keys",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"name": "Production - checkout",
"permission": "sending_access",
"domain_id": "0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('api-keys', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'name' => 'Production - checkout',
'permission' => 'sending_access',
'domain_id' => '0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f',
],
]);
$data = json_decode($response->getBody(), true);
{
"id": "0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
"token": "ps_4Fq9xLmT2vKp8sWd1Yc5Hf3Lz9Ag6Ej0Qm8rBnX"
}
#Retrieve a key
GET/api-keys/{id}
Returns the key with its domain (when restricted) and who created it. The token is never returned; token_start shows the first characters so you can recognize the key.
Path parameters
idstringrequiredKey ID.
Errors
- 404
not_foundThe resource does not exist or belongs to another team.
cURL
curl https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"id": "0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
"name": "Production - checkout",
"permission": "sending_access",
"domain_id": "0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f",
"token_start": "ps_4Fq9xLm",
"uses_count": 1284,
"domain": {
"id": "0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f",
"name": "acmestore.com"
},
"created_by": {
"name": "Rachel Lane",
"email": "[email protected]"
},
"created_at": "2026-10-02T18:40:00.000000Z",
"last_used_at": "2026-10-24T13:02:11.000000Z",
"paused_by_plan": false
}
#Rename a key
PATCH/api-keys/{id}
Only the name can change. To change the permission, create a new key and delete the old one.
Path parameters
idstringrequiredKey ID.
Request body
namestringrequiredNew name, up to 50 characters.
Errors
- 422
validation_errorA field is invalid. The message says which one. - 403
paused_by_planThe key is paused because it is above the plan's key limit. - 404
not_foundThe resource does not exist or belongs to another team.
cURL
curl -X PATCH https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production - checkout v2"
}'
Node.js
const response = await fetch('https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Production - checkout v2',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.patch(
"https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"name": "Production - checkout v2",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->patch('api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'name' => 'Production - checkout v2',
],
]);
$data = json_decode($response->getBody(), true);
{
"id": "0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
"name": "Production - checkout v2",
"permission": "sending_access",
"domain_id": "0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f",
"token_start": "ps_4Fq9xLm",
"uses_count": 1284,
"created_at": "2026-10-02T18:40:00.000000Z",
"last_used_at": "2026-10-24T13:02:11.000000Z",
"paused_by_plan": false
}
#Delete a key
DELETE/api-keys/{id}
Revokes the key immediately: requests and SMTP connections that use it start to receive 403 invalid_api_key.
Path parameters
idstringrequiredKey ID.
Errors
- 404
not_foundThe resource does not exist or belongs to another team.
cURL
curl -X DELETE https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.delete(
"https://api.postal.so/api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->delete('api-keys/0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "api_key",
"id": "0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
"deleted": true
}