---
title: "File Uploads"
description: "Upload invoice, credit note and email attachment files using binary or base64 encoding."
canonical: https://developer.billabex.com/en/guides/file-uploads/
lang: en
alternate: https://developer.billabex.com/fr/guides/televersement-de-fichiers/
last-updated: 2026-10-06
---

# File Uploads

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

Source: https://developer.billabex.com/en/guides/file-uploads/
Language: English (en)
French version: https://developer.billabex.com/fr/guides/televersement-de-fichiers/

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

For both sending and editing, each existing file must belong to the account organization.
Knowing its identifier, or placing it in a task or an older email, is insufficient. The server
checks ownership before sending. If an older file cannot be verified, upload it again through
`files`. MCP uses the same rule. New files are assigned to the organization after account or
communication access is checked. New financial documents also carry this proof and can be attached
immediately after creation. A send can report a later error after the message was stored; its files
are preserved so its references remain valid.

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 selectable on that communication, subject to
the ownership check: the files already on the email, the files uploaded through `files` in the same call,
the files of its financial manifest, its bank details document, and, for an email prepared from a
task, the files of that task: those of its thread and of the emails it refers to. 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](/en/guides/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)

The `taxAmount` field accepts signed tax on invoices and credit notes. For example,
`totalAmount=27` and `taxAmount=-31.67` give a pre-tax amount of 58.67 without an adjustment.
Preserve the tax sign: a taxable advance deduction can produce negative net tax with
a positive total. The same rule applies to MCP imports and the web app.

For an invoice, `adjustmentAmount` is a signed post-tax amount already included in `totalAmount`.
It defaults to zero when omitted. For example, 100 before tax, 20 tax and a -115 adjustment give
`totalAmount=5`, `taxAmount=20`, `adjustmentAmount=-115`. Invoice details return all three amounts;
the amount owed uses the final total of 5.

Most HTTP clients send files as binary data by default. This is the recommended method when your platform supports it.

#### JavaScript Example

```javascript
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('https://next.billabex.com/api/public/v1/invoices', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
  body: formData,
});
```

#### cURL Example

```bash
curl -X POST "https://next.billabex.com/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)

```javascript
// 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('https://next.billabex.com/api/public/v1/invoices', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': `multipart/form-data; boundary=${boundary}`,
  },
  body: body,
});
```

#### Node.js Example (Base64)

```javascript
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('https://next.billabex.com/api/public/v1/invoices', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': `multipart/form-data; boundary=${boundary}`,
  },
  body: body,
});
```

#### Python Example (Base64)

```python
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(
    'https://next.billabex.com/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:

```json
{
  "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:

```javascript
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](/en/guides/getting-started/)** - API basics and authentication
- **[API Reference](/en/api-reference/)** - Full endpoint documentation

## Support

Questions about file uploads?  
Contact us via the [website contact form](https://billabex.com).

---

Billabex developer portal. OpenAPI specification: https://developer.billabex.com/openapi.json.
Agent instructions: https://developer.billabex.com/llms.txt. Complete documentation: https://developer.billabex.com/llms-full.txt.
