# EmailConnect.eu > Email-in → signed-webhook-out trigger layer for agents and automations. 100% EU-hosted, GDPR-compliant inbound email-to-webhook processing: forward email to an alias, get structured JSON at your endpoint. Fire-and-forget — no mailbox access, no outbound sending. EmailConnect converts incoming emails into structured JSON webhooks. It is purpose-built for inbound email processing — no outbound sending, no AI interpretation, no US infrastructure. Forward specific emails to an alias, and EmailConnect parses them into a rich payload (text, HTML, markdown, attachments, authentication results, spam scores, classification) and delivers it to your webhook. For agents and automations, the integration path is a scoped API key plus three REST calls — see [For agents & automation](https://emailconnect.eu/integrations/agents/). Platform-specific setup guides live in the [integrations directory](https://emailconnect.eu/integrations/), including [Supabase Edge Functions](https://emailconnect.eu/integrations/supabase-edge-functions/). The service runs entirely on EU infrastructure (Hetzner in Germany, Scaleway in the Netherlands) with the company headquartered in Leiden, Netherlands (incorporated in Amsterdam). It follows a Unix philosophy: do one thing well — deliver clean, structured email data so you can build your own automation on top. --- ## How It Works EmailConnect provides two deployment models: ### System Aliases (Basic) Format: `rnd5chars+alias@user.emailconnect.eu` - Instant setup, no DNS required - Text and HTML email body - **Inline attachments only — system domains have no S3 offloading.** Anything that would be offloaded is excluded with `excludeReason: "system-domain-no-storage"`. Inline caps are the normal plan caps (128KB per file on Free, up to 2MB on paid plans) - Good for: prototyping, personal workflows, quick integrations ### Custom Domains (Advanced) Format: `support@yourdomain.com` - Requires MX and TXT DNS record configuration - Full HTML support - **Maximum email size is 10MB** (raw MIME — headers plus base64-encoded attachments — for the whole message, on every plan). Larger emails are rejected at ingest; nothing is delivered - On paid plans, attachments that can't be inlined are offloaded to S3 (see Attachment Processing) - Professional appearance, brand preservation - Per-alias webhook routing ### Email Processing Pipeline 1. **SMTP Reception**: TLS-encrypted email accepted on EU infrastructure (Hetzner DE). No data leaves the EU. 2. **Spam Filtering**: rspamd-based spam scoring (negative = cleaner, higher = more suspicious). Score included in payload for your threshold logic. Known spam sources blocked outright. 3. **Authentication Verification**: DKIM (proves email not altered), SPF (confirms authorized sender), DMARC (domain owner policy). All results included in payload. 4. **Parsing & Extraction**: Raw email converted to structured JSON. Body in 3 formats (plain text, HTML, markdown). Links extracted. Attachments processed. Envelope data included. 5. **Webhook Delivery**: HTTP POST to your configured URL. Signed using Standard Webhooks (HMAC-SHA256). Automatic retries with exponential backoff and ±10% jitter (6 attempts over 12+ hours). 30-second timeout. --- ## Webhook Payload Reference Every email delivered to an alias triggers an HTTP POST to the configured webhook URL. The payload is a JSON object containing all parsed email data. ### Complete Payload Structure The payload's top-level keys are: `message`, `envelope`, `spam`, `classification`, `integrity`, `security`, plus the routing IDs `domainId` and `aliasId`. Lower plans and per-alias settings remove blocks — the field list below says which. Below is a Business+ delivery with every optional block present. ```json { "message": { "sender": { "email": "alice@example.com", "name": "Alice Martin" }, "recipient": { "email": "invoices+acme@yourdomain.com", "name": null, "tag": "acme" }, "subject": "Invoice #2026-0042", "content": { "text": "Hi team,\n\nPlease find invoice #2026-0042 attached.\n\nBest,\nAlice", "html": "

Hi team,

Please find invoice #2026-0042 attached.

