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.pdf

Multipart, 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:

formatWhat comes back
textThe file's own text
pdfThe text layer, a form feed between pages
docxHeaders, body, footers; a table row on one line, cells tab-separated. Footnotes and comments are left out
xlsxEach 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.