Search the docs
Find a page, a section, or an endpoint.
Attachments
Files up, files down, and never through the API twice.
Files go up on their own and are named when you send. Nothing here is base64 in a JSON body.
Sending one
Upload it, then reference the id:
curl https://pidgeon.ai/api/v1/attachments \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-F identity_id=0c939bd7-2d69-4150-a2ac-de994f0e4887 \
-F file=@invoice.pdfMultipart, not JSON. A ten-megabyte PDF is thirteen megabytes of base64, and every proxy between you and us would carry the difference for nothing.
Under 4 MB this way. A request to the API can be at most 4.5 MB, whatever
your plan allows a file to be, and a larger one is refused with a 413 before
it reaches us. For anything bigger, use the next section.
Sending a large one
Say what the file is, put it straight into storage, then say you are done:
curl https://pidgeon.ai/api/v1/attachments/uploads -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{"identity_id": "0c939bd7-2d69-4150-a2ac-de994f0e4887",
"filename": "report.pdf", "content_type": "application/pdf",
"size": 18874368}'
# PUT the bytes to data.upload.url with the headers in data.upload.headers
curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --upload-file report.pdf
curl -X POST https://pidgeon.ai/api/v1/attachments/$ID/complete -H "Authorization: Bearer $PIDGEON_API_KEY"The size and type are part of the link. Storage refuses a body of any
other length or a different Content-Type, so the file you put is the file
you described. The link works for 15 minutes.
Nothing is sendable until it is complete. A send naming an upload you have
not completed is a 409, and so is completing one before the PUT has
landed. Completing twice is fine.
Both ways check the same things: that the key may draft from the address, that the type is one we carry, and that the file fits your plan.
The same id can go on a draft instead, which is how a reply gets a file attached before anybody has decided to send it.
Receiving one
A message that arrives with files lists them, with their names, types and sizes. The bytes are not in the payload — a webhook carries ids, never bodies, which is the same rule everywhere in this API.
Ask for a link when you want the file:
curl https://pidgeon.ai/api/v1/attachments/att_7c2a \
-H "Authorization: Bearer $PIDGEON_API_KEY"A signed URL rather than the bytes. Streaming a 25 MB file through the API would pay for the transfer twice and gain the caller nothing — the link goes straight to storage, and expires.
It is short-lived on purpose. Fetch it when you are about to use it rather than storing it: a URL that outlived its usefulness in somebody's database is a file leaking to whoever finds the row.
Reading one as text
A CSV, a log, JSON, a calendar file, a PDF, a Word file or a spreadsheet can come back as text instead, for an agent that needs to read what it was sent rather than download it:
curl "https://pidgeon.ai/api/v1/attachments/att_7c2a/text?body=clean" \
-H "Authorization: Bearer $PIDGEON_API_KEY"The file is screened like the message it came with. An attachment is mail
a model reads, so firewall says whether the text tries to steer the agent,
judged with the message's sender, and body=clean returns it with invisible
characters removed and lines posing as a chat turn marked — see
Pidgeon Firewall. The default is original, the text as it
arrived; the MCP server asks for clean.
Text a document hides counts as hidden text. A Word run marked hidden, or
a hidden sheet, row or column in a spreadsheet, is what a person opening the
file never sees, so an instruction there is flagged hidden_text and left out
of body=clean. At an address that
holds suspicious mail, a flagged attachment
holds its message: your key gets 403 held, and a person decides.
format says how it was read:
format | What comes back |
|---|---|
text | The file's own text |
pdf | The text layer, a form feed between pages |
docx | Headers, body, footers; a table row on one line, cells tab-separated. Footnotes and comments are left out |
xlsx | Each sheet as ## Sheet: <name> and CSV. Dates are Excel serial numbers (days since 30 December 1899), not formatted dates |
Up to 5 MB for a text file and 10 MB for a document, of which the first
200,000 characters come back, with truncated: true when there was more.
Refused with a validation error that names the reason, and with the link
above as the way to the file: a scanned PDF (no text layer, and no OCR), a
password-protected PDF, a document over 300 pages or unpacking to more than
50 MB, .doc, .xls, PowerPoint, HTML and images.