Sign in Create account
Getting started

API authentication and API keys

Every request uses an API key in the Authorization header. Keys can have full access or sending-only access, and can be restricted to one domain.

#API key

Every request needs an API key in the Authorization header, using the Bearer scheme:

Header
Authorization: Bearer ps_4Fq9xLmT2vKp8sWd1Yc5Hf3Lz9Ag6Ej0Qm8rBnX

Keys start with ps_. Create one under API keys in the dashboard, or through the API with POST /api-keys. The full token is shown only once, when you create it: Postal stores just the hash and the first characters (token_start), so you can recognize the key in the list.

GET /domains

cURL

curl https://api.postal.so/domains \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/domains', {
  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/domains",
    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('domains', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);

#Permissions

PermissionWhat it can do
full_access
Full access
The whole API: sending, domains, templates, webhooks, logs, keys…
sending_access
Sending only
Send: POST /emails, POST /emails/batch, POST /events and SMTP.
Read: the status and events of the emails it sent itself, by API or SMTP (GET /emails/{id}; emails sent by other keys or from the dashboard answer 404), and the plan, quotas and rate limit in GET /usage. Everything else needs full access.
It can be restricted to one domain (domain_id): it then sends only from addresses on that domain.

Creating sending-only or domain-restricted keys depends on the plan (restricted_api_keys), and so does the number of keys (api_keys). See Limits.

Use sending-only keys in systems that just trigger emails or events (the checkout, WordPress, the ERP) and keep full-access keys for infrastructure automation. If a sending-only key leaks, it cannot change your settings or read your contacts, domains or the emails of other keys: it only sees what it sent itself and the plan limits.

#Authentication errors

StatusNameWhen it happens
401missing_api_keyThe Authorization header is missing.
403invalid_api_keyThe key does not exist, was deleted or does not start with ps_.
401restricted_api_keyA sending-only key tried to use another endpoint (it can send, read the emails it sent with GET /emails/{id} and call GET /usage).
403restricted_api_keyA key restricted to one domain tried to send from another domain.
403paused_by_planThe key went over the API key limit of the current plan and was paused (paused_by_plan: true in GET /api-keys). It does not authenticate until the plan or the count allows it again; see Limits.
403team_suspendedThe account was suspended by the platform that manages it.

#Message language and source of changes

Two optional choices per request:

  • Accept-Language: pt-BR (or en) picks the language of the message in errors and of the other texts the API writes for your request (see the table below). Without the header it comes in English. The name is always the same.
  • X-Postal-Source: erp labels the changes the key makes to contacts. The label appears in source in the contact webhooks and in the contact's last_change_source, and lets a webhook skip what your own system did.

The language of each text Postal writes depends on who it is for:

TextLanguage
The texts in API responsesThe error message, the labels (such as the label in paused of GET /usage), the sending.reason of the same endpoint, the diagnostic of the domain DNS records, the suffix of copy names (“(copy)” or “(cópia)”), the [Test] or [Teste] prefix of the broadcast test and the template test and the “Other:” or “Outro:” in the form answer CSV files. They follow the request's Accept-Language (pt-BR or en, the first one Postal knows). With an API key or without credentials, they come in English without the header; GET /plans, which is public, comes in Portuguese without it.
Text of the diagnostics AIPOST /ai/emails/{id}/fix-insights: the language parameter (pt-BR, en or es); without it, the language of the request.
Sending pause webhooksThe reason comes in the team's language, the one the owner chose in the dashboard; English until they choose. The reason_code does not depend on language.
Unsubscribe footer and preferences pageEach recipient's language: the broadcast's language, the contact's properties, the team's language. See Footer and page language.
Public forms and confirmation emailThe form's settings.language, else the Accept-Language of whoever fills it in, else pt-BR. See Form language.
Portuguese on purposeThe default templates of the Supabase authentication emails, which you edit on the connection.

#Best practices

  • Keep the key in an environment variable or a secrets vault, never in code. Do not use the key in a browser or a mobile app: anyone could copy it. Call the API from your server.
  • Use one key per system and per environment. That way you can revoke one without breaking the others and see in the logs who made each request.
  • If a key leaks, delete it right away (DELETE /api-keys/{id}) and create another.
  • To give an AI assistant access to your account, prefer OAuth on the MCP server, which limits access to one team and to scopes and can be revoked without changing a key.