Email API and SMTP relay / Hosted in Germany
Transactional email that stays in the EU.
Receipts, password resets and alerts, sent from your own domain. Messages, logs and inbound mail are stored on servers in Germany.
Message 48213 / ana@example.com
14:20:01 Accepted by the API 14:20:02 250 2.0.0 OK gmail-smtp-in 14:20:03 email.delivered sent, 200
Authentication-Results: dkim=pass header.d=acme.eu spf=pass smtp.mailfrom=psrp.acme.eu dmarc=pass List-Unsubscribe-Post: List-Unsubscribe=One-Click
Hosted in Germany (EU)
REST + SMTP 587 / 2525
Standard Webhooks
OpenAPI 3.1
zsender-sdk, 0 dependencies
One pipeline, four jobs
Every send leaves a trail you can read later, line by line.
Send
- REST API
- SMTP relay
- TypeScript SDK
- Idempotency keys
Verify
- SPF, DKIM, return path
- Found vs expected value
- Subdomain-aware hosts
- Test keys
Watch
- SMTP transcripts
- Spam score with rules
- Signed webhooks
- Tags and analytics
Receive
- Inbound to webhook
- 30-day message store
- Raw MIME on request
- Suppression list
Underneath: one mail server in Germany, Postgres for every event, one error contract with 16 codes.
Why transactional mail goes quiet
Mail rarely fails loudly. It fails in a DNS record, a retry or a bounce nobody reads.
SPF INVALID found v=spf1 include:_spf.google.com expect include:spf.zsender.io
The record is there, and wrong.
We show the value we found next to the one we expect, so you fix the right line.
POST /v1/emails/send Idempotency-Key: rcpt-2041 (timeout, retry) 200, same message, sent once
The timeout that sends twice.
Retry with the same key and the receipt still arrives one time.
email.bounced ana@exmaple.com suppressed HARD_BOUNCE next send skipped, listed in response
The bounce nobody hears about.
Hard bounces go on your suppression list, and every later API send lists the recipients it skipped.
What you get
Four things, done carefully.
Domains
Send from your own domain.
Publish the records in one visit to your DNS provider. One check covers sending and receiving, and tells you which line is wrong.
acme.eu / DNS
Test mode
Test without sending.
Keys that start with zst_ run the real pipeline and stop before delivery. Your CI can send a hundred receipts and none reach a customer.
Sandbox inbox
Observability
Know what happened.
Every message keeps the receiving server's reply, the spam score with the rules behind it, and each event we sent to your webhook.
Message 48213
Inbound
Receive mail as JSON.
Point an MX record at us. Each message is stored for 30 days and announced to your endpoints as email.received, signed.
email.received
Built in the open
What we found while building it.
Faults we hit in real services, measured against their source and fixed. Written up so you do not hit them too.
- 2026-10-05@fastify/cors v11 blocks browser DELETE and PATCHA major bump of @fastify/cors narrowed its default methods to GET, HEAD and POST. Browsers stopped sending DELETE and PATCH, and our server logged no error.
- 2026-10-05pino logs your Error as {} unless the key is errpino runs its error serializer on the key err only. Under any other key an Error goes through JSON.stringify and is logged as an empty object.
- 2026-10-05SpamAssassin DNSBLs go quiet on shared DNSBlocklists refuse queries that arrive through a shared DNS resolver. SpamAssassin scores the refusal at 0.001, and the spam score still looks normal.
Data residency
What stays in the EU.
What we store about the mail you send stays in Germany. The providers outside the EU, and what each one sees, are named in the privacy policy.
- Message content, headers, recipients
- Germany
- Delivery logs, events, webhook payloads
- Germany
- Inbound mail and attachments, 30 days
- Germany
- Billing, analytics after consent, network security, internal alerts, the IP lookup at signup
- Outside the EU
One key. Your domain. Mail from Germany.
1,000 emails a month free, no card. Pro is $29 a month for 50,000.