Skip to main content

Email

An Email is a single inbound message received through AWS SES, recorded in Postgres and tied to one inbox. The original .eml is preserved on S3 and is reachable through the API.

Purpose​

  • Persist every message that enters the platform, including those rejected by policy.
  • Carry the processing state (received → approved | rejected → processing → processed | failed).
  • Provide the joining row for agent runs, system events and webhook deliveries.

Important: no direct writes​

Emails are never modified via the public API except through the explicit reprocess action. They are written by the SES ingestion handler and the worker pipeline; the only way to create one through the API is POST /emails, which submits a raw .eml into that same pipeline (metadata.source is api). DELETE /emails/{id} soft-deletes.

Schema​

FieldTypeDescription
idUUIDInternal id.
workspace_idUUIDOwning workspace.
inbox_idUUIDInbox that matched.
agent_idUUID | nullAgent bound to the inbox at the time of processing.
message_idstringSES messageId. Unique — guarantees idempotency.
raw_s3_keystringKey of the original .eml on the raw bucket.
subjectstring | nullDecoded subject. Filled by the worker after parsing.
fromstringEnvelope sender.
tostring[]Recipients from the SES payload.
ccstring[]Filled by the worker.
bccstring[]Filled by the worker (rarely populated — Bcc is stripped in transit).
statusenumSee lifecycle.
rejection_reasonenum | nullSet when status=rejected.
spam_verdictstring | nullFrom SES (PASS, FAIL, GRAY, etc.).
virus_verdictstring | nullFrom SES.
aws_ses_metadataobjectRaw SPF/DKIM/DMARC + receipt info from SES.
custom_metadataobjectFree-form, set by clients.
agent_metadataobjectOutput written by the agent run (extracted fields, tags, etc.).
metadataobjectSystem metadata.
sent_atdatetime | nullFrom the message Date header.
received_atdatetimeWhen SES received it.
processed_atdatetime | nullWhen the agent finished.
created_at / updated_at / deleted_atdatetime

Status​

StatusMeaning
receivedIngestion handler wrote the row; worker hasn't run yet.
approvedWorker parsed the .eml, matched filters and allowed recipients. Queued for the agent.
rejectedWorker rejected the email. rejection_reason is set. Agent will not run.
processingAgent run started.
processedAgent finished successfully.
failedAgent run failed. Manual reprocess is allowed.

Rejection reasons​

ReasonMeaning
recipient_unknownNo matching Inbox.
recipientsallowed_recipients rejected the to address.
attachmentsAttachment count, per-file size or total size exceeded.
filtersAt least one filter rule failed.
encoding.eml could not be parsed.
spamSES spamVerdict=FAIL (and inbox policy treats FAIL as block).
virusSES virusVerdict=FAIL.
sender_rate_limitSender exceeded per-inbox cap.

Example​

{
"id": "e5f6a7b8-9c0d-4e1f-2a3b-4c5d6e7f8a9b",
"workspace_id": "a8b2c1d4-9e7f-4a3b-8c5d-1f2e3a4b5c6d",
"inbox_id": "b3c4d5e6-7a89-4b0c-9d1e-2f3a4b5c6d7e",
"agent_id": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"message_id": "01HZ9X8K4N0P2Q5R7S9T1V3W5X",
"raw_s3_key": "incoming/01HZ9X8K4N0P2Q5R7S9T1V3W5X.eml",
"subject": "Invoice 4123",
"from": "billing@acme.com",
"to": ["triage-3f4a@inbox.kobana.com.br"],
"cc": [],
"bcc": [],
"status": "processed",
"rejection_reason": null,
"spam_verdict": "PASS",
"virus_verdict": "PASS",
"aws_ses_metadata": {
"spfVerdict": { "status": "PASS" },
"dkimVerdict": { "status": "PASS" },
"dmarcVerdict": { "status": "PASS" }
},
"custom_metadata": {},
"agent_metadata": {
"type": "invoice",
"amount": 4123.5,
"due_date": "2026-06-01"
},
"metadata": {},
"sent_at": "2026-05-17T10:00:00.000Z",
"received_at": "2026-05-17T10:00:14.123Z",
"processed_at": "2026-05-17T10:00:18.927Z",
"created_at": "2026-05-17T10:00:14.123Z",
"updated_at": "2026-05-17T10:00:18.927Z",
"deleted_at": null
}

Downloading the .eml​

GET /v1/emails/{id}/eml returns a 302 redirect to a short-lived presigned S3 URL. The URL is valid for 15 minutes and serves the original message with full headers, MIME parts and attachments preserved.

Downloading the .eml from a webhook​

Every email.* webhook payload also carries a ready-to-use data.email.download_url — a link a webhook receiver can follow directly, with none of the credentials above:

https://app.inbox.kobana.com.br/eml/{id}/{token}
  • It is not part of /v1 and is not served on api.inbox.*: that host requires an mTLS client certificate on every request, which a webhook receiver has no way to present. This link lives on the app host instead (no mTLS).
  • {token} is a one-time-issued, short-lived (30 minutes) credential minted specifically for that delivery — it is not your API token and grants access to that one email only.
  • Following it redirects (302) straight to a presigned S3 URL valid for 5 minutes — the same underlying file GET /v1/emails/{id}/eml serves, just reached without mTLS/JWT.
  • After 30 minutes the link stops working; re-fetch the email (GET /v1/emails/{id}, mTLS+JWT) and download it the regular way instead.

Reprocessing​

POST /v1/emails/{id}/reprocess re-enqueues the email for the agent. Only emails with status=approved, processed or failed are eligible — emails still in received, processing or rejected return 422 unprocessable_entity.