Data Management6 min

File Uploads

Upload invoice, credit note and email attachment files using binary or base64 encoding.

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 invoice
  • POST /api/public/v1/credit-notes - Create a credit note
  • PUT /api/public/v1/organizations/{organizationId}/logo - Set the organization logo
  • PUT /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 now
  • PUT /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 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 Request error

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:

  1. Encode your file content as base64
  2. Set the Content-Transfer-Encoding header to base64 in 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

Support

Questions about file uploads?
Contact us via the website contact form.