postkit
Guides

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:

  1. Sender emails support@acme.inbound.postkit.eu
  2. Postkit's SMTP engine receives the message
  3. MIME content is parsed (headers, text/html body, attachments)
  4. Spam score is calculated
  5. Attachments are stored in object storage
  6. An email.received webhook 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:

FieldValue
TypeMX
Name / Hostinbound (or your chosen subdomain)
Priority10
Valuemail.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 verification
import 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 verification

Test 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 with in_ to identify inbound emails (e.g., in_x9y8z7w6)
  • from -- An object with email and optional name (not a plain string like outbound emails)
  • to -- Array of recipient objects, each with email and optional name
  • text_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 of 0.2 is clean; scores above 5.0 are likely spam
  • is_spam -- Boolean indicating whether the message exceeds the spam threshold
  • attachment_count -- Integer count of attached files
  • domain -- 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:

FieldTypeDescription
idstringAttachment ID (prefixed with att_)
filenamestringOriginal filename
content_typestringMIME type (e.g., application/pdf)
sizeintegerFile size in bytes
download_urlstringPre-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: true separately 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