File Uploads
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.
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.