How to build a freight email automator with bem
Integrate into your freight product the most advanced quote and document freight email automator -- fully customizable, adaptive, and learning continuously
Fully customizable quoting and invoice extraction, routed and structured in real-time
Freight brokers and 3PLs live in their inboxes. Every quote request, invoice, or rate confirmation arrives via email — often attached as a PDF, Excel, or embedded in the body. Most teams handle this manually. A rep reads the email, copies the data, logs it in a TMS, and maybe replies.
It’s slow, error-prone, and doesn’t scale.
You don’t need a pre-packaged product that tries to guess your workflow.
You need infrastructure that lets you build your own system — exactly how your team works.
With bem, you can build a freight inbox automator that:
Accepts any email, PDF, XLSX, or attachment
Classifies it as a quote request or invoice (without regex or manual rules)
Extracts structured data into your schema
Notifies the right person or system
Learns from corrections automatically
Let’s walk through how to build this system — with zero custom model training, just APIs and real infrastructure.
What You’re Building
We'll set up:
A quote transformer for requests
An invoice transformer for billing docs
A router that detects the right doc type
Optional notifications or downstream syncs
A feedback loop to auto-learn from fixes
Every piece is configurable, modular, and upgradeable — no vendor lock-in, no “feature request” delays. You own it.
Step 1: Define Your Schemas
Before you automate anything, define the output you want. Here are two examples.
Quote Request Schema
{
"type": "object",
"required": ["origin", "destination", "pickupDate", "equipmentType", "commodity", "weight", "contact"],
"properties": {
"origin": { "type": "string", "description": "Pickup location — city, state, or ZIP" },
"destination": { "type": "string", "description": "Dropoff location — city, state, or ZIP" },
"pickupDate": { "type": "string", "format": "date", "description": "Requested pickup date" },
"equipmentType": { "type": "string", "description": "Requested trailer type (e.g. dry van, reefer)" },
"commodity": { "type": "string", "description": "Description of freight" },
"weight": { "type": "number", "description": "Shipment weight in lbs" },
"contact": {
"type": "object",
"required": ["name", "email"],
"properties": {
"name": { "type": "string", "description": "Name of the requester" },
"email": { "type": "string", "format": "email", "description": "Email of the requester" },
"phone": { "type": "string", "description": "Optional phone number" }
}
}
}
}
Freight Invoice Schema
{
"type": "object",
"required": ["invoiceNumber", "loadReference", "carrier", "lineItems", "totalAmount", "dueDate"],
"properties": {
"invoiceNumber": { "type": "string", "description": "Invoice identifier" },
"loadReference": { "type": "string", "description": "Internal reference or load ID" },
"carrier": {
"type": "object",
"required": ["name", "email"],
"properties": {
"name": { "type": "string", "description": "Carrier company name" },
"email": { "type": "string", "format": "email", "description": "Billing contact" }
}
},
"lineItems": {
"type": "array",
"description": "List of charges",
"items": {
"type": "object",
"required": ["description", "amount"],
"properties": {
"description": { "type": "string", "description": "Charge type (e.g. linehaul, fuel)" },
"amount": { "type": "number", "description": "Charge amount in USD" }
}
}
},
"totalAmount": { "type": "number", "description": "Total billed amount" },
"dueDate": { "type": "string", "format": "date", "description": "Payment due date" }
}
}
⚙️ Step 2: Create Your Transform Configs
Each schema becomes a transform config in bem. This gives you:
A dedicated transformer for each doc type
A unique email to forward files to
An ID you can use in routing or downstream flows
Create the Quote Transformer
Endpoint: POST /v1-alpha/action-type-configs
{
"actionType": "transform",
"name": "Freight Quote Transformer",
"outputSchemaName": "QuoteRequest",
"outputSchema": { /* schema from above */ },
"independentDocumentProcessingEnabled": true,
"complexTabularTransformEnabled": true
}
Create the Invoice Transformer
Endpoint: POST /v1-alpha/action-type-configs
{
"actionType": "transform",
"name": "Freight Invoice Transformer",
"outputSchemaName": "FreightInvoice",
"outputSchema": { /* schema from above */ },
"independentDocumentProcessingEnabled": true,
"complexTabularTransformEnabled": true
}
You’ll get back two actionTypeConfigID
s and two email addresses — one for each transformer.
🧭 Step 3: Route Automatically (No Regex Needed)
To avoid manually deciding where each doc should go, create a router that uses LLM logic to determine the right destination.
Create the Router
Endpoint: POST /v1-alpha/action-type-configs
{
"actionType": "route",
"name": "Freight Intake Router",
"description": "Detects whether inbound is a quote or invoice",
"routes": [
{
"name": "Quotes",
"actionTypeConfigID": "<quote_transform_config_id>"
},
{
"name": "Invoices",
"actionTypeConfigID": "<invoice_transform_config_id>"
}
]
}
No regex. No hand-tuned rules. bem figures out where the doc should go.
You get a single inbox like:
eml_xyz@actions.bem.ai
🛠️ Step 4: (Optional) Trigger Programmatically
Instead of email, you can also use the API directly:
Transform an uploaded file
Endpoint: POST /v1-beta/transformations
{
"pipelineID": "<quote_or_invoice_config_id>",
"transformations": [
{
"referenceID": "req_001",
"inputType": "pdf",
"inputContent": "<base64 file>"
}
]
}
Or send to the router
Endpoint: POST /v1-alpha/actions
{
"actionType": "route",
"actionTypeConfigID": "<router_config_id>",
"actions": [
{
"referenceID": "email_123",
"inputType": "email",
"inputContent": "<base64 raw email>"
}
]
}
📣 Step 5: Notify or Integrate
Once a document is transformed, you can:
Send it to a Slack channel
Notify a rep by email
Push to your TMS
Insert into a database
bem gives you all the structured output, and you route it however you want.
This is the power of building it yourself: you own the logic, flow, and UX.
🧠 Step 6: Learn From Corrections (PATCH)
No extractor gets it perfect out of the gate. But with bem, you don’t retrain a model — you just correct and patch.
Let’s say a rep fixes a pickup date:
Submit a correction
Endpoint: PATCH /v1-beta/transformations
{
"transformations": [
{
"transformationID": "tr_abc123",
"correctedJSON": {
"pickupDate": "2025-04-23"
}
}
]
}
That correction gets remembered and used next time.
You’re not building static automation. You’re building infrastructure that learns.
🔄 Why This Matters
Most “AI quoting tools” or invoice parsers give you what they think logistics should look like. But freight is messy. Your formats, fields, contacts, exceptions — they’re unique.
With bem, you don’t need to change how you work. You make the tool work the way you do.
Want to add PO numbers? Update the schema.
Want to route invoices differently after 5pm? Add a rule.
Want to auto-notify reps when BOL is missing? Patch it in.
Invisible automation, fully owned by you. That’s what scales.
🎓 Extra Credit: Add Voice-Native Workflows with Whisper + Cartesia
Want to make your quoting automation feel even more magical? Layer in voice input and output — while reusing the exact same bem
pipelines.
Here’s how to do it:
1. Transcribe Audio with Whisper
Use OpenAI’s Whisper API to turn voice memos, phone recordings, or customer voicemails into structured text.
from openai import OpenAI
client = OpenAI()
audio_file = open("/path/to/audio.mp3", "rb")
transcription = client.audio.transcriptions.create(
model="gpt-4o-transcribe",
file=audio_file
)
structured_text = transcription.text
Once you’ve got text, just forward it to your existing quote or invoice pipeline in bem — no changes needed. Your schema does the rest.
2. Generate Speech with Cartesia
Want your system to respond conversationally? Use Cartesia’s WebSocket TTS API to turn structured bem outputs into dynamic, natural speech.
Example message over WebSocket:
{
"model_id": "sonic-2",
"transcript": "Your quote from Houston to Austin has been generated at $650.",
"voice": {
"mode": "id",
"id": "a0e99841-438c-4a64-b679-ae501e7d6091"
},
"language": "en",
"context_id": "quote-session-123",
"output_format": {
"container": "raw",
"encoding": "pcm_s16le",
"sample_rate": 8000
},
"add_timestamps": true,
"continue": false
}
Cartesia supports streaming responses, prosody-aware continuation, and high concurrency over a persistent WebSocket — perfect for realtime agents.
3. Close the Loop with bem PATCH
And when users correct something in the voice flow (e.g. “No, I meant Dry Van, not Reefer”), PATCH the corrected output to bem:
PATCH /v1-beta/transformations
PATCH /v1-beta/transformations
{
"transformations": [
{
"transformationID": "tr_xxxx",
"correctedJSON": {
"equipmentType": "Dry Van"
}
}
]
}
You’ve now built a self-healing voice quoting loop — one that learns with every correction.
Why this matters
Email workflows are table stakes.
Voice is next.
And with bem
at the center, you don’t need to rebuild anything — just extend what you’ve already built.
This is what infrastructure should feel like:
Composable. Omnichannel. Quietly brilliant.
Let’s Build It
In a few API calls, you just created:
✅ Structured extraction for quotes and invoices
✅ LLM-powered classification without regex
✅ Email-to-JSON pipelines with patchable learning
✅ Full extensibility for notifications and integrations
Want to see this running live?
Drop in a sample doc. We’ll help you build the full flow — your way.
This is what freight ops looks like when you own the system.
Not a product. Not a dashboard. Real infrastructure.