odysseus-dev/odysseus · error · HTTPException
Invalid send payload: {exc}
Error message
Invalid send payload: {exc} What it means
Raised by the Codex email proxy POST /api/codex/emails/send when the raw JSON body fails to construct routes.email_routes.SendEmailRequest. That Pydantic model requires the fields to, subject, and body (cc, bcc, body_html, attachments, account_id, in_reply_to, references, source_uid, source_folder are optional), so any missing required field, wrong type, or extra/misspelled key surfaces as ValidationError and is re-raised as HTTP 400 with the Pydantic message embedded.
Source
Thrown at routes/codex_routes.py:386
from routes.email_routes import SendEmailRequest
try:
req = SendEmailRequest(**body)
except Exception as exc:
raise HTTPException(400, f"Invalid draft payload: {exc}")
return await email_draft_endpoint(req=req, owner=owner)
@router.post("/emails/send")
async def codex_email_send(request: Request, body: dict[str, Any] = Body(default_factory=dict)):
owner = _scope_owner(request, EMAIL_SEND_SCOPES)
if email_send_endpoint is None:
raise HTTPException(503, "Email integration is not available")
from routes.email_routes import SendEmailRequest
try:
req = SendEmailRequest(**body)
except Exception as exc:
raise HTTPException(400, f"Invalid send payload: {exc}")
return await email_send_endpoint(req=req, background_tasks=BackgroundTasks(), owner=owner)
# ── Memory ────────────────────────────────────────────────────────────
@router.get("/memory")
async def codex_memory_list(request: Request):
owner = _scope_owner(request, MEMORY_READ_SCOPES)
if memory_list_endpoint is None:
raise HTTPException(503, "Memory integration is not available")
return await _as_owner(request, owner, memory_list_endpoint, request)
@router.post("/memory")
async def codex_memory_add(request: Request, body: dict[str, Any] = Body(default_factory=dict)):
owner = _scope_owner(request, MEMORY_WRITE_SCOPES)
if memory_add_endpoint is None:
raise HTTPException(503, "Memory integration is not available")
from src.request_models import MemoryAddRequest
View on GitHub (pinned to f9235ebbf1)
Solutions
- Include all required fields: to (string), subject (string), body (plain-text string); optional fields are cc, bcc, body_html, in_reply_to, references, attachments, account_id, source_uid, source_folder.
- Read the ValidationError text in the 400 response body — it names the exact offending field and reason.
- Keep 'to' as a single comma/semicolon-separated string, matching the model's str type, not a JSON array.
- Pre-validate locally with the same model: from routes.email_routes import SendEmailRequest; SendEmailRequest(**payload).
Example fix
// before
fetch('/api/codex/emails/send', {method:'POST', body: JSON.stringify({recipient: 'a@b.c', subject: 'hi', body: 'hello'})})
// after
fetch('/api/codex/emails/send', {method:'POST', body: JSON.stringify({to: 'a@b.c', subject: 'hi', body: 'hello'})}) Defensive patterns
Strategy: validation
Validate before calling
from routes.email_routes import SendEmailRequest
def valid_send_payload(body: dict) -> bool:
try:
SendEmailRequest(**body)
return True
except Exception:
return False
# client-side (no imports): required string fields
# ok = all(isinstance(body.get(k), str) and body[k] for k in ('to', 'subject', 'body')) Type guard
def is_send_payload(b: dict) -> bool:
return (
isinstance(b, dict)
and isinstance(b.get('to'), str) and b['to'].strip()
and isinstance(b.get('subject'), str)
and isinstance(b.get('body'), str)
) Try / catch
resp = requests.post(url, json=payload)
if resp.status_code == 400 and 'Invalid send payload' in resp.text:
# parse the embedded pydantic message, fix fields, do NOT retry unchanged
show_validation_error(resp.json()['detail']) Prevention
- Build send payloads through one shared helper that always sets to/subject/body.
- Keep 'to' a single string, not a list.
- Add a pre-flight SendEmailRequest(**payload) check in agent code that composes mail.
When it happens
Trigger: POST /api/codex/emails/send with a body missing 'to', 'subject', or 'body'; passing 'recipient' instead of 'to'; sending 'to' as a list when the model declares str; or any nested object the model cannot coerce.
Common situations: An LLM agent or script composing the payload by hand guesses field names; a UI refactor renames compose fields; sending HTML-only mail and forgetting the plain-text 'body' fallback; copying a payload from a different mail API (e.g. 'html' instead of 'body_html').
Related errors
- Invalid draft payload: {exc}
- Cannot create reply: sender address is missing from this ema
- Email account not found for selector {account!r}. Available
- IMAP folder not found: {folder}
- Invalid remote_host — must be host or user@host, no SSH opti
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/3054707c03008d40.
Report an issue: GitHub.