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)
}
idstring
att_…
stringfilenamestring
As the sender named it.
stringcontent_typestring
The declared media type.
stringsizenumber
Bytes.
numbercidstring | null
Content-ID when the attachment is inline in the HTML body.
string | nulldownload_urlstring
Short-lived signed URL for the original file.
stringpreparationobject | null
Extraction result, or null when nothing was attempted.
object | nullDownloading 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.contentcurl -L https://api.aiinbx.com/api/v2/attachments/att_... \
-H "Authorization: Bearer $AI_INBX_API_KEY" \
-o report.pdfThe 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:
status"ready" | "partial" | "unsupported" | "failed"
Whether the extraction produced usable text.
"ready" | "partial" | "unsupported" | "failed"format"markdown" | "text" | null
Markdown keeps document structure; text is a flat rendering.
"markdown" | "text" | nullpagesnumber | null
Page count, where the format has pages.
number | nullwarningsstring[]
What was skipped or approximated — worth logging on partial results.
string[]content_urlstring | null
Short-lived signed URL for the extracted text.
string | nulltext?string | null
The extracted text inline — only present when you ask for it.
string | 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_...").textcurl -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”.