In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
I was building a document-processing app where users could upload files from the UI, and everything worked well through a clean ingest pipeline. Then the practical request came in: users wanted to send invoices and documents directly by email, and have those attachments processed exactly like normal uploads. This guide shows the full implementation path for that flow in a Next.js + PayloadCMS app, using Brevo inbound parsing as the email entry point.
By the end, you will have a working architecture where:
users send attachments to a dedicated email address,
Brevo parses the email and calls your webhook,
your backend fetches the attachment binaries,
and your app pushes those files through the same ingestion and OCR pipeline as UI uploads.
The use case and the problem we are solving
Let’s define the exact use case first.
Your app already has a UI uploader in src/components/upload/FileUploadZone.tsx. That component sends files to POST /api/ingest. The ingest route creates:
an ingestion-job,
a document,
a document-file,
a pipeline-run,
and then queues OCR.
This is already solid. The issue is that email does not naturally arrive as multipart/form-data to your existing UI route. It comes from an external SMTP world, through mail infrastructure, with different trust and parsing requirements.
So the real problem is not “how do I parse an email?” The real problem is:
how to securely accept inbound email from a provider,
how to normalize email attachments into your internal file ingest model,
and how to guarantee email files follow the same business pipeline as UI files.
That third point is critical. If you build a second ingestion path with different behavior, you will create drift and hard-to-debug production inconsistencies.
The technical model that makes this possible
To receive email attachments in a web app, you need an inbound email provider. In this implementation we use Brevo Inbound Parse:
You configure a receiving subdomain like reply.yourdomain.com.
DNS MX records route incoming email to Brevo.
Brevo parses each incoming email and sends structured JSON to your webhook endpoint.
JSON includes attachment metadata and DownloadTokens.
Your app downloads attachment bytes with those tokens and feeds files to your ingest core.
The important architectural decision is this:
You do not call your current POST /api/ingest directly from Brevo.
Your current ingest route relies on an authenticated browser session. Brevo won’t have that session cookie. Instead, create a new webhook route and extract common ingest logic into a shared service used by both:
POST /api/ingest (UI path)
POST /api/webhooks/email-ingest (email path)
Final architecture before implementation
Here is the target shape:
text
User Email Client
-> SMTP
-> Brevo Inbound Parse
-> POST /api/webhooks/email-ingest
-> validate auth and tenant
-> fetch attachment bytes via DownloadToken
-> call shared ingest service
-> create ingestion-job, document, document-file, pipeline-run
-> upload to B2
-> queue publishOcr
And your UI flow remains:
text
FileUploadZone.tsx -> POST /api/ingest -> shared ingest service -> same pipeline
This means both channels converge into one ingest core.
Step 1: Configure Brevo inbound parsing
Start in Brevo and your DNS provider.
Set a dedicated receiving subdomain, for example reply.yourdomain.com, then configure MX records:
This service is the foundation. It gives you one place where all ingest side effects happen. From here on, every channel should call this function instead of duplicating logic.
Step 3: Make the existing /api/ingest route a thin wrapper
Then in your ingest core, when emailMeta exists, persist these values in the created ingestion job. That gives you deterministic dedupe and operational visibility.
Keep these separate from UI-upload limits. Email input is an external channel and needs explicit boundaries.
Step 8: Update Brevo webhook to your production endpoint
Once backend code is deployed:
Set webhook URL to https://api.yourapp.com/api/webhooks/email-ingest.
Confirm bearer auth header is configured in Brevo webhook security.
Send a real email to a tenant address like t-abc123@reply.yourdomain.com.
Confirm records are created in:
ingestion-jobs
documents
document-files
pipeline-runs
Confirm document status transitions to ocr_pending and OCR queue is populated.
At this point, you have true channel parity: email and UI both flow through one ingestion behavior.
Why this implementation works in production terms
This design works because it treats inbound email as an external integration boundary, not as another version of UI upload.
The webhook route is intentionally narrow:
authenticate webhook request,
parse provider payload,
resolve tenant identity,
enforce attachment constraints,
call shared ingest core.
Everything else remains in your existing pipeline engine.
That separation lets you evolve providers later without rewriting ingestion. If you move from Brevo to another provider, you mostly rewrite one adapter route, not your document pipeline.
Conclusion
We solved a practical product problem: users can now send documents by email and still get the same processing flow as standard uploads. The key to this implementation is not email parsing by itself, but architectural convergence through a shared ingest core used by both UI and webhook channels.
You now have a concrete path to implement inbound email attachments safely with tenant mapping, deduplication, and consistent pipeline behavior in a Next.js + Payload stack.
Let me know in the comments if you have questions, and subscribe for more practical development guides.