", "markdown": "Hi team,\n\nPlease find invoice #2026-0042 attached.", "links": [ { "url": "https://billing.example.com/inv/2026-0042", "domain": "billing.example.com", "anchorText": "View it online", "source": "html" } ] }, "date": "2026-07-13T10:30:00.000Z", "attachments": [ { "filename": "invoice-2026-0042.pdf", "contentType": "application/pdf", "size": 245680, "downloadUrl": "https://app.emailconnect.eu/attachments/9f8e7d6c5b4a3f2e/download", "status": "completed", "uploadType": "sync", "virusScan": { "status": "clean", "duration": 42 } } ] }, "envelope": { "messageId": "", "xMailer": null, "xOriginalTo": "invoices+acme@yourdomain.com", "returnPath": "alice@example.com", "allRecipients": { "to": ["invoices+acme@yourdomain.com"], "cc": [], "bcc": [] }, "headers": { "from": "alice@example.com", "subject": "Invoice #2026-0042" }, "processed": { "timestamp": "2026-07-13T10:30:01.000Z", "domain": "yourdomain.com", "alias": "invoices@yourdomain.com", "originalSize": 248320 } }, "spam": { "score": -0.99, "engine": "rspamd", "symbols": [ { "name": "R_DKIM_ALLOW", "weight": -0.2, "description": "example.com:s=selector1" } ], "authentication": { "dkim": { "result": "pass", "domain": "example.com" }, "spf": { "result": "pass", "domain": "example.com" }, "dmarc": { "result": "pass", "domain": "example.com" } } }, "classification": { "type": "normal", "confidence": "definite", "signals": [ { "header": "return-path", "value": "alice@example.com" } ] }, "integrity": { "contentHash": "a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1", "rawEmailHash": "9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e" }, "security": { "virusScan": { "scanned": true, "engine": "clamav", "attachmentsScanned": 1, "attachmentsSkipped": 0, "threatsFound": 0 } }, "domainId": "dom_a1b2c3", "aliasId": "ali_d4e5f6" } ``` **Field names that are commonly guessed wrong — get these right or your integration breaks:** - Links use **`anchorText`**, never `text`. - Attachments use **`downloadUrl`**, never `url`. - `classification.signals` is an array of **objects** (`{header, value}`), not an array of strings. - `classification.type` is one of exactly `bounce`, `auto-reply`, `mailing-list`, `read-receipt`, `normal`. There is no `transactional`, `marketing`, `notification` or `automated` — those values can never be emitted. - **There is no `spam.isSpam`, no `spam.threshold`, no `spam.action`.** You compare `spam.score` against your own cut-off. - `integrity` holds `contentHash` and `rawEmailHash` only. There is **no `integrity.signature`** — the signature is an HTTP header (see Webhook Signing). `contentHash` is a bare hex digest, with no `sha256:` prefix. ### Payload Fields by Plan **All Plans (including Free):** - `message.date` — ISO 8601 timestamp of when the email was sent - `message.sender.name`, `message.sender.email` — Sender information (`name` is `null` when the address carried none) - `message.recipient.name`, `message.recipient.email` — Recipient alias that received the email, exactly as written, including any `+tag` - `message.recipient.tag` — The `+tag` from a plus-addressed recipient (`inbox+support@domain.tld` → `"support"`), or `null` when the address carried none. Plus-addressed mail is delivered to the webhook on the base `inbox@` alias, so one alias can carry unlimited routing keys. Treat it as untrusted input — senders invent tags freely - `message.subject` — Email subject line (nullable) - `message.content.text` — Plain text body (per-alias `includeText`) - `message.content.html` — HTML body (per-alias `includeHtml`) - `message.content.links[]` — Extracted URLs. Free receives `{url, domain}` only - `message.attachments[]` — Capped at 20 per email. `content` (base64) when inlined, `downloadUrl` when offloaded, plus `filename`, `contentType`, `size`, `status`, `storage`, `uploadType`, and `excluded`/`excludeReason`/`manageUrl` when a file was not delivered - `envelope` — Technical envelope (`messageId`, `returnPath`, `xMailer`, `xOriginalTo`, `allRecipients`, raw `headers`, `processed`). Per-alias `includeEnvelope`; when off, only `messageId` remains - `classification.type`, `classification.confidence`, `classification.signals` — Header-derived routing hints. **Not gated** — every plan gets all three - `domainId`, `aliasId` — Routing IDs. `aliasId` is `null` for domain-level and catch-all webhooks - **Per-field include/exclude toggles** — every plan can choose which fields an alias delivers **Maker Plan and above:** - `message.content.markdown` — Server-side Markdown conversion of the HTML body. Requires the per-alias `includeMarkdown` setting, which is **off by default** - `message.content.reply` — The new content of a reply with the quoted thread stripped. Requires the per-alias `includeReplyParsing` setting, **off by default** - `message.content.links[].anchorText`, `.source` — Link anchor text, and which body the link came from - `message.attachments[].metadata` — `hash`, `width`, `height`, `pageCount` - `integrity.contentHash`, `integrity.rawEmailHash` — SHA-256 hashes for validation - `spam.*` — Full spam analysis (`score`, `symbols`, `authentication`, `engine`, `report`). **On Free, `spam` is present but is a stub** — `{available: false, message: "..."}`. It is truthy, so test for `spam.score`, not for the presence of `spam` - S3 offloading of attachments that can't be inlined - Custom webhook headers **Business Plan and above:** - `security.virusScan` and `message.attachments[].virusScan` — ClamAV scanning (also requires the virus-scanning setting to be enabled) - Custom payload **template** — a mustache (`{{path}}`) template that reshapes the payload into whatever your endpoint expects - Configurable retention, analytics dashboard, `DELETE /api/v1/emails/{messageId}` - All Maker features included --- ## Attachment Processing **The hard ceiling is the 10MB maximum email size** — raw MIME, headers plus base64-encoded attachments, for the *whole message*, on every plan. An email over 10MB is **rejected at ingest**: nothing is delivered, no webhook fires, and a notification is created in the dashboard. **Do not tell users to "use S3 for files over 10MB".** There is no such path. S3 offloading happens *inside* the 10MB envelope — the whole email is rejected before any attachment reaches storage. In practice S3 handles attachments roughly 2MB–10MB on paid plans (and any non-inlineable type at any size). Files bigger than 10MB have to be sent as a link, not as an attachment. ### How Attachments Are Handled Attachment delivery depends on three factors: your plan, whether the file type is inline-appropriate, and the per-attachment size. **Inline-appropriate types**: text (.txt, .csv, .json, .xml, .yaml, .md, .log), documents (.pdf, .doc, .docx, .rtf, .odt), and images (.jpg, .png, .gif, .webp, .svg, .bmp). These can be embedded as base64 in `message.attachments[].content` when within size limits. **Non-inlineable types**: archives (ZIP, RAR, 7z), executables, video, audio, and other binaries. These are **never** embedded inline, regardless of size. On paid plans a 40KB `.zip` goes to S3 exactly like a 5MB one — small does not mean inline. Attachments are capped at **20 per email**; beyond that they are silently truncated. ### Free Plan — no S3 offloading at all | Attachment type | Size | Result | |---|---|---| | Inline-appropriate (text, docs, images) | ≤ 128KB | Embedded as base64 in payload | | Inline-appropriate | > 128KB | **REJECTED** — excluded from the payload, not offloaded | | Non-inlineable (ZIP, video, exe, etc.) | Any | **REJECTED** — excluded from the payload, not offloaded | **Free has no storage tier.** There is nowhere for an oversized file to go, so anything over 128KB is dropped from the payload. It is not uploaded to S3 and no `downloadUrl` is produced. Excluded attachments still appear in the payload with `excluded: true` and a descriptive `excludeReason`. The email body is always delivered. System-domain aliases (`…@user.emailconnect.eu`) behave the same way on **every** plan: no S3, inline only, with `excludeReason: "system-domain-no-storage"` for anything that would otherwise be offloaded. ### Paid Plans — Inline Mode (Default) When `attachmentHandling` is `"inline"` (the default), two thresholds apply: 1. **Max attachment size** (Settings → Storage slider, default 1MB, configurable 0.1–10MB): attachments exceeding this are rejected. 2. **Inline/S3 boundary** (always 2MB): inline-appropriate files ≤ 2MB are base64-encoded; larger accepted files go to S3. | Attachment type | Size vs max attachment | Size vs 2MB | Result | |---|---|---|---| | Inline-appropriate | ≤ limit | ≤ 2MB | Embedded as base64 in payload | | Inline-appropriate | ≤ limit | > 2MB | Uploaded to S3, download URL in payload | | Non-inlineable (ZIP, video, etc.) | ≤ limit | Any | Uploaded to S3, download URL in payload | | Any type | > limit | — | Excluded with reason | Custom file type rules (block/allow lists) are applied in both inline and S3 paths. Attachments blocked by rules appear with `excluded: true`. ### Paid Plans — Storage Mode When `attachmentHandling` is set to `"storage"` on an alias, **all** attachments go directly to S3. The max attachment size slider still applies — attachments exceeding the limit are rejected. Custom file type rules (block/allow lists) are also applied. ### Managed S3 Storage (Maker+) Attachments that can't be inlined are offloaded to EmailConnect's Scaleway S3 bucket (nl-ams region, Netherlands). A download URL appears in `message.attachments[].downloadUrl`. Per-alias configuration (`attachmentHandling: 'storage'`) can force all attachments to S3 regardless of type or size. **`downloadUrl` is not a presigned S3 URL.** It points at EmailConnect (`https://app.emailconnect.eu/attachments/{fileId}/download`); fetching it 302-redirects to a short-lived signed S3 URL minted at fetch time. Downloads therefore route through us, so they can be counted, expired, and kept in-region. Its real lifetime is **your retention window** — the record is purged along with the email, after which the URL returns 404. Do not cache it as if it had a fixed TTL, and do not expect an `s3.…` or `static.emailconnect.eu` hostname in the payload. ### Custom S3 Storage (Business+) Upload attachments directly to your own S3-compatible bucket (AWS S3, Scaleway, Hetzner, MinIO, etc.). Full control over retention, access, and lifecycle. ### Sync vs Async S3 Upload When attachments go to S3, the upload mode depends on file size: - **Sync** (≤ 2MB): The S3 upload completes during email processing. The download URL is immediately usable. Payload fields: `status: "completed"`, `uploadType: "sync"`. - **Async** (> 2MB): The upload is queued for background processing. The download URL initially returns **HTTP 202** with a JSON body containing `retryAfter` (seconds) and `estimatedCompletion` (ISO timestamp). Payload fields: `status: "pending"`, `uploadType: "async"`. **`message.attachments[].status` values:** `included` (inline in this payload), `completed` (uploaded, `downloadUrl` ready), `pending` (still uploading — retry shortly), `failed`, `rejected`, `unknown`. **Consuming async attachments:** 1. Check `message.attachments[].status` — if `completed`, the URL works immediately 2. If `pending`, poll the download URL (`GET /attachments/:fileId/download`): returns **202 Accepted** with `retryAfter: 5` until ready, then redirects to the file when completed 3. A lighter polling option: `GET /attachments/:fileId/status` returns metadata without triggering a download 4. **410 Gone** means the upload failed, or the file's own expiry passed while the record still exists 5. **404 Not Found** means the file is gone for good — this is what a **retention-purged** attachment returns. When your retention window closes, the record is deleted, so the endpoint 404s rather than 410s. Fetch attachments inside your retention window; don't treat a `downloadUrl` as durable There is no webhook callback when an async upload completes — consumers must poll. ### Attachment Security - **Virus scanning is live** on **Business plan and above**, powered by **ClamAV**. It also requires the virus-scanning setting to be enabled on the account. Results appear in the payload as `security.virusScan` (a per-email summary: `scanned`, `engine`, `attachmentsScanned`, `attachmentsSkipped`, `threatsFound`) and `message.attachments[].virusScan` (per file: `status` of `clean` / `infected` / `skipped` / `error`, plus `threat` and `duration`). Infected attachments are **excluded from delivery** with `excludeReason: "virus-detected"` and carry no `downloadUrl`. Free and Maker plans are not scanned - File type rules at account level: block dangerous types (.exe, .bat, .ps1) or allow-list specific types (.pdf, .doc, .xlsx) --- ## Alias Rules Rules operate at two levels: ### Account-level (per S3 bucket) - **Attachment file type rules**: Block or allow-list attachment types (e.g., reject .exe, only accept .pdf/.xlsx) ### Alias-level Each alias can have rules that control which emails are accepted and processed: - **Sender address filtering**: Accept or reject by specific sender email address - **Sender domain filtering**: Accept or reject by sender domain - **Attachment presence rules**: Reject emails based on missing or mere presence of attachments - **Sender notifications**: Optionally notify blocked senders --- ## Webhook Signing (Standard Webhooks) Available on Maker plan and above. EmailConnect signs every webhook payload following the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) convention. **Headers sent with signed webhooks:** - `webhook-id`: Unique message identifier - `webhook-timestamp`: Unix timestamp (seconds) when the request was signed - `webhook-signature`: HMAC-SHA256 signature in format `v1,` **Verification steps:** 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers 2. Construct signed content: `{webhook-id}.{webhook-timestamp}.{body}` 3. Base64-decode your secret (after removing the `whsec_` prefix) 4. Compute HMAC-SHA256 of the signed content, then base64-encode the result 5. Compare with the signature value after the `v1,` prefix (use constant-time comparison) 6. Reject if timestamp is older than 5 minutes (replay protection) --- ## Custom Webhook Headers Add custom headers to every webhook request for an alias. Common uses: - `Authorization: Bearer ` for endpoint authentication - Custom identifiers for routing in your application - API keys for third-party services receiving the webhook --- ## Per-Alias Webhook Routing Each alias has its own webhook endpoint: ``` support@yourdomain.com → https://api.app.com/tickets invoices@yourdomain.com → https://api.app.com/accounting leads@yourdomain.com → https://api.app.com/crm *@yourdomain.com → https://api.app.com/catch-all ``` This enables building microservice-style architectures where each email type routes to a different processing endpoint. --- ## API The EmailConnect API allows programmatic management of domains, aliases, and webhooks. **Base URL:** `https://app.emailconnect.eu` **Authentication:** API key via `X-API-KEY` header. Keys are created in account settings with configurable scopes. ### API Key Scopes - **Full Access** (`["*"]`): Complete access to all endpoints - **Read Only** (`["domains:read", "domains:status", "aliases:read", "webhooks:read"]`): Read-only access - **API User** (`["domains:read", "domains:status", "domains:config", "aliases:*", "webhooks:*"]`): Limited domain access with full alias and webhook management ### Rate Limits Per hour, per account: - Free: **60** requests/hour - Maker: **600** requests/hour - Business: **3,000** requests/hour - Platform: **unlimited** ### Key Endpoints **Aliases and webhooks are flat, top-level resources.** They are not nested under domains, and they are addressed by their own ID. There is no `/api/domains/:domainId/aliases/:id` and no `/api/aliases/:aliasId/webhooks`. Writes use `PUT`, not `PATCH`. **Domains:** - `GET /api/domains` — List all domains - `POST /api/domains` — Add a new domain - `GET /api/domains/:domainId` — Get domain details - `PUT /api/domains/:domainId` — Update a domain - `GET /api/domains/:domainId/status` — Check domain DNS verification status - `POST /api/domains/:domainId/verify` — Trigger DNS verification - `PUT /api/domains/:domainId/webhook` — Set the domain-level (catch-all) webhook - `DELETE /api/domains/:domainId` — Remove a domain - `GET /api/system-domains` — List available system domains **Aliases** (flat): - `GET /api/aliases` — List all aliases for the authenticated user - `POST /api/aliases` — Create a new alias (the domain is given in the body, not the path) - `GET /api/aliases/:aliasId` — Get alias details - `PUT /api/aliases/:aliasId` — Update alias settings - `DELETE /api/aliases/:aliasId` — Delete an alias - `PUT /api/aliases/:aliasId/webhook` — Attach a webhook to the alias - `GET | PUT | DELETE /api/aliases/:aliasId/rules` — Alias acceptance rules (paid plans) - `GET /api/domains/:domainId/aliases` — Convenience read: list the aliases on one domain. This is the *only* domain-nested alias route, and it is read-only **Webhooks** (flat): - `GET /api/webhooks` — List webhooks - `POST /api/webhooks` — Create a webhook - `GET /api/webhooks/:webhookId` — Get webhook details - `PUT /api/webhooks/:webhookId` — Update a webhook - `DELETE /api/webhooks/:webhookId` — Delete a webhook - `POST /api/webhooks/:webhookId/verify` — Start endpoint verification - `POST /api/webhooks/:webhookId/test` — Send a test payload A webhook is created independently and then bound to an alias (`PUT /api/aliases/:aliasId/webhook`) or to a domain (`PUT /api/domains/:domainId/webhook`), so one webhook can serve several aliases. The full OpenAPI 3.0 specification is available at: https://emailconnect.eu/openapi.json --- ## Domain Setup To use custom domains with EmailConnect: 1. **Add the domain** in the EmailConnect dashboard 2. **Add MX records** pointing to EmailConnect's mail servers 3. **Add a TXT record** for domain ownership verification 4. **Wait for propagation** (typically 15 minutes to 48 hours) 5. **Verify** in the dashboard — EmailConnect checks DNS automatically Once verified, create aliases on your domain and configure webhooks for each. --- ## Spam Filtering EmailConnect uses rspamd for spam analysis. Every email receives: - **Spam score**: rspamd composite score (negative = cleaner, higher = more suspicious). Example: -0.99 for a well-authenticated email - **Symbols**: Array of rspamd rules that matched, each with `name`, `weight`, and `description` (e.g., `DMARC_POLICY_ALLOW` at -0.5, `R_DKIM_ALLOW` at -0.2) - **Authentication results**: DKIM, SPF, and DMARC pass/fail status - **Report**: Full rspamd text report for debugging **There is no `isSpam` field, no `threshold`, and no `action`.** We report the score and you judge it. A spam verdict is a policy decision that depends on your tolerance, not ours, and a boolean derived from a cut-off you chose tells you nothing the score didn't. Compare `spam.score` against your own threshold, or express it declaratively with alias rules and let EmailConnect accept, block, or tag the mail before it reaches your endpoint. You control the cut-off. EmailConnect includes all scoring data in the payload so your automation can decide what to do with borderline emails. Known spam sources are blocked at the SMTP level before processing. --- ## Data Retention **Retention is set per user (account-wide) and per storage connection. It is never configured per alias.** An alias has no retention setting — do not look for one, and do not tell an agent to set one. - **Free / Maker**: fixed at **1 hour** (the default equals the maximum — not configurable) - **Business**: configurable, **up to 30 days** (720 hours); defaults to 24 hours - **Platform**: configurable, **up to 365 days** (8,760 hours); defaults to 24 hours - **After retention**: the stored email — including the body and any attachment records — is permanently deleted. Attachment `downloadUrl`s then return 404 - **Business+ can delete on demand**: `DELETE /api/v1/emails/{messageId}` (needs an API key with the `emails:delete` scope) removes a stored email immediately after your webhook succeeds, shrinking the window from hours to seconds - **Webhook logs**: delivery status, response codes, and timing retained for the retention period --- ## Plans and Pricing ### Free Plan — EUR 0/month - 100 emails/month - **3 aliases, 1 custom domain, 3 webhooks** - No S3 storage: attachments are inline-only, up to 128KB per file - Full classification (`type`, `confidence`, `signals`), link extraction (`url` + `domain`) - 1-hour data retention (fixed) - 60 API requests/hour - Community support ### Maker Plan — EUR 12.95/month or EUR 129.50/year - 1,500 emails/month (overage: EUR 5.00/1,000) - 5 aliases, 1 custom domain, 5 webhooks - 1 storage connection - Markdown conversion (per-alias opt-in), integrity hashes, link `anchorText`/`source`, attachment metadata - Spam analysis with DKIM/SPF/DMARC results - Standard Webhooks signing (HMAC-SHA256) - Custom webhook headers - S3 attachment offloading (EmailConnect managed) - 1-hour data retention (fixed) - 600 API requests/hour - Email support ### Business Plan — EUR 49.00/month or EUR 490.00/year - 10,000 emails/month (overage: EUR 4.00/1,000) - 25 aliases, 3 custom domains, 25 webhooks - **Virus scanning (ClamAV)** - Custom payload template (mustache `{{path}}` reshaping) - 3 storage connections (own bucket support) - Advanced alias routing rules - Configurable retention **up to 30 days**, plus the delete-email API - Analytics dashboard - 3,000 API requests/hour - Priority email support ### Platform Plan — Custom pricing - 100,000 emails/month - **100 aliases, 100 webhooks, unlimited custom domains** - Configurable retention **up to 365 days** - Data residency mode (email content is never written to disk — metadata-only stub) - Role-based access control (RBAC) - Immutable audit logging - Unlimited storage connections - Unlimited API rate - Dedicated support - Signed Data Processing Agreements (DPAs) - Custom overage rates and volume pricing --- ## EU Compliance and Privacy ### Infrastructure EmailConnect operates entirely within the European Union: - **SMTP and email processing**: Hetzner, Nuremberg, Germany - **Application hosting**: Hetzner, Nuremberg, Germany - **Managed S3 storage**: Scaleway, nl-ams region (Netherlands) - **Custom S3 storage**: Bring your own S3-compatible bucket (AWS, Scaleway, Hetzner, MinIO, etc.) - **Transactional email**: Scaleway, France - **Company**: XADI, Leiden, Netherlands (incorporated in Amsterdam) No data is processed, stored, or routed through US infrastructure at any point. ### GDPR Compliance - **Lawful basis**: Data processing for legitimate interest (contract fulfillment) - **Data minimization**: Only processes emails explicitly forwarded — no mailbox access - **Purpose limitation**: Parses and delivers; does not profile, analyze, or sell data - **Retention control**: 1 hour on Free and Maker; configurable up to 30 days on Business and up to 365 days on Platform. Set per user and per storage connection (never per alias), auto-deleted after - **Right to erasure**: Full support for data deletion requests - **Audit logging**: Immutable timestamps for compliance evidence (Platform plan) - **DPA availability**: Signed Data Processing Agreements for Platform customers ### No US Jurisdiction Exposure - No CLOUD Act applicability - No FISA or Patriot Act jurisdiction - No US sub-processors - All infrastructure providers are EU-headquartered (Scaleway: France, Hetzner: Germany) ### Selective Email Access Model Unlike services that require OAuth tokens with full mailbox read access, EmailConnect only processes emails explicitly forwarded to its aliases. It never has access to your mailbox, contacts, or email history. Security teams approve this approach because the attack surface is minimal — only the emails you choose to send are processed. --- ## Use Cases ### Invoice Processing Automation Forward invoices to `invoices@yourdomain.com`. EmailConnect delivers the JSON payload with sender info, subject, body content, and PDF attachments (base64 or S3 URL) to your accounting webhook. Your automation extracts amounts, dates, and vendor details. ### Support Ticket Automation Route `support@yourdomain.com` to your ticket system's webhook. Each email becomes a structured payload with customer info, subject, body, and attachments. No helpdesk platform required — just your webhook and processing logic. ### DMARC Report Monitoring Point DMARC aggregate report emails to an alias. EmailConnect delivers the XML attachments to your webhook for parsing and dashboarding. Automated monitoring without manual email checking. ### Gmail to Webhook Set up Gmail forwarding to an EmailConnect alias. Every email forwarded becomes a webhook payload — no Google API credentials, no OAuth, no API quota management. Works with any Gmail account. ### Enterprise Calendar Integration Forward calendar invites to an alias. EmailConnect delivers the ICS attachment and email body as JSON. Your webhook parses the ICS data for automated calendar updates, room booking, or scheduling workflows. ### Financial Document Automation For regulated industries: forward compliance documents to dedicated aliases. Integrity hashes verify document authenticity. Audit logs provide compliance evidence. EU-only processing satisfies data residency requirements. ### Customer Feedback Processing Route feedback emails to a webhook for structured collection. Classification identifies feedback type. Links and attachments are extracted. Build feedback dashboards without manual processing. ### Sales Lead Qualification Forward inbound sales emails to a webhook. Extract sender info, company details from signatures, and route to your CRM. Classify as high/low priority based on content signals. --- ## Security ### Responsible Disclosure EmailConnect has a responsible vulnerability disclosure policy. Security researchers can report vulnerabilities to security@emailconnect.eu. **In scope:** - emailconnect.eu and app.emailconnect.eu - API endpoints - Email processing pipeline - Authentication and authorization **Out of scope:** - Social engineering - Physical attacks - Denial of service - Third-party services ### Encryption - **In transit**: TLS 1.3 for all connections - **At rest**: credentials and secrets — S3 access keys, webhook signing secrets, custom webhook headers — are encrypted with AES-256-GCM - **Webhooks**: Standard Webhooks signing (HMAC-SHA256) **Email content is not encrypted at rest.** We do not claim otherwise, because the answer we prefer is a better one: minimise what exists and for how long, rather than encrypt a large, long-lived store. - On **Free and Maker**, the retention window is **one hour** — a fixed maximum, not a default. An email that arrives at 09:00 is gone by 10:00. - On **Business and Platform**, you set the window yourself, and you can call `DELETE /api/v1/emails/{messageId}` the moment your webhook has succeeded — shrinking exposure from hours to **seconds**, under your control rather than ours. - On **Platform**, [data residency mode](https://emailconnect.eu/help/data-residency-mode/) is the complete answer: the message body and attachments are **never written to our database at all**. We hold routing metadata only, and the email passes straight through to your endpoint. If encryption-at-rest for email content is a hard requirement for you, data residency mode is the configuration to ask about. ### Access Control - Principle of least privilege - API key scopes for granular access - RBAC for Platform plan teams - Immutable audit logs for accountability --- ## Integration Examples ### Receiving a Webhook (Node.js) ```javascript const express = require('express'); const crypto = require('crypto'); const app = express(); app.use('/webhook', express.json({ verify: (req, res, buf) => { req.rawBody = buf.toString(); } })); app.post('/webhook', (req, res) => { // Verify Standard Webhooks signature const msgId = req.headers['webhook-id']; const timestamp = req.headers['webhook-timestamp']; const signatures = req.headers['webhook-signature']; const signedContent = `${msgId}.${timestamp}.${req.rawBody}`; const secret = process.env.WEBHOOK_SECRET; const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const expected = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64'); const valid = signatures.split(' ').some(sig => { const [v, val] = sig.split(','); return v === 'v1' && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(val)); }); if (!valid) return res.status(401).send('Invalid signature'); const { message } = req.body; console.log(`Email from ${message.sender.email}: ${message.subject}`); res.status(200).send('OK'); }); ``` ### Receiving a Webhook (Python) ```python import hmac, hashlib, base64 from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/webhook', methods=['POST']) def handle_webhook(): # Verify Standard Webhooks signature msg_id = request.headers.get('webhook-id') timestamp = request.headers.get('webhook-timestamp') signatures = request.headers.get('webhook-signature') raw_body = request.get_data(as_text=True) signed_content = f'{msg_id}.{timestamp}.{raw_body}' secret_bytes = base64.b64decode(WEBHOOK_SECRET.removeprefix('whsec_')) expected = base64.b64encode( hmac.new(secret_bytes, signed_content.encode(), hashlib.sha256).digest() ).decode() valid = any( v == 'v1' and hmac.compare_digest(expected, val) for sig in signatures.split(' ') for v, _, val in [sig.partition(',')] ) if not valid: return jsonify({'error': 'Invalid signature'}), 401 data = request.get_json() print(f"Email from {data['message']['sender']['email']}: {data['message']['subject']}") return jsonify({'status': 'ok'}), 200 ``` --- ## Vision and Philosophy **"We deliver your data. You decide what happens next."** EmailConnect is an infrastructure layer, not an interpretation layer. It provides: - Clean, structured email data via webhooks - Full authentication verification (DKIM, SPF, DMARC) - Integrity hashes for tamper detection - Composable primitives for building your own automation It deliberately does not provide: - AI-based email interpretation or summarization - Template parsing or OCR - Outbound email sending - Built-in workflow automation This makes EmailConnect composable with any automation platform (n8n, Make, Zapier) or custom code, without lock-in or opinionated processing. --- ## Company **XADI** — Leiden, Netherlands (incorporated in Amsterdam) Founded by Xander, with 15+ years of experience in consulting and innovation (ex-Capgemini, Bay Area). ### Contact - General: hello@emailconnect.eu - Support: support@emailconnect.eu - Security: security@emailconnect.eu - Privacy: privacy@emailconnect.eu - Enterprise: enterprise@emailconnect.eu - DPO: dpo@emailconnect.eu ### Links - Website: https://emailconnect.eu - Application: https://app.emailconnect.eu - Help Center: https://emailconnect.eu/help/ - Changelog: https://emailconnect.eu/changelog/ - Pricing: https://emailconnect.eu/pricing/ - API Docs (OpenAPI): https://emailconnect.eu/openapi.json - Privacy Policy: https://emailconnect.eu/privacy-policy/ - Terms of Service: https://emailconnect.eu/terms-of-service/