Inbound Email
Receive and parse incoming emails with Postkit
Postkit can receive incoming emails at your domain, parse the MIME content, extract attachments, and deliver the parsed result to your application via webhooks. This guide covers setting up inbound email processing end to end.
Prerequisites: A verified domain in Postkit. See Domain Setup. A webhook endpoint configured to receive inbound events. See Webhooks.
How inbound email works
Emails sent to your inbound address (e.g., support@acme.inbound.postkit.eu) are received by Postkit's SMTP engine. The message is then parsed -- MIME decoded, headers extracted, attachments separated, and spam scored. The parsed result is published as an email.received webhook event to your endpoint. Your application processes the parsed email data via HTTP.
The flow looks like this:
- Sender emails
support@acme.inbound.postkit.eu - Postkit's SMTP engine receives the message
- MIME content is parsed (headers, text/html body, attachments)
- Spam score is calculated
- Attachments are stored in object storage
- An
email.receivedwebhook event is sent to your endpoint
Set up inbound email
Configure your DNS
Add an MX record pointing your inbound subdomain to Postkit's mail server:
| Field | Value |
|---|---|
| Type | MX |
| Name / Host | inbound (or your chosen subdomain) |
| Priority | 10 |
| Value | mail.postkit.eu |
This tells mail servers to route emails for *@inbound.yourdomain.com to Postkit for processing.
Alternatively, use Postkit's built-in inbound subdomain -- emails to *@yourdomain.inbound.postkit.eu work automatically with no DNS changes.
Create a webhook for inbound events
Subscribe your endpoint to email.received events so your application gets notified when an inbound email arrives.
curl -X POST https://api.postkit.eu/v1/webhooks \
-H "Authorization: Bearer pk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/postkit",
"events": ["email.received"]
}'const response = await fetch('https://api.postkit.eu/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': 'Bearer pk_live_abc123...',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/webhooks/postkit',
events: ['email.received'],
}),
});
const webhook = await response.json();
// Save webhook.signing_secret for signature verificationimport requests
response = requests.post(
'https://api.postkit.eu/v1/webhooks',
headers={'Authorization': 'Bearer pk_live_abc123...'},
json={
'url': 'https://example.com/webhooks/postkit',
'events': ['email.received'],
},
)
webhook = response.json()
# Save webhook['signing_secret'] for signature verificationTest by sending an email
Send a test email to your inbound address (e.g., test@acme.inbound.postkit.eu). Your webhook endpoint should receive the parsed email within a few seconds.
Inbound email payload
When Postkit receives an inbound email, the parsed data is delivered to your webhook endpoint and is also available via the API. Here is the structure of a received email:
{
"id": "in_x9y8z7w6",
"from": {
"email": "customer@example.com",
"name": "Jane Doe"
},
"to": [
{ "email": "support@acme.inbound.postkit.eu" }
],
"subject": "Question about my order",
"text_body": "Hi, I have a question about order ORD-4821...",
"html_body": "<p>Hi, I have a question about order ORD-4821...</p>",
"headers": {
"message-id": "<abc123@example.com>",
"date": "Thu, 12 Mar 2026 10:30:00 +0100"
},
"spam_score": 0.2,
"is_spam": false,
"attachment_count": 1,
"domain": "acme.inbound.postkit.eu",
"created_at": "2026-03-12T09:30:05Z"
}Key fields:
id-- Prefixed within_to identify inbound emails (e.g.,in_x9y8z7w6)from-- An object withemailand optionalname(not a plain string like outbound emails)to-- Array of recipient objects, each withemailand optionalnametext_body/html_body-- The plain text and HTML versions of the email body (either or both may be present)spam_score-- A float from SpamAssassin (lower is better). A score of0.2is clean; scores above5.0are likely spamis_spam-- Boolean indicating whether the message exceeds the spam thresholdattachment_count-- Integer count of attached filesdomain-- The domain that received the email
Handling attachments
The attachment_count field tells you how many files are attached to the inbound email. To retrieve attachment details, use the attachments endpoint:
# List attachments for an inbound email
curl https://api.postkit.eu/v1/emails/received/in_x9y8z7w6/attachments \
-H "Authorization: Bearer pk_live_abc123..."
# Download a specific attachment
curl https://api.postkit.eu/v1/emails/received/in_x9y8z7w6/attachments/att_abc123 \
-H "Authorization: Bearer pk_live_abc123..."// List attachments for an inbound email
const attachments = await fetch(
'https://api.postkit.eu/v1/emails/received/in_x9y8z7w6/attachments',
{ headers: { 'Authorization': 'Bearer pk_live_abc123...' } }
).then(r => r.json());
// Each attachment has: id, filename, content_type, size
for (const att of attachments.data) {
// Get download URL for a specific attachment
const detail = await fetch(
`https://api.postkit.eu/v1/emails/received/in_x9y8z7w6/attachments/${att.id}`,
{ headers: { 'Authorization': 'Bearer pk_live_abc123...' } }
).then(r => r.json());
// detail.download_url is a pre-signed URL valid for 1 hour
console.log(`${att.filename}: ${detail.download_url}`);
}import requests
headers = {'Authorization': 'Bearer pk_live_abc123...'}
# List attachments for an inbound email
attachments = requests.get(
'https://api.postkit.eu/v1/emails/received/in_x9y8z7w6/attachments',
headers=headers,
).json()
# Each attachment has: id, filename, content_type, size
for att in attachments['data']:
# Get download URL for a specific attachment
detail = requests.get(
f'https://api.postkit.eu/v1/emails/received/in_x9y8z7w6/attachments/{att["id"]}',
headers=headers,
).json()
# detail['download_url'] is a pre-signed URL valid for 1 hour
print(f"{att['filename']}: {detail['download_url']}")Each attachment includes:
| Field | Type | Description |
|---|---|---|
id | string | Attachment ID (prefixed with att_) |
filename | string | Original filename |
content_type | string | MIME type (e.g., application/pdf) |
size | integer | File size in bytes |
download_url | string | Pre-signed URL valid for 1 hour (detail endpoint only) |
Spam filtering
The spam_score field (float) and is_spam boolean help you filter unwanted messages. Postkit calculates the spam score using SpamAssassin but does not silently discard inbound emails -- your application decides how to handle them.
Common approaches:
- Accept all -- Process every inbound email regardless of score. Useful for support inboxes where false positives are costly.
- Flag and review -- Store emails with
is_spam: trueseparately for manual review. - Reject high scores -- Discard emails above a threshold (e.g.,
spam_score > 8.0). Set your own threshold based on your use case.
// Example: filter by spam score in your webhook handler
app.post('/webhooks/postkit', (req, res) => {
const { data } = req.body;
if (data.is_spam || data.spam_score > 5.0) {
console.log(`Spam detected (score: ${data.spam_score}), skipping`);
return res.sendStatus(200);
}
// Process the legitimate inbound email
processInboundEmail(data);
res.sendStatus(200);
});List received emails
Retrieve your inbound emails via the API with cursor-based pagination:
# List recent inbound emails
curl https://api.postkit.eu/v1/emails/received \
-H "Authorization: Bearer pk_live_abc123..."
# Filter by sender
curl "https://api.postkit.eu/v1/emails/received?from=customer@example.com" \
-H "Authorization: Bearer pk_live_abc123..."
# Filter by domain
curl "https://api.postkit.eu/v1/emails/received?domain=acme.inbound.postkit.eu" \
-H "Authorization: Bearer pk_live_abc123..."// List recent inbound emails
const response = await fetch(
'https://api.postkit.eu/v1/emails/received',
{ headers: { 'Authorization': 'Bearer pk_live_abc123...' } }
);
const { data, has_more } = await response.json();
// Paginate with cursor
if (has_more) {
const lastId = data[data.length - 1].id;
const nextPage = await fetch(
`https://api.postkit.eu/v1/emails/received?starting_after=${lastId}`,
{ headers: { 'Authorization': 'Bearer pk_live_abc123...' } }
).then(r => r.json());
}import requests
headers = {'Authorization': 'Bearer pk_live_abc123...'}
# List recent inbound emails
response = requests.get(
'https://api.postkit.eu/v1/emails/received',
headers=headers,
)
result = response.json()
# Paginate with cursor
if result.get('has_more'):
last_id = result['data'][-1]['id']
next_page = requests.get(
f'https://api.postkit.eu/v1/emails/received?starting_after={last_id}',
headers=headers,
).json()The response returns a data array of received emails and a has_more boolean for pagination. Use starting_after with the last email's id to fetch the next page.
What's next?
- Webhooks -- set up webhook endpoints and verify signatures
- Domain Setup -- configure DNS records for your domain