> ## Documentation Index
> Fetch the complete documentation index at: https://docs.contazen.ro/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload Attachment

> Upload and attach files to an expense for documentation and compliance purposes

## Overview

The Upload Attachment endpoint allows you to attach a single file to an expense record. This is essential for maintaining proper documentation, compliance with tax regulations, and audit trails. Expenses support one attachment at a time - uploading a new file replaces the previous one.

<Info>
  **Security Note**: Files undergo multiple validation checks including MIME type verification and content validation to ensure security.
</Info>

<Warning>
  Each expense supports only one attachment. Uploading a new file will replace the existing attachment.
</Warning>

## Path Parameters

<ParamField path="id" type="string" required>
  The CzUid of the expense to attach the file to
</ParamField>

## Request Body

This endpoint uses `multipart/form-data` encoding for file uploads:

<ParamField body="file" type="file" required>
  The file to upload. See supported file types below.
</ParamField>

## Supported File Types

<ParamField name="Allowed Extensions" type="array">
  * `pdf` - Portable Document Format (most common for invoices/receipts)
  * `jpg`, `jpeg` - JPEG images
  * `png` - PNG images
</ParamField>

<Note>
  Files are validated at multiple levels:

  1. Extension validation
  2. MIME type verification
  3. Content header validation (e.g., PDFs must start with %PDF)
</Note>

## File Size Limits

<ParamField name="Maximum File Size" type="string">
  10 MB per file
</ParamField>

## Response

<ResponseField name="attachment" type="object">
  Information about the uploaded attachment

  <Expandable title="Attachment Object">
    <ResponseField name="id" type="string">
      Unique CzUid for the attachment
    </ResponseField>

    <ResponseField name="filename" type="string">
      Original filename as uploaded
    </ResponseField>

    <ResponseField name="filesize" type="integer">
      File size in bytes
    </ResponseField>

    <ResponseField name="description" type="string">
      Description provided during upload (may be empty)
    </ResponseField>

    <ResponseField name="created_at" type="string" format="datetime">
      When the attachment was uploaded
    </ResponseField>

    <ResponseField name="download_url" type="string">
      URL for downloading the attachment (authentication required)
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="message" type="string">
  Success message confirming the upload
</ResponseField>

## File Storage and Security

### Storage Location

Files are stored securely on the server with:

* **Organized structure**: `galleries/expenses/{firm_id}/{year}/{month}/`
* **Unique filenames**: Generated to prevent conflicts and enhance security
* **Access control**: Only accessible to authorized users of the owning firm

### Security Measures

* **File validation**: Content type verification beyond extension checking
* **Virus scanning**: Files may be scanned for malware (implementation dependent)
* **Access logging**: Download access is logged for audit purposes
* **Firm isolation**: Attachments are strictly isolated per firm

## Integration with Expense Workflow

### Automatic Attachment Detection

When retrieving expenses, attachments are automatically detected:

```json theme={null}
{
  "expense": {
    "id": "exp_abc123",
    "attachment": {
      "url": "https://api.contazen.ro/v1/files/download/att_def456",
      "type": "pdf"
    }
  }
}
```

### Document Management

Attachments become part of the expense's permanent record:

* **Audit trail**: Preserved for compliance and audit requirements
* **Version control**: Multiple attachments can be added to an expense
* **Integration**: Can be referenced in reports and exports

## Common Use Cases

### Invoice Documentation

```javascript theme={null}
// Upload invoice PDF for expense documentation
const uploadInvoice = async (expenseId, invoiceFile) => {
  const formData = new FormData();
  formData.append('file', invoiceFile);
  formData.append('description', 'Original supplier invoice');
  
  const response = await uploadAttachment(expenseId, formData);
  return response.attachment;
};
```

### Receipt Management

```javascript theme={null}
// Upload receipt photo from mobile app
const uploadReceipt = async (expenseId, receiptPhoto) => {
  const formData = new FormData();
  formData.append('file', receiptPhoto);
  formData.append('description', 'Receipt photo taken on mobile');
  
  return await uploadAttachment(expenseId, formData);
};
```

### Supporting Documentation

```javascript theme={null}
// Upload additional supporting documents
const uploadSupporting = async (expenseId, documents) => {
  const attachments = [];
  
  for (const doc of documents) {
    const formData = new FormData();
    formData.append('file', doc.file);
    formData.append('description', doc.description);
    
    const attachment = await uploadAttachment(expenseId, formData);
    attachments.push(attachment);
  }
  
  return attachments;
};
```

