CSV exports API
CSV files with emails, domains, logs, keys, suppressions and contacts.
Generate CSV files with emails, domains, logs, API keys, suppressions, contacts (properties go in one JSON column), the submissions of a form or the visits to a form. The export runs in the background: create it, poll until status is completed and download it from download_url. Files stay available for 7 days and the download link is valid for 1 hour (fetch the export again to generate another one). Exporting is part of the plan (exports): without it, creating one answers 403 plan_limit_reached.
cURL
curl https://api.postal.so/exports \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/exports', {
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/exports",
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('exports', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": false,
"data": [
{
"object": "export",
"id": "019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c",
"type": "emails",
"filters": {
"status": "bounced"
},
"status": "completed",
"rows": 312,
"total_rows": 312,
"error": null,
"created_at": "2026-10-24T13:20:00.000000Z",
"completed_at": "2026-10-24T13:20:04.000000Z",
"expires_at": "2026-10-31T13:20:00.000000Z",
"filename": "postal-emails-2026-10-24-132000.csv",
"download_url": "https://api.postal.so/exports/019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c/download?expires=1761315600&signature=7b1d…"
}
]
}
#Create an export
POST/exports
The filters are the same as those of the corresponding list: emails, domains, logs, API keys and contacts (suppressions have no filters). For form_submissions and form_visits, the form_id filter is required (one form per export) and the form must exist (404 if not). If an identical export is still running, it is returned (status 200) instead of creating another. Each team can have up to 3 exports running at a time. For form_submissions, an answer typed in the “Other” option comes out as Other: text or Outro: texto, in the language of the request that created the export (Accept-Language).
Request body
typestringrequiredemails,domains,logs,api_keys,suppressions,contacts,form_submissionsorform_visits.filtersobjectList filters, such as{"status": "bounced", "start_date": "2026-10-01"}. Forform_submissions, onlyform_id. Forform_visits:form_idand, optionally,fromandto(a date or a date-time; a date alone intocovers the whole day).
Errors
- 403
plan_limit_reachedThe plan does not include exports. - 404
not_foundThe form inform_iddoes not exist. - 422
validation_errorInvalid type or filter, or a missingform_id. - 429
rate_limit_exceededThere are already 3 exports running.
cURL
curl -X POST https://api.postal.so/exports \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "emails",
"filters": {
"status": "bounced"
}
}'
Node.js
const response = await fetch('https://api.postal.so/exports', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'emails',
filters: {
status: 'bounced',
},
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/exports",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"type": "emails",
"filters": {
"status": "bounced",
},
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('exports', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'type' => 'emails',
'filters' => [
'status' => 'bounced',
],
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "export",
"id": "019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c",
"type": "emails",
"filters": {
"status": "bounced"
},
"status": "pending",
"rows": 0,
"total_rows": null,
"error": null,
"created_at": "2026-10-24T13:20:00.000000Z",
"completed_at": null,
"expires_at": "2026-10-31T13:20:00.000000Z",
"filename": "postal-emails-2026-10-24-132000.csv",
"download_url": null
}
#Get an export
GET/exports/{id}
Shows the progress (rows out of total_rows) and, when ready, the download link. status: pending, processing, completed or failed (with error).
Path parameters
idstringrequiredExport ID.
Errors
- 404
not_foundThe resource does not exist or belongs to another team.
cURL
curl https://api.postal.so/exports/019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/exports/019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c', {
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/exports/019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c",
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('exports/019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "export",
"id": "019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c",
"type": "emails",
"filters": {
"status": "bounced"
},
"status": "completed",
"rows": 312,
"total_rows": 312,
"error": null,
"created_at": "2026-10-24T13:20:00.000000Z",
"completed_at": "2026-10-24T13:20:04.000000Z",
"expires_at": "2026-10-31T13:20:00.000000Z",
"filename": "postal-emails-2026-10-24-132000.csv",
"download_url": "https://api.postal.so/exports/019a1c10-7e8f-7a90-8b1c-2d3e4f5a6b7c/download?expires=1761315600&signature=7b1d…"
}