Skip to content
AI Inbx
Esc
navigateopen⌘Jpreview
On this page

Attachments

Download what arrived, or read the prepared Markdown so a model can use a PDF without you running a parser.

Inbound attachments come back two ways. There’s the original file, byte for byte, and there’s the prepared text — the same document extracted to Markdown or plain text so it can go straight into a prompt.

Sending attachments is covered in Sending; this page is about what arrives.

What’s on a message

Retrieve an email and each attachment carries its metadata plus two URLs:

const email = await aiinbx.emails.retrieve("eml_...")

for (const attachment of email.attachments) {
  console.log(attachment.filename, attachment.content_type, attachment.size)
  console.log(attachment.preparation?.status, attachment.preparation?.format)
}
PropType
idstring

att_…

Typestring
filenamestring

As the sender named it.

Typestring
content_typestring

The declared media type.

Typestring
sizenumber

Bytes.

Typenumber
cidstring | null

Content-ID when the attachment is inline in the HTML body.

Typestring | null
download_urlstring

Short-lived signed URL for the original file.

Typestring
preparationobject | null

Extraction result, or null when nothing was attempted.

Typeobject | null

Downloading the original

Both SDKs resolve the redirect for you:

const response = await aiinbx.attachments.download("att_...")
const bytes = new Uint8Array(await response.arrayBuffer())
response = client.attachments.download("att_...")
data = response.content
curl -L https://api.aiinbx.com/api/v2/attachments/att_... \
  -H "Authorization: Bearer $AI_INBX_API_KEY" \
  -o report.pdf

The endpoint answers 307 with a Location header pointing at a signed URL — hence -L on the curl. A HEAD on the same path returns the metadata in headers (Content-Type, Content-Length, Content-Disposition) without transferring the file, which is the cheap way to check a size before committing to a download.

Prepared text

Documents that can be read as text are extracted on arrival. preparation tells you how that went:

PropType
status"ready" | "partial" | "unsupported" | "failed"

Whether the extraction produced usable text.

Type"ready" | "partial" | "unsupported" | "failed"
format"markdown" | "text" | null

Markdown keeps document structure; text is a flat rendering.

Type"markdown" | "text" | null
pagesnumber | null

Page count, where the format has pages.

Typenumber | null
warningsstring[]

What was skipped or approximated — worth logging on partial results.

Typestring[]
content_urlstring | null

Short-lived signed URL for the extracted text.

Typestring | null
text?string | null

The extracted text inline — only present when you ask for it.

Typestring | null
Status What it means
ready The whole document was extracted.
partial Some of it came through; warnings says what didn’t. Usable, with care.
unsupported The file type has no text to extract — an image, an archive, a binary.
failed Extraction was attempted and didn’t work. The original is still downloadable.

Reading it

Fetch the text on its own:

const response = await aiinbx.attachments.content("att_...")
const markdown = await response.text()
markdown = client.attachments.content("att_...").text
curl -L https://api.aiinbx.com/api/v2/attachments/att_.../content \
  -H "Authorization: Bearer $AI_INBX_API_KEY"

Or pull every attachment’s text down with the message in one call, which is usually what you want before a model call:

const email = await aiinbx.emails.retrieve("eml_...", {
  include: ["attachment_content"],
})

const documents = email.attachments
  .filter((a) => a.preparation?.text)
  .map((a) => `## ${a.filename}\n\n${a.preparation!.text}`)
email = client.emails.retrieve("eml_...", include=["attachment_content"])

documents = [
    f"## {a['filename']}\n\n{a['preparation']['text']}"
    for a in email["attachments"]
    if a.get("preparation", {}).get("text")
]
curl "https://api.aiinbx.com/api/v2/emails/eml_...?include=attachment_content" \
  -H "Authorization: Bearer $AI_INBX_API_KEY"

An attachment with no prepared text — because it’s unsupported, failed, or still being worked on — comes back with text: null. Asking for content on one that was never prepared returns 404 not_prepared.

Handling an incoming document

if (event.type !== "email.received") return

const email = await aiinbx.emails.retrieve(event.data.email_id, {
  include: ["attachment_content"],
})

for (const attachment of email.attachments) {
  const prep = attachment.preparation

  if (prep?.status === "ready" || prep?.status === "partial") {
    if (prep.warnings.length) {
      console.warn(attachment.filename, prep.warnings)
    }
    await ingest(attachment.filename, prep.text!)
    continue
  }

  // No text to read — keep the original for a human.
  const file = await aiinbx.attachments.download(attachment.id)
  await archive(attachment.filename, await file.arrayBuffer())
}

The email.received webhook also carries a compact attachment list, so you can decide whether a message is worth fetching before you fetch it.

Timing

Preparation runs as the message is ingested, so by the time email.received reaches you the result is normally already there. A large document can still be in flight — preparation is null in that case rather than an error. If you’re handling big files, re-fetch the email rather than treating null as “no text”.

Last updated on September 9, 2026

Was this page helpful?