Store and attach files
Upload a file once, then attach it by id to drafts, replies and transactional email. You can also list and delete stored files.
Store a small file
Send the file's bytes in base64 as content, with its name and type. This stores a short text file:
const file = await colding.files.create({
name: "price-list.txt",
type: "text/plain",
content: btoa("Hello Ana, here is our price list.\n"),
});
console.log(file.id, file.sha256);The answer is the stored file: its id, name, type, sizeBytes, sha256, who stored it and expiresAt. Compare sha256 with your own copy's hash to know the file arrived whole. We keep the type you give and never read the bytes to check it. Left out, it is application/octet-stream.
On MCP a whole call is capped at 1 MB, about 750 KB of file. For a bigger file, use an upload URL.
Upload a bigger file
files.createUpload takes the file's name and type and answers a url. PUT the raw bytes to it from a shell or your program. Don't send your API key with the PUT. The URL works once, for 15 minutes, and takes up to 15 MB. Keep it secret.
const upload = await colding.files.createUpload({ name: file.name, type: file.type });
if (!upload.url) throw new Error("Ask for another upload URL with a new Idempotency-Key.");
const response = await fetch(upload.url, { method: "PUT", body: file });
if (!response.ok) {
const wait = response.headers.get("Retry-After");
throw new Error(
wait
? `Upload paused. Wait ${wait} seconds, then retry the same URL.`
: `Upload failed (${response.status}). Read the answer before you retry.`,
);
}
console.log(await response.json()); // { data: the stored file, with its id and sha256 }From a shell, curl -T invoice-1042.pdf "$URL" sends the file. The PUT answers the stored file, in the same shape files.create answers.
If the PUT answer is lost, retry with the same URL while it works. A used URL answers the same file without reading the bytes again. Your key and the person who made it must still have permission, even on a retry. If the URL expired, ask for another. Asking again with the same Idempotency-Key answers url: null, so use a new key to get a new URL.
Attach files to an email
Pass up to 10 file ids in fileIds when you write or change a draft (drafts.create, drafts.update, drafts.reply), answer a message now (messages.reply) or send a transactional email (transactional.send). On drafts.update, fileIds replaces the draft's list, and [] removes every file.
const draft = await colding.drafts.create({
mailboxId: "11111111-1111-4111-8111-111111111111",
to: "[email protected]",
subject: "Our price list",
bodyText: "Here is the price list you asked for.",
fileIds: [fileId],
});
console.log(draft.files);
const reply = await colding.messages.reply({
messageId: "22222222-2222-4222-8222-222222222222",
bodyText: "Here is the signed copy.",
fileIds: [fileId],
intentKey: "reply-signed-copy-1",
});
console.log(reply.files);
const receipt = await colding.transactional.send({
mailboxId: "33333333-3333-4333-8333-333333333333",
to: "[email protected]",
subject: "Invoice 1042",
bodyText: "Thank you for your payment. Your invoice is attached.",
fileIds: [fileId],
intentKey: "invoice-1042",
});
console.log(receipt.files);Attached files go out only from a mailbox you connected with its own mail server. An agent inbox, a sender address or a mailbox that routes through our mail servers answers 409 serverErrors.filesNotFromOurServers (and drafts.preview lists files_not_from_our_servers). Microsoft 365 takes up to 4 MB of base64 MIME in all, and Gmail and SMTP take up to 25 MB of raw MIME; past that, the send answers 409 serverErrors.messageTooLargeForMailbox (and drafts.preview lists message_too_large). When a draft with files goes to an address that has not written to the workspace, drafts.preview also includes files_on_first_email in warnings.
Drafts and previews list attached files in files (id, name, type, sizeBytes, sha256). Sends freeze the same metadata in files with fileId, which becomes null once the stored file is deleted or expires.
Find and delete files
files.list lists the workspace's files, newest first, a page at a time. files.get reads one, and files.remove deletes one now, bytes and all (409 serverErrors.filePinned while an unsent draft or an email waiting to go out uses it). None of them answers the bytes.
const page = await colding.files.list({ limit: 50 });
for (const file of page.items) console.log(file.name, file.sizeBytes, file.expiresAt);
const [newest] = page.items;
if (newest) {
const file = await colding.files.get({ fileId: newest.id });
console.log(file.sha256);
await colding.files.remove({ fileId: file.id });
}Any key with operate, send or transactional can store files. A key with operate and no read list sees every file in the workspace. A key with a read list, or without operate, sees, attaches and deletes only the files it stored itself. A file in another workspace answers 404.
Limits and how long files stay
A file can be up to 15 MB, and an email can carry up to 10 files. A bigger file answers 413, and an empty one answers 400.
A workspace keeps up to 1,024 MB of files in all. Each upload URL not used yet holds 15 MB of that until it is used or expires. Past the limit, storing a file or asking for a URL answers 409 serverErrors.fileStorageFull, with used and limit in bytes. Delete files you no longer need to make room.
While an unsent draft or a scheduled, reserved or sending email uses a file, expiresAt is null and the file cannot be deleted or pruned. Otherwise it is deleted 7 days after it was stored or last used, or sooner when you delete it. Reading or listing it doesn't change that. Once its expiresAt passes, it answers 404 and no longer counts toward the limit.
A workspace can store files and ask for upload URLs 300 times an hour. Past that it answers 429 with Retry-After.