Sign in Create account
API reference

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

Full-access key

Query parameters

  • limitinteger
    From 1 to 100. Default: 20.
  • afterstring
    Cursor for the next page.
  • beforestring
    Cursor for the previous page.
  • searchstring
    Searches the name or the start of the token.
  • permissionstring
    full_access or sending_access.

Errors

  • 422validation_error
    A field is invalid. The message says which one.
GET /api-keys

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);
200Response
{
  "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

Full-access key

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

  • namestringrequired
    A name to identify the key, up to 50 characters.
  • permissionstring
    full_access (default): the whole API. sending_access: sending (POST /emails, POST /emails/batch, POST /events and SMTP), plus reading GET /emails/{id} (only the emails it sent) and GET /usage.
  • domain_idstring
    Restricts a sending_access key to one domain. It cannot be used with full_access.

Errors

  • 422validation_error
    Missing or too long name, invalid permission, a domain of another team, or domain_id with full_access.
  • 403plan_limit_reached
    The plan does not allow more keys, or does not include restricted keys (sending_access or with domain_id).
POST /api-keys

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);
201Response
{
  "id": "0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
  "token": "ps_4Fq9xLmT2vKp8sWd1Yc5Hf3Lz9Ag6Ej0Qm8rBnX"
}

#Retrieve a key

GET/api-keys/{id}

Full-access key

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

  • idstringrequired
    Key ID.

Errors

  • 404not_found
    The resource does not exist or belongs to another team.
GET /api-keys/{id}

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);
200Response
{
  "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}

Full-access key

Only the name can change. To change the permission, create a new key and delete the old one.

Path parameters

  • idstringrequired
    Key ID.

Request body

  • namestringrequired
    New name, up to 50 characters.

Errors

  • 422validation_error
    A field is invalid. The message says which one.
  • 403paused_by_plan
    The key is paused because it is above the plan's key limit.
  • 404not_found
    The resource does not exist or belongs to another team.
PATCH /api-keys/{id}

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);
200Response
{
  "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}

Full-access key

Revokes the key immediately: requests and SMTP connections that use it start to receive 403 invalid_api_key.

Path parameters

  • idstringrequired
    Key ID.

Errors

  • 404not_found
    The resource does not exist or belongs to another team.
DELETE /api-keys/{id}

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);
200Response
{
  "object": "api_key",
  "id": "0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
  "deleted": true
}