<RequestExample>
  ```bash cURL theme={null}
  # Upload a PDF invoice
  curl -X POST "https://api.contazen.ro/v1/expenses/exp_abc123/attachments" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -F "file=@invoice_001.pdf" \
    -F "description=Original supplier invoice"

  # Upload an image receipt
  curl -X POST "https://api.contazen.ro/v1/expenses/exp_abc123/attachments" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -F "file=@receipt.jpg" \
    -F "description=Receipt photo"
  ```

  ```javascript JavaScript theme={null}
  const uploadAttachment = async (expenseId, file, description = '') => {
    const formData = new FormData();
    formData.append('file', file);
    if (description) {
      formData.append('description', description);
    }

    const response = await fetch(`https://api.contazen.ro/v1/expenses/${expenseId}/attachments`, {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_API_KEY'
        // Note: Don't set Content-Type header - let browser set it for multipart/form-data
      },
      body: formData
    });

    const data = await response.json();
    
    if (data.success) {
      console.log('File uploaded:', data.attachment.filename);
      console.log('Download URL:', data.attachment.download_url);
      return data.attachment;
    } else {
      throw new Error(data.error.message);
    }
  };

  // Usage with file input
  const handleFileUpload = async (expenseId, fileInputElement) => {
    const file = fileInputElement.files[0];
    if (!file) return;
    
    try {
      const attachment = await uploadAttachment(expenseId, file, 'Uploaded via web form');
      alert('File uploaded successfully!');
    } catch (error) {
      alert('Upload failed: ' + error.message);
    }
  };

  // Usage with drag-and-drop
  const handleDrop = async (expenseId, event) => {
    event.preventDefault();
    const files = event.dataTransfer.files;
    
    for (let i = 0; i < files.length; i++) {
      try {
        const attachment = await uploadAttachment(expenseId, files[i]);
        console.log(`Uploaded: ${attachment.filename}`);
      } catch (error) {
        console.error(`Failed to upload ${files[i].name}:`, error.message);
      }
    }
  };
  ```

  ```php PHP theme={null}
  function uploadExpenseAttachment($expenseId, $filePath, $description = '') {
      if (!file_exists($filePath)) {
          throw new Exception("File not found: $filePath");
      }

      $curl = curl_init();
      
      $postFields = [
          'file' => new CURLFile($filePath),
          'description' => $description
      ];

      curl_setopt_array($curl, [
          CURLOPT_URL => "https://api.contazen.ro/v1/expenses/{$expenseId}/attachments",
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_POST => true,
          CURLOPT_POSTFIELDS => $postFields,
          CURLOPT_HTTPHEADER => [
              'Authorization: Bearer YOUR_API_KEY'
          ]
      ]);

      $response = curl_exec($curl);
      $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
      $data = json_decode($response, true);
      curl_close($curl);

      if ($httpCode == 201 && $data['success']) {
          echo "Uploaded: {$data['attachment']['filename']} ({$data['attachment']['filesize']} bytes)\n";
          return $data['attachment'];
      } else {
          throw new Exception($data['error']['message']);
      }
  }

  // Usage examples
  try {
      $attachment = uploadExpenseAttachment('exp_abc123', '/path/to/invoice.pdf', 'Original invoice');
      echo "Download URL: {$attachment['download_url']}\n";
  } catch (Exception $e) {
      echo "Upload failed: " . $e->getMessage() . "\n";
  }

  // Bulk upload from directory
  function uploadExpenseAttachmentsFromDirectory($expenseId, $directory) {
      $allowedTypes = ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx'];
      $uploadedFiles = [];
      
      foreach (glob("$directory/*") as $filePath) {
          $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
          
          if (in_array($extension, $allowedTypes)) {
              try {
                  $filename = basename($filePath);
                  $attachment = uploadExpenseAttachment($expenseId, $filePath, "Auto-uploaded: $filename");
                  $uploadedFiles[] = $attachment;
              } catch (Exception $e) {
                  echo "Failed to upload $filePath: " . $e->getMessage() . "\n";
              }
          }
      }
      
      return $uploadedFiles;
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 201 - Success theme={null}
  {
    "success": true,
    "attachment": {
      "id": "att_def456",
      "filename": "invoice_001.pdf",
      "filesize": 245760,
      "description": "Original supplier invoice",
      "created_at": "2024-01-20 14:30:00",
      "download_url": "https://api.contazen.ro/v1/files/download/att_def456"
    },
    "message": "Attachment uploaded successfully"
  }
  ```

  ```json 400 - File Missing theme={null}
  {
    "success": false,
    "error": {
      "message": "No file was uploaded or upload failed",
      "type": "invalid_request_error",
      "code": "file_missing"
    }
  }
  ```

  ```json 400 - Invalid File Type theme={null}
  {
    "success": false,
    "error": {
      "message": "Invalid file type. Allowed types: pdf, jpg, jpeg, png, doc, docx, xls, xlsx",
      "type": "invalid_request_error",
      "code": "invalid_file_type",
      "allowed_types": ["pdf", "jpg", "jpeg", "png", "doc", "docx", "xls", "xlsx"]
    }
  }
  ```

  ```json 400 - File Too Large theme={null}
  {
    "success": false,
    "error": {
      "message": "File size exceeds maximum allowed size",
      "type": "invalid_request_error",
      "code": "file_too_large",
      "max_size": "10MB"
    }
  }
  ```

  ```json 404 - Expense Not Found theme={null}
  {
    "success": false,
    "error": {
      "message": "Expense not found",
      "type": "invalid_request_error",
      "code": "resource_missing",
      "param": "id"
    }
  }
  ```

  ```json 500 - Upload Failed theme={null}
  {
    "success": false,
    "error": {
      "message": "Failed to save attachment",
      "type": "api_error",
      "code": "upload_failed",
      "details": "Server error occurred while processing the upload"
    }
  }
  ```
</ResponseExample>

## Best Practices

### File Organization

1. **Naming conventions**: Use descriptive filenames that identify the expense
2. **File types**: Prefer PDF for official documents, JPEG/PNG for photos
3. **File sizes**: Optimize images to reduce file size while maintaining readability
4. **Descriptions**: Always include meaningful descriptions for better organization

### Upload Workflow

1. **Validation**: Check file type and size before uploading
2. **Progress indicators**: Show upload progress for large files
3. **Error handling**: Implement proper error handling and user feedback
4. **Backup strategy**: Consider keeping local copies of important documents

### Security Considerations

1. **File scanning**: Scan uploaded files for malware before processing
2. **Access control**: Ensure only authorized users can upload attachments
3. **Data privacy**: Be mindful of sensitive information in uploaded files
4. **Retention policies**: Establish policies for how long attachments are kept

### Integration Tips

1. **Bulk uploads**: For multiple files, upload them sequentially to avoid overwhelming the server
2. **Mobile optimization**: Optimize upload process for mobile devices with potentially slower connections
3. **Automated uploads**: Consider automated workflows that upload documents from email or document management systems
4. **Thumbnail generation**: For images, consider generating thumbnails for better UI experience


## OpenAPI

````yaml POST /expenses/{id}/attachments
openapi: 3.1.0
info:
  title: Contazen API
  version: 1.2.0
  description: >
    Build powerful integrations with the Contazen invoicing platform. The
    Contazen API is organized around REST, 

    has predictable resource-oriented URLs, accepts JSON request bodies, returns
    JSON-encoded responses, 

    and uses standard HTTP response codes, authentication, and verbs.


    ## Authentication

    The API uses Bearer token authentication. Include your API key in the
    Authorization header.


    ### Getting your API Key

    1. Log in to your Contazen account

    2. Navigate to Settings > API

    3. Generate or copy your API key (starts with `sk_live_` for production or
    `sk_test_` for testing)


    ### Using the API Key

    Include your API key in the Authorization header:

    ```

    Authorization: Bearer sk_live_YOUR_API_KEY

    ```


    ### Example Request with cURL

    ```bash

    curl --request GET \
      --url https://api.contazen.ro/v1/clients \
      --header 'Authorization: Bearer sk_live_YOUR_API_KEY' \
      --header 'Accept: application/json'
    ```


    ## Rate Limiting

    - 1000 requests per hour per API key

    - 100 create operations per minute per API key


    Rate limit information is included in response headers:

    - `X-RateLimit-Limit`: Maximum requests allowed

    - `X-RateLimit-Remaining`: Requests remaining

    - `X-RateLimit-Reset`: Reset time (Unix timestamp)


    ## Pagination

    All list endpoints return paginated results with the following format:

    ```json

    {
      "success": true,
      "data": {
        "object": "list",
        "data": [...],
        "has_more": true,
        "total": 245,
        "page": 1,
        "per_page": 50,
        "total_pages": 5
      },
      "meta": {
        "version": "v1",
        "request_id": "req_1a2b3c4d",
        "response_time": "23.45ms"
      }
    }

    ```


    ## Multi-Work-Point Access

    API keys belong to a specific work point but can access data from all work
    points

    within the same parent company.


    ## Error Handling

    The API uses conventional HTTP response codes to indicate success or
    failure. 

    In general: 2xx codes indicate success, 4xx codes indicate an error due to
    the 

    information provided, and 5xx codes indicate an error with Contazen's
    servers.


    ## Expanding Nested Objects

    Many endpoints support the `expand` parameter to include related objects in
    the response.

    This follows the Stripe API pattern. For example:

    - `expand[]=lines` - Include invoice line items

    - `expand[]=payments` - Include payment records

    - `expand[]=client` - Include full client object


    ## Localization

    The API supports multiple languages through:

    - `locale` query parameter (en, ro)

    - `Accept-Language` header

    - Default: English
  contact:
    name: Contazen Support
    email: support@contazen.ro
    url: https://contazen.ro
  license:
    name: Proprietary
    url: https://www.contazen.ro/termeni-si-conditii-de-utilizare/
servers:
  - url: https://api.contazen.ro/v1
    description: Production API server
security:
  - bearerAuth: []
tags:
  - name: Authentication
    description: API authentication and test endpoints
  - name: Clients
    description: Manage your customers (B2B and B2C)
  - name: Invoices
    description: Create and manage invoices, proformas, and receipts
  - name: Products
    description: Manage your product and service catalog
  - name: Expenses
    description: Track and manage business expenses
  - name: Expense Categories
    description: Organize expenses with categories
  - name: Suppliers
    description: Manage expense suppliers and vendors
  - name: Settings
    description: API settings and configuration
  - name: Payments
    description: Payments received against invoices
  - name: Receipts
    description: Cash receipts (chitanțe) — standalone or paired with an invoice
  - name: Company Lookup
    description: Romanian company lookup (ANAF / VIES)
  - name: Invoice Series
    description: Manage invoice numbering series
  - name: Bank Accounts
    description: Manage IBAN bank accounts
  - name: E-Factura
    description: Romanian e-invoicing status and configuration
  - name: Supplier Bills
    description: Supplier invoices imported from the ANAF SPV inbox
  - name: VAT Rates
    description: Firm-scoped custom VAT rates on top of the Romanian catalog
  - name: Currencies
    description: Currencies enabled for the firm's bill templates
  - name: Languages
    description: Languages enabled for the firm's bill templates
  - name: Conta
    description: |
      Public ANAF data for the firm's CUI: fiscal profile (TVA scope, RTVAI,
      split TVA, status, e-Factura registration, CAEN) and annual balance
      sheets (cifra de afaceri, profit, capitaluri, datorii, salariați).

      Powered by the public ANAF webservices (no OAuth required for these
      endpoints). The data is cached locally and refreshed on demand via
      the `/sync` actions.
paths:
  /expenses/{id}/attachments:
    post:
      tags:
        - Expenses
      summary: Upload expense attachment
      description: >
        Upload a single PDF or image attachment for an expense. Each expense
        supports only ONE attachment (stored in the 'image' field).


        ## File Requirements

        - Supported types: PDF, JPG, JPEG, PNG

        - Maximum file size: 10MB

        - Only one attachment per expense


        ## Important Notes

        - Uploading a new file will replace any existing attachment

        - Creates a default supplier if the expense doesn't have one

        - There is NO GET endpoint for listing attachments (single attachment
        only)

        - There is NO DELETE endpoint for removing attachments individually
      operationId: uploadExpenseAttachment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Expense CzUid
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: PDF or image file (max 10MB)
      responses:
        '200':
          description: Attachment uploaded successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      filename:
                        type: string
                      url:
                        type: string
                      size:
                        type: integer
                      type:
                        type: string
                        enum:
                          - pdf
                          - image
                  meta:
                    $ref: '#/components/schemas/ResponseMeta'
        '400':
          description: Invalid file or size limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          $ref: '#/components/responses/NotFoundError'
components:
  schemas:
    ResponseMeta:
      type: object
      properties:
        version:
          type: string
          default: v1
        request_id:
          type: string
          format: uuid
          description: Unique request identifier for debugging
        response_time:
          type: string
          example: 23.45ms
    ErrorResponse:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          default: false
        error:
          type: object
          required:
            - message
            - type
            - code
          properties:
            message:
              type: string
              description: Human-readable error message
            type:
              type: string
              enum:
                - api_error
                - authentication_error
                - invalid_request_error
                - rate_limit_error
                - permission_error
                - validation_error
            code:
              type: string
              description: Machine-readable error code
            param:
              type: string
              description: The parameter that caused the error
            doc_url:
              type: string
              format: uri
              description: URL to relevant documentation
        meta:
          $ref: '#/components/schemas/ResponseMeta'
  responses:
    UnauthorizedError:
      description: API key is missing or invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFoundError:
      description: The specified resource was not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Use your API key (sk_live_xxx or sk_test_xxx)

````