When creating invoices or credit notes via the Billabex API, you need to attach a document file (PDF or image). This guide explains the supported file formats, size limits, and encoding options.
Overview
File uploads are required for:
POST /api/public/v1/invoices- Create an invoicePOST /api/public/v1/credit-notes- Create a credit notePUT /api/public/v1/organizations/{organizationId}/logo- Set the organization logoPUT /api/public/v1/users/me/avatar- Set the authenticated user’s profile picture
These endpoints accept multipart/form-data requests with a file attachment.
Email attachments take a different shape: they travel as JSON, not as
multipart/form-data, because an email carries several of them at once. Both endpoints below
accept a files array of { content, filename, mimeType }, where content is the base64-encoded
file, alongside an attachmentIds/attachments array naming files that already exist:
POST /api/public/v1/accounts/{accountId}/outgoing-email-communications- Send an email nowPUT /api/public/v1/outgoing-email-communications/{communicationId}- Edit a scheduled reminder
On the edit, attachments is the list of attachment IDs to keep, so sending a shorter list is
how a piece is detached. It only accepts identifiers the server already vouched for on that
communication: the files already on the email, the files uploaded through files in the same call,
the files of its financial manifest, and its bank details document. A financial document the
reminder declared required cannot be removed, and requiredAttachmentIds on the communication says
which ones those are. Files sent through files are attached whether or not attachments lists
them: their identifiers are minted by that very call. The server checks access to the scheduled
reminder before uploading and deletes every file created by the request if the edit is rejected.
Documents created this way are stamped with the PublicApi channel, which is returned on every invoice and credit note. They sit alongside documents imported by a connector and documents typed in the web app: an organization can use all of these at once. See Sources for how a document’s channel differs from an account’s source.
Supported File Formats
| MIME Type | Extension | Description |
|---|---|---|
application/pdf |
PDF documents (recommended) | |
image/png |
.png | PNG images |
image/jpeg |
.jpg, .jpeg | JPEG images |
image/webp |
.webp | WebP images |
The organization logo and user avatar are exceptions: they take images only (image/png,
image/jpeg, image/webp), never a PDF, and are capped at 2MB. DELETE on the same route removes
either image. Removing an organization logo falls back to the organization’s email-domain logo;
removing an avatar falls back to the user’s initials.
Size Limits
- Maximum file size: 10MB (decoded binary size), 2MB for logos and avatars
- Files exceeding this limit will be rejected with a
400 Bad Requesterror
Encoding Options
Billabex supports two encoding methods for file uploads:
| Encoding | Content-Transfer-Encoding | Use Case |
|---|---|---|
| Binary | 7bit, 8bit, or binary |
Default, most HTTP clients |
| Base64 | base64 |
Legacy systems, NetSuite, etc. |
Binary Upload (Default)
Most HTTP clients send files as binary data by default. This is the recommended method when your platform supports it.
JavaScript Example
const formData = new FormData();
formData.append('accountId', 'ACCOUNT_ID');
formData.append('number', 'INV-2024-001');
formData.append('issuedDate.year', '2024');
formData.append('issuedDate.month', '1');
formData.append('issuedDate.day', '15');
formData.append('dueDate.year', '2024');
formData.append('dueDate.month', '2');
formData.append('dueDate.day', '15');
formData.append('totalAmount', '1500.00');
formData.append('taxAmount', '300.00');
formData.append('paidAmount', '0');
formData.append('billingAddress.street', '123 Main Street');
formData.append('billingAddress.city', 'Paris');
formData.append('billingAddress.postalCode', '75001');
formData.append('billingAddress.country', 'FR');
formData.append('file', fileBlob, 'invoice.pdf');
const response = await fetch('[baseURL]/api/public/v1/invoices', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
},
body: formData,
});
cURL Example
curl -X POST "[baseURL]/api/public/v1/invoices" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-F "accountId=ACCOUNT_ID" \
-F "number=INV-2024-001" \
-F "issuedDate.year=2024" \
-F "issuedDate.month=1" \
-F "issuedDate.day=15" \
-F "dueDate.year=2024" \
-F "dueDate.month=2" \
-F "dueDate.day=15" \
-F "totalAmount=1500.00" \
-F "taxAmount=300.00" \
-F "paidAmount=0" \
-F "billingAddress.street=123 Main Street" \
-F "billingAddress.city=Paris" \
-F "billingAddress.postalCode=75001" \
-F "billingAddress.country=FR" \
-F "file=@invoice.pdf"
Base64 Upload
Some platforms (like NetSuite) cannot send binary data directly and must encode files as base64. Billabex automatically detects and decodes base64-encoded files.
To use base64 encoding:
- Encode your file content as base64
- Set the
Content-Transfer-Encodingheader tobase64in the file part
How Base64 Works
Base64 encoding converts binary data to ASCII text, increasing the size by approximately 33%. A 10MB file becomes ~13.3MB when base64-encoded.
Billabex handles this automatically:
- The 10MB limit applies to the decoded binary size
- Base64-encoded uploads up to ~13.7MB are accepted
- The file is decoded server-side before storage
JavaScript Example (Base64)
// Read file and encode as base64
const fileBuffer = await file.arrayBuffer();
const base64Content = btoa(String.fromCharCode(...new Uint8Array(fileBuffer)));
// Create multipart request with base64-encoded file
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).slice(2);
const body = [
`--${boundary}`,
'Content-Disposition: form-data; name="accountId"',
'',
'ACCOUNT_ID',
`--${boundary}`,
'Content-Disposition: form-data; name="number"',
'',
'INV-2024-001',
// ... other fields ...
`--${boundary}`,
'Content-Disposition: form-data; name="file"; filename="invoice.pdf"',
'Content-Type: application/pdf',
'Content-Transfer-Encoding: base64',
'',
base64Content,
`--${boundary}--`,
].join('\r\n');
const response = await fetch('[baseURL]/api/public/v1/invoices', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
},
body: body,
});
Node.js Example (Base64)
const fs = require('fs');
const path = require('path');
// Read file and encode as base64
const filePath = './invoice.pdf';
const fileContent = fs.readFileSync(filePath);
const base64Content = fileContent.toString('base64');
const filename = path.basename(filePath);
// Build multipart body manually
const boundary = '----FormBoundary' + Date.now();
const CRLF = '\r\n';
const parts = [
`--${boundary}`,
'Content-Disposition: form-data; name="accountId"',
'',
'ACCOUNT_ID',
`--${boundary}`,
'Content-Disposition: form-data; name="number"',
'',
'INV-2024-001',
`--${boundary}`,
'Content-Disposition: form-data; name="issuedDate.year"',
'',
'2024',
`--${boundary}`,
'Content-Disposition: form-data; name="issuedDate.month"',
'',
'1',
`--${boundary}`,
'Content-Disposition: form-data; name="issuedDate.day"',
'',
'15',
`--${boundary}`,
'Content-Disposition: form-data; name="dueDate.year"',
'',
'2024',
`--${boundary}`,
'Content-Disposition: form-data; name="dueDate.month"',
'',
'2',
`--${boundary}`,
'Content-Disposition: form-data; name="dueDate.day"',
'',
'15',
`--${boundary}`,
'Content-Disposition: form-data; name="totalAmount"',
'',
'1500.00',
`--${boundary}`,
'Content-Disposition: form-data; name="taxAmount"',
'',
'300.00',
`--${boundary}`,
'Content-Disposition: form-data; name="paidAmount"',
'',
'0',
`--${boundary}`,
'Content-Disposition: form-data; name="billingAddress.street"',
'',
'123 Main Street',
`--${boundary}`,
'Content-Disposition: form-data; name="billingAddress.city"',
'',
'Paris',
`--${boundary}`,
'Content-Disposition: form-data; name="billingAddress.postalCode"',
'',
'75001',
`--${boundary}`,
'Content-Disposition: form-data; name="billingAddress.country"',
'',
'FR',
`--${boundary}`,
`Content-Disposition: form-data; name="file"; filename="${filename}"`,
'Content-Type: application/pdf',
'Content-Transfer-Encoding: base64',
'',
base64Content,
`--${boundary}--`,
];
const body = parts.join(CRLF);
const response = await fetch('[baseURL]/api/public/v1/invoices', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
},
body: body,
});
Python Example (Base64)
import base64
import requests
# Read and encode file
with open('invoice.pdf', 'rb') as f:
file_content = f.read()
base64_content = base64.b64encode(file_content).decode('ascii')
# Build multipart body
boundary = '----PythonFormBoundary'
crlf = '\r\n'
body_parts = [
f'--{boundary}',
'Content-Disposition: form-data; name="accountId"',
'',
'ACCOUNT_ID',
f'--{boundary}',
'Content-Disposition: form-data; name="number"',
'',
'INV-2024-001',
# ... other fields ...
f'--{boundary}',
'Content-Disposition: form-data; name="file"; filename="invoice.pdf"',
'Content-Type: application/pdf',
'Content-Transfer-Encoding: base64',
'',
base64_content,
f'--{boundary}--',
]
body = crlf.join(body_parts)
response = requests.post(
'[baseURL]/api/public/v1/invoices',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': f'multipart/form-data; boundary={boundary}',
},
data=body.encode('utf-8'),
)
Error Handling
| Error | HTTP Status | Description |
|---|---|---|
| No file uploaded | 400 | The file field is missing from the request |
| File too large | 400 | File exceeds 10MB (decoded size) |
| Invalid file type | 400 | MIME type not in allowed list |
| Invalid multipart | 400 | Malformed multipart request |
Example error response:
{
"statusCode": 400,
"message": "File too large. Maximum size: 10485760 bytes",
"error": "Bad Request"
}
Best Practices
Choose the Right Encoding
- Use binary (default) whenever possible - it’s more efficient
- Use base64 only when your platform doesn’t support binary uploads
Validate Files Client-Side
Check file size and type before uploading to avoid unnecessary API calls:
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_TYPES = [
'application/pdf',
'image/png',
'image/jpeg',
'image/webp',
];
if (file.size > MAX_SIZE) {
throw new Error('File too large');
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error('Invalid file type');
}
Use PDF When Possible
PDF is the recommended format for invoice documents:
- Preserves formatting across devices
- Supports text extraction
- Smaller file sizes for text-heavy documents
Next Steps
- Getting Started - API basics and authentication
- API Reference - Full endpoint documentation
Support
Questions about file uploads?
Contact us via the website contact form.