Attachments
Split down the middle: v2 reads attachments, v1 uploads them. v2 has no create endpoint at all, which is one of the clearest reasons v1 still ships.
import { createV1Client, createV2Client } from 'confluence.js';
const v2 = createV2Client({ host, auth });
const v1 = createV1Client({ host, auth });Upload
await v1.contentAttachments.createAttachment({
id: '12345',
attachments: { filename: 'report.pdf', content: bytes },
});content takes whatever you have:
| Type | Use |
|---|---|
string | text you already have in memory |
Buffer / Uint8Array | the usual for a file read into memory |
Blob / File | browser input, or fs.openAsBlob() |
Readable (Node stream) | a large file you do not want to buffer |
ReadableStream | a fetch response body, piped straight through |
AsyncIterable<Uint8Array> | anything else that yields bytes |
import { readFile } from 'node:fs/promises';
await v1.contentAttachments.createAttachment({
id: '12345',
attachments: { filename: 'report.pdf', content: await readFile('report.pdf') },
});Several at once — pass an array:
const created = await v1.contentAttachments.createAttachment({
id: '12345',
attachments: [
{ filename: 'notes.txt', content: 'first' },
{ filename: 'data.csv', content: 'a,b\n1,2' },
],
});
console.log(created.results.map(result => result.title));No FormData, no header
2.x made you build the FormData and remember X-Atlassian-Token: no-check. Neither is your job now — the call encodes the multipart body and sends the XSRF header itself.
Version comments and minor edits
Each attachment can set optional comment and minorEdit fields. comment is the UTF-8 version message; minorEdit: true suppresses notification emails and activity stream updates for the change.
await v1.contentAttachments.createAttachment({
id: '12345',
attachments: [
{ filename: 'notes.txt', content: 'first' },
{ filename: 'report.txt', content: 'second', comment: 'Résumé updated', minorEdit: true },
],
});These fields also work on each item passed to createOrUpdateAttachments and on attachment in updateAttachmentData.
When at least one file sets comment, files without it send an empty comment. When at least one sets minorEdit, files without it send false. If every file omits a field, the request omits that field entirely. Explicit false and empty comments are preserved.
Upsert
createAttachment (POST) adds; createOrUpdateAttachments (PUT) adds or versions an existing one with the same filename:
// First call creates it, the second adds a version — same id both times.
const first = await v1.contentAttachments.createOrUpdateAttachments({
id: '12345',
attachments: { filename: 'report.pdf', content: v1Bytes },
});
const second = await v1.contentAttachments.createOrUpdateAttachments({
id: '12345',
attachments: { filename: 'report.pdf', content: v2Bytes },
});
second.results[0].id === first.results[0].id; // trueReplace the data of one attachment
await v1.contentAttachments.updateAttachmentData({
id: '12345',
attachmentId: 'att67890',
attachment: { filename: 'report.pdf', content: newBytes },
});Rename, or edit properties
const attachment = (await v2.attachment.getAttachmentById({ id: 'att67890' }));
await v1.contentAttachments.updateAttachmentProperties({
id: '12345',
attachmentId: 'att67890',
body: {
id: 'att67890',
type: 'attachment',
title: 'report-final.pdf',
version: { number: (attachment.version?.number ?? 1) + 1 },
},
});List — v2
const attachments = await v2.attachment.getPageAttachments({ id: 12345 });
for (const attachment of attachments.results ?? []) {
console.log(attachment.title, attachment.fileSize, attachment.mediaType);
}There are siblings for every container: getBlogpostAttachments, getCustomContentAttachments, getLabelAttachments, and getAttachments for the whole site.
Download
const bytes = await v1.contentAttachments.downloadAttatchment({
id: '12345',
attachmentId: 'att67890',
});
await writeFile('report.pdf', Buffer.from(bytes as Uint8Array));The method name is Atlassian's typo. It is kept because it is the operation id — renaming it in the client would hide which endpoint you are calling.
TIP
The download is a 302 to a signed URL; fetch follows it, so you get the bytes and not the redirect. If you would rather hand the URL to a browser, v2.attachment.getAttachmentById({ id }) gives you downloadLink.
Delete — v2
await v2.attachment.deleteAttachment({ id: 'att67890' });