# Create a Bank Account Source: https://docs.contazen.ro/api-reference/endpoints/bank-accounts/create POST /bank-accounts Add a new IBAN bank account # Delete a Bank Account Source: https://docs.contazen.ro/api-reference/endpoints/bank-accounts/delete DELETE /bank-accounts/{id} Permanently delete a bank account # List Bank Accounts Source: https://docs.contazen.ro/api-reference/endpoints/bank-accounts/list GET /bank-accounts List all IBAN bank accounts # Update a Bank Account Source: https://docs.contazen.ro/api-reference/endpoints/bank-accounts/update PUT /bank-accounts/{id} Update bank account details # Create Client Source: https://docs.contazen.ro/api-reference/endpoints/clients/create POST /clients Create a new client # Delete Client Source: https://docs.contazen.ro/api-reference/endpoints/clients/delete DELETE /clients/{id} Delete a client # List Clients Source: https://docs.contazen.ro/api-reference/endpoints/clients/list GET /clients Retrieve a paginated list of all clients # Retrieve Client Source: https://docs.contazen.ro/api-reference/endpoints/clients/retrieve GET /clients/{id} Retrieve a specific client by ID # Update Client Source: https://docs.contazen.ro/api-reference/endpoints/clients/update PUT /clients/{id} Update an existing client # Company Lookup Source: https://docs.contazen.ro/api-reference/endpoints/company-lookup/lookup GET /company-lookup Look up a Romanian company by CUI/CIF number # Get E-Factura Status Source: https://docs.contazen.ro/api-reference/endpoints/efactura/status GET /efactura/status Get e-Factura configuration and OAuth connection status # Create Expense Category Source: https://docs.contazen.ro/api-reference/endpoints/expense-categories/create POST /expense-categories Create a new custom expense category for your firm ## Overview The Create Expense Category endpoint allows you to create new custom expense categories for your firm. You can create both root-level categories and subcategories under existing parent categories. **Note**: You can only create custom categories - system default categories are pre-defined and cannot be created via the API. ## Request Body The name of the expense category (must be unique within your firm) Parent category ID. Use `0` for root-level categories, or specify an existing category ID to create a subcategory. Whether the category should be visible and available for use when creating expenses. ## Hierarchy Rules * **Maximum Depth**: Categories can only be 2 levels deep (category → subcategory) * **Parent Validation**: Parent categories must exist and belong to your firm * **Subcategory Limitation**: You cannot create subcategories under existing subcategories * **Circular Reference**: A category cannot be its own parent ## Response The created expense category object Unique identifier for the new category Category name Parent category ID (0 for root categories) Always `false` for custom categories Whether the category is visible and active for use Always `"custom"` for user-created categories Success message confirming the category creation ## Validation Rules * **Name**: Required, must be non-empty after trimming * **Parent Category**: Must exist and belong to your firm (if specified) * **Hierarchy**: Cannot create subcategories under existing subcategories * **Permissions**: Requires write permissions for your firm ```bash Create Root Category theme={null} curl -X POST "https://api.contazen.ro/v1/expense-categories" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Marketing Expenses", "parent_id": 0, "is_visible": true }' ``` ```bash Create Subcategory theme={null} curl -X POST "https://api.contazen.ro/v1/expense-categories" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Digital Advertising", "parent_id": 5, "is_visible": true }' ``` ```javascript Create Root Category theme={null} const categoryData = { name: "Marketing Expenses", parent_id: 0, is_visible: true }; const response = await fetch('https://api.contazen.ro/v1/expense-categories', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(categoryData) }); const data = await response.json(); console.log('Created category:', data.category); ``` ```javascript Create Subcategory theme={null} const subcategoryData = { name: "Digital Advertising", parent_id: 5, // ID of existing parent category is_visible: true }; const response = await fetch('https://api.contazen.ro/v1/expense-categories', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(subcategoryData) }); const data = await response.json(); console.log('Created subcategory:', data.category); ``` ```php Create Root Category theme={null} $categoryData = [ 'name' => 'Marketing Expenses', 'parent_id' => 0, 'is_visible' => true ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => 'https://api.contazen.ro/v1/expense-categories', CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($categoryData), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Created category ID: ' . $data['category']['id']; ``` ```php Create Subcategory theme={null} $subcategoryData = [ 'name' => 'Digital Advertising', 'parent_id' => 5, // ID of existing parent category 'is_visible' => true ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => 'https://api.contazen.ro/v1/expense-categories', CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($subcategoryData), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Created subcategory: ' . $data['category']['name']; ``` ```json 201 - Success theme={null} { "success": true, "category": { "id": 25, "name": "Marketing Expenses", "parent_id": 0, "is_default": false, "is_visible": true, "type": "custom" }, "message": "Category created successfully" } ``` ```json 400 - Validation Error theme={null} { "success": false, "error": { "message": "Name is required", "type": "invalid_request_error", "code": "parameter_missing", "param": "name" } } ``` ```json 400 - Invalid Parent theme={null} { "success": false, "error": { "message": "Parent category not found", "type": "invalid_request_error", "code": "invalid_parent", "param": "parent_id" } } ``` ```json 400 - Invalid Hierarchy theme={null} { "success": false, "error": { "message": "Cannot create subcategory under another subcategory", "type": "invalid_request_error", "code": "invalid_parent_level", "param": "parent_id" } } ``` ```json 403 - Permission Denied theme={null} { "success": false, "error": { "message": "You don't have permission to create categories", "type": "invalid_request_error", "code": "permission_denied" } } ``` # Delete Expense Category Source: https://docs.contazen.ro/api-reference/endpoints/expense-categories/delete DELETE /expense-categories/{id} Delete a custom expense category from your firm ## Overview The Delete Expense Category endpoint allows you to permanently remove custom expense categories that are no longer needed. This operation has strict safety checks to prevent data loss. **Important Restrictions**: * You can only delete **custom categories** that you created * **System default categories** cannot be deleted * Categories must have **no expenses** assigned to them * Categories must have **no subcategories** under them * Categories must belong to your firm ## Path Parameters The unique identifier of the expense category to delete ## Safety Checks Before deleting a category, the system performs several validation checks: 1. **System Protection**: Cannot delete system/default categories 2. **Usage Check**: Cannot delete categories that have expenses assigned 3. **Hierarchy Check**: Cannot delete categories that have subcategories 4. **Ownership**: Can only delete categories belonging to your firm ## Response Success message confirming the category deletion The ID of the deleted category ## Pre-Deletion Checklist Before attempting to delete a category: 1. **Move or delete all expenses** using this category 2. **Delete or reassign all subcategories** under this category 3. **Confirm it's a custom category** (not a system default) ```bash Delete Category theme={null} curl -X DELETE "https://api.contazen.ro/v1/expense-categories/25" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Delete Category theme={null} const categoryId = 25; const response = await fetch(`https://api.contazen.ro/v1/expense-categories/${categoryId}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { console.log('Category deleted:', data.message); } else { console.error('Delete failed:', data.error.message); } ``` ```javascript Safe Delete with Checks theme={null} const categoryId = 25; // First, check if category can be deleted const checkResponse = await fetch(`https://api.contazen.ro/v1/expense-categories/${categoryId}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const categoryData = await checkResponse.json(); if (categoryData.category.type === 'system') { console.error('Cannot delete system category'); return; } // Proceed with deletion const deleteResponse = await fetch(`https://api.contazen.ro/v1/expense-categories/${categoryId}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const result = await deleteResponse.json(); console.log('Delete result:', result); ``` ```php Delete Category theme={null} $categoryId = 25; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expense-categories/{$categoryId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); if ($data['success']) { echo 'Category deleted: ' . $data['message']; } else { echo 'Delete failed: ' . $data['error']['message']; } ``` ```php Safe Delete with Validation theme={null} $categoryId = 25; // First check the category type $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expense-categories/{$categoryId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $categoryData = json_decode($response, true); if ($categoryData['category']['type'] === 'system') { echo 'Error: Cannot delete system category'; exit; } // Proceed with deletion curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expense-categories/{$categoryId}", CURLOPT_CUSTOMREQUEST => 'DELETE' ]); $deleteResponse = curl_exec($curl); $deleteData = json_decode($deleteResponse, true); curl_close($curl); if ($deleteData['success']) { echo 'Successfully deleted category ID: ' . $deleteData['id']; } else { echo 'Delete failed: ' . $deleteData['error']['message']; if (isset($deleteData['error']['expense_count'])) { echo ' (Has ' . $deleteData['error']['expense_count'] . ' expenses)'; } } ``` ```json 200 - Success theme={null} { "success": true, "message": "Category deleted successfully", "id": 25 } ``` ```json 400 - Missing ID theme={null} { "success": false, "error": { "message": "Category ID is required", "type": "invalid_request_error", "code": "parameter_missing", "param": "id" } } ``` ```json 404 - Not Found theme={null} { "success": false, "error": { "message": "Category not found", "type": "invalid_request_error", "code": "resource_missing", "param": "id" } } ``` ```json 403 - System Category theme={null} { "success": false, "error": { "message": "Cannot delete system category", "type": "invalid_request_error", "code": "system_category" } } ``` ```json 400 - Has Expenses theme={null} { "success": false, "error": { "message": "Category has expenses assigned and cannot be deleted", "type": "invalid_request_error", "code": "category_in_use", "expense_count": 15 } } ``` ```json 400 - Has Subcategories theme={null} { "success": false, "error": { "message": "Category has subcategories and cannot be deleted", "type": "invalid_request_error", "code": "category_has_children", "subcategory_count": 3 } } ``` ```json 403 - Permission Denied theme={null} { "success": false, "error": { "message": "You don't have permission to delete categories", "type": "invalid_request_error", "code": "permission_denied" } } ``` ```json 500 - Delete Failed theme={null} { "success": false, "error": { "message": "Failed to delete category", "type": "api_error", "code": "delete_failed" } } ``` ## Best Practices ### Before Deleting Categories 1. **Check Dependencies**: Use the List Expenses API to find expenses using this category 2. **Reassign Expenses**: Move expenses to different categories before deletion 3. **Handle Subcategories**: Delete or reassign all subcategories first 4. **Backup Important Data**: Export expense data if the category contains historical records ### Alternative to Deletion Instead of deleting categories, consider: * **Hide Categories**: Set `is_visible: false` to hide unused categories * **Rename Categories**: Update the name for repurposing * **Archive Approach**: Keep historical categories for reporting consistency # List Expense Categories Source: https://docs.contazen.ro/api-reference/endpoints/expense-categories/list GET /expense-categories Retrieve a list of expense categories for your firm, including both system and custom categories ## Overview The List Expense Categories endpoint allows you to retrieve all expense categories available to your firm. This includes both system-provided default categories and custom categories you've created. Categories are hierarchical - you can have parent categories with subcategories (up to 2 levels deep). ## Query Parameters Filter by parent category ID. Use `0` to get only root-level categories, or specify a parent ID to get its subcategories. When `true`, includes subcategories in the response for each parent category. When `true`, includes system default categories in the response. Set to `false` to get only custom categories. ## Response Array of expense category objects Unique identifier for the category Category name Parent category ID (0 for root categories, null if no parent) Whether this is a system default category Whether the category is visible and active for use Category type: `"system"` for default categories, `"custom"` for user-created categories Array of subcategory objects (only included when `include_children=true`) Total number of categories returned ## Category Hierarchy * **Root Categories**: Categories with `parent_id = 0` * **Subcategories**: Categories with `parent_id > 0` * **Maximum Depth**: 2 levels (category → subcategory) ## System vs Custom Categories * **System Categories**: Pre-defined categories (`type: "system"`, `is_default: true`) * **Custom Categories**: User-created categories (`type: "custom"`, `is_default: false`) * System categories cannot be modified or deleted ```bash cURL theme={null} curl -X GET "https://api.contazen.ro/v1/expense-categories" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash With Filters theme={null} curl -X GET "https://api.contazen.ro/v1/expense-categories?parent_id=0&include_children=true&include_system=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.contazen.ro/v1/expense-categories', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); console.log('Expense categories:', data.categories); ``` ```javascript With Filters theme={null} const params = new URLSearchParams({ parent_id: '0', include_children: 'true', include_system: 'true' }); const response = await fetch(`https://api.contazen.ro/v1/expense-categories?${params}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); console.log('Root categories with children:', data.categories); ``` ```php PHP theme={null} $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => 'https://api.contazen.ro/v1/expense-categories', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Found ' . $data['total'] . ' categories'; ``` ```php With Filters theme={null} $params = http_build_query([ 'parent_id' => 0, 'include_children' => 'true', 'include_system' => 'true' ]); $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => 'https://api.contazen.ro/v1/expense-categories?' . $params, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); foreach ($data['categories'] as $category) { echo $category['name'] . " (ID: " . $category['id'] . ")\n"; } ``` ```json 200 - Success theme={null} { "success": true, "categories": [ { "id": 1, "name": "Office Supplies", "parent_id": 0, "is_default": true, "is_visible": true, "type": "system" }, { "id": 2, "name": "Travel & Transportation", "parent_id": 0, "is_default": true, "is_visible": true, "type": "system", "children": [ { "id": 15, "name": "Fuel", "parent_id": 2, "is_default": false, "is_visible": true, "type": "custom" }, { "id": 16, "name": "Public Transport", "parent_id": 2, "is_default": false, "is_visible": true, "type": "custom" } ] }, { "id": 3, "name": "Marketing", "parent_id": 0, "is_default": false, "is_visible": true, "type": "custom" } ], "total": 3 } ``` ```json 401 - Unauthorized theme={null} { "success": false, "error": { "message": "Unauthorized. API key is required.", "type": "authentication_error", "code": "api_key_required" } } ``` # Retrieve Expense Category Source: https://docs.contazen.ro/api-reference/endpoints/expense-categories/retrieve GET /expense-categories/{id} Get details of a specific expense category by its ID ## Overview The Retrieve Expense Category endpoint allows you to get detailed information about a specific expense category by its ID. This works for both system default categories and your custom categories. ## Path Parameters The unique identifier of the expense category to retrieve ## Query Parameters When `true`, includes all subcategories under this category in the response (only applicable for parent categories) ## Response The expense category object with full details Unique identifier for the category Category name Parent category ID (0 for root categories, null if no parent) Whether this is a system default category Whether the category is visible and active for use Category type: `"system"` for default categories, `"custom"` for user-created categories Array of subcategory objects (only included when `include_children=true` and category has subcategories) Child category ID Child category name Parent category ID Whether child is a system category Whether child category is visible Child category type ## Access Control * You can only retrieve categories that belong to your firm * Both system and custom categories are accessible * Category must exist and not be deleted ```bash Basic Retrieval theme={null} curl -X GET "https://api.contazen.ro/v1/expense-categories/5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash With Children theme={null} curl -X GET "https://api.contazen.ro/v1/expense-categories/5?include_children=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Basic Retrieval theme={null} const categoryId = 5; const response = await fetch(`https://api.contazen.ro/v1/expense-categories/${categoryId}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); console.log('Category details:', data.category); ``` ```javascript With Children theme={null} const categoryId = 5; const params = new URLSearchParams({ include_children: 'true' }); const response = await fetch(`https://api.contazen.ro/v1/expense-categories/${categoryId}?${params}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); console.log('Category with children:', data.category); if (data.category.children) { console.log('Subcategories:', data.category.children.length); } ``` ```php Basic Retrieval theme={null} $categoryId = 5; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expense-categories/{$categoryId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Category: ' . $data['category']['name']; echo ' (Type: ' . $data['category']['type'] . ')'; ``` ```php With Children theme={null} $categoryId = 5; $params = http_build_query([ 'include_children' => 'true' ]); $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expense-categories/{$categoryId}?{$params}", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Category: ' . $data['category']['name'] . "\n"; if (isset($data['category']['children'])) { echo 'Subcategories: ' . count($data['category']['children']) . "\n"; foreach ($data['category']['children'] as $child) { echo ' - ' . $child['name'] . " (ID: {$child['id']})\n"; } } ``` ```json 200 - Success (Root Category) theme={null} { "success": true, "category": { "id": 5, "name": "Marketing Expenses", "parent_id": 0, "is_default": false, "is_visible": true, "type": "custom" } } ``` ```json 200 - Success (With Children) theme={null} { "success": true, "category": { "id": 2, "name": "Travel & Transportation", "parent_id": 0, "is_default": true, "is_visible": true, "type": "system", "children": [ { "id": 15, "name": "Fuel", "parent_id": 2, "is_default": false, "is_visible": true, "type": "custom" }, { "id": 16, "name": "Public Transport", "parent_id": 2, "is_default": false, "is_visible": true, "type": "custom" }, { "id": 17, "name": "Hotel Accommodation", "parent_id": 2, "is_default": false, "is_visible": true, "type": "custom" } ] } } ``` ```json 200 - Success (Subcategory) theme={null} { "success": true, "category": { "id": 15, "name": "Fuel", "parent_id": 2, "is_default": false, "is_visible": true, "type": "custom" } } ``` ```json 400 - Missing ID theme={null} { "success": false, "error": { "message": "Category ID is required", "type": "invalid_request_error", "code": "parameter_missing", "param": "id" } } ``` ```json 404 - Not Found theme={null} { "success": false, "error": { "message": "Category not found", "type": "invalid_request_error", "code": "resource_missing", "param": "id" } } ``` ```json 401 - Unauthorized theme={null} { "success": false, "error": { "message": "Unauthorized. API key is required.", "type": "authentication_error", "code": "api_key_required" } } ``` # Update Expense Category Source: https://docs.contazen.ro/api-reference/endpoints/expense-categories/update PUT /expense-categories/{id} Update an existing custom expense category ## Overview The Update Expense Category endpoint allows you to modify existing custom expense categories. You can update the name, parent category, and visibility settings. **Important Limitations**: * You can only update **custom categories** that you created * **System default categories** cannot be modified via the API * Categories that belong to your firm only ## Path Parameters The unique identifier of the expense category to update ## Request Body All parameters are optional - only include the fields you want to update. The new name for the expense category New parent category ID. Use `0` to make it a root-level category, or specify an existing category ID to move it under that parent. Whether the category should be visible and available for use when creating expenses ## Validation Rules * **Name**: Must be non-empty if provided * **Parent Category**: Must exist and belong to your firm (if specified) * **Hierarchy**: Cannot create circular references or exceed 2-level depth * **Self-Reference**: A category cannot be its own parent * **System Categories**: Cannot update categories with `type: "system"` ## Hierarchy Constraints * **Maximum Depth**: Categories can only be 2 levels deep (category → subcategory) * **Parent Validation**: Parent must be a root-level category (parent\_id = 0) * **Circular Reference**: Prevents categories from referencing themselves or creating loops ## Response The updated expense category object Unique identifier for the category Updated category name Updated parent category ID (0 for root categories) Always `false` for custom categories Updated visibility setting Always `"custom"` for user-created categories Success message confirming the category update ```bash Update Name Only theme={null} curl -X PUT "https://api.contazen.ro/v1/expense-categories/25" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Digital Marketing" }' ``` ```bash Change Parent Category theme={null} curl -X PUT "https://api.contazen.ro/v1/expense-categories/25" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "parent_id": 5 }' ``` ```bash Multiple Fields Update theme={null} curl -X PUT "https://api.contazen.ro/v1/expense-categories/25" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Marketing & Advertising", "parent_id": 0, "is_visible": true }' ``` ```javascript Update Name Only theme={null} const categoryId = 25; const updateData = { name: "Digital Marketing" }; const response = await fetch(`https://api.contazen.ro/v1/expense-categories/${categoryId}`, { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await response.json(); console.log('Updated category:', data.category); ``` ```javascript Move to Different Parent theme={null} const categoryId = 25; const updateData = { parent_id: 5 // Move under category ID 5 }; const response = await fetch(`https://api.contazen.ro/v1/expense-categories/${categoryId}`, { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await response.json(); console.log('Category moved to parent:', data.category.parent_id); ``` ```javascript Complete Update theme={null} const categoryId = 25; const updateData = { name: "Marketing & Advertising", parent_id: 0, // Make it root level is_visible: true }; const response = await fetch(`https://api.contazen.ro/v1/expense-categories/${categoryId}`, { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await response.json(); console.log('Updated category:', data.category); ``` ```php Update Name Only theme={null} $categoryId = 25; $updateData = [ 'name' => 'Digital Marketing' ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expense-categories/{$categoryId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_POSTFIELDS => json_encode($updateData), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Updated category: ' . $data['category']['name']; ``` ```php Change Parent and Visibility theme={null} $categoryId = 25; $updateData = [ 'parent_id' => 5, 'is_visible' => false ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expense-categories/{$categoryId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_POSTFIELDS => json_encode($updateData), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Category moved to parent ID: ' . $data['category']['parent_id']; ``` ```json 200 - Success theme={null} { "success": true, "category": { "id": 25, "name": "Digital Marketing", "parent_id": 0, "is_default": false, "is_visible": true, "type": "custom" }, "message": "Category updated successfully" } ``` ```json 400 - Missing ID theme={null} { "success": false, "error": { "message": "Category ID is required", "type": "invalid_request_error", "code": "parameter_missing", "param": "id" } } ``` ```json 404 - Not Found theme={null} { "success": false, "error": { "message": "Category not found", "type": "invalid_request_error", "code": "resource_missing", "param": "id" } } ``` ```json 403 - System Category theme={null} { "success": false, "error": { "message": "Cannot update system category", "type": "invalid_request_error", "code": "system_category" } } ``` ```json 400 - Self Reference theme={null} { "success": false, "error": { "message": "Category cannot be its own parent", "type": "invalid_request_error", "code": "invalid_parent", "param": "parent_id" } } ``` ```json 400 - Invalid Parent theme={null} { "success": false, "error": { "message": "Parent category not found", "type": "invalid_request_error", "code": "invalid_parent", "param": "parent_id" } } ``` ```json 400 - Invalid Hierarchy theme={null} { "success": false, "error": { "message": "Cannot create subcategory under another subcategory", "type": "invalid_request_error", "code": "invalid_parent_level", "param": "parent_id" } } ``` ```json 400 - Circular Reference theme={null} { "success": false, "error": { "message": "Circular reference detected", "type": "invalid_request_error", "code": "circular_reference", "param": "parent_id" } } ``` ```json 403 - Permission Denied theme={null} { "success": false, "error": { "message": "You don't have permission to update categories", "type": "invalid_request_error", "code": "permission_denied" } } ``` # AI-categorize expenses Source: https://docs.contazen.ro/api-reference/endpoints/expenses/ai-categorize-batch POST /expenses/ai-categorize-batch Sweep every uncategorized expense in the firm and assign a category from the existing list using AI classification. ## When to use After bulk-importing expenses (OCR, supplier sync, e-Factura SPV) the `category_id` is often `null`. This endpoint sweeps every such row and assigns a category from the firm's existing list — system defaults plus custom categories, merged. Existing categorizations are never overwritten. Call repeatedly until `remaining = 0`. Each call processes up to `cap` expenses (a server-side constant returned in the response), so a large backlog can be drained in controlled passes rather than a single long-running request. ## Permissions Requires `write` permission on expenses. ## Request Optional cap on the number of expenses to process this call. The server enforces its own ceiling (`BATCH_MAX_PER_CALL`), so very large values are clamped silently. Omit for "process up to the server cap." ## Response ```json theme={null} { "data": { "processed": 50, "categorized": 47, "skipped": 2, "errors": 1, "remaining": 1242, "batch_size": 10, "cap": 50 } } ``` | Field | Meaning | | ------------- | ------------------------------------------------------------------------------------------------- | | `processed` | Expenses inspected this call. | | `categorized` | Expenses that received a `category_id`. | | `skipped` | Classifier declined to assign a category (low confidence or no obvious match in the firm's list). | | `errors` | Per-row failures — logged server-side, retryable on next call. | | `remaining` | Uncategorized expenses still queued. **Stop when this hits 0.** | | `batch_size` | Expenses per classifier call (informational). | | `cap` | Hard ceiling per invocation (informational). | ## Driver pattern ```js theme={null} async function categorizeAll(client) { while (true) { const { data } = await client.post('/expenses/ai-categorize-batch'); if (data.remaining === 0) return; // Optional: brief pause between passes. await new Promise(r => setTimeout(r, 500)); } } ``` ## Errors * **401** — missing / invalid bearer token * **403** — API key lacks `write` permission on expenses * **502 `ai_unavailable`** — classifier call failed; safe to retry * **503 `ai_not_configured`** — AI categorization is not enabled on this deployment # Upload Attachment Source: https://docs.contazen.ro/api-reference/endpoints/expenses/attachment POST /expenses/{id}/attachments 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. **Security Note**: Files undergo multiple validation checks including MIME type verification and content validation to ensure security. Each expense supports only one attachment. Uploading a new file will replace the existing attachment. ## Path Parameters The CzUid of the expense to attach the file to ## Request Body This endpoint uses `multipart/form-data` encoding for file uploads: The file to upload. See supported file types below. ## Supported File Types * `pdf` - Portable Document Format (most common for invoices/receipts) * `jpg`, `jpeg` - JPEG images * `png` - PNG images Files are validated at multiple levels: 1. Extension validation 2. MIME type verification 3. Content header validation (e.g., PDFs must start with %PDF) ## File Size Limits 10 MB per file ## Response Information about the uploaded attachment Unique CzUid for the attachment Original filename as uploaded File size in bytes Description provided during upload (may be empty) When the attachment was uploaded URL for downloading the attachment (authentication required) Success message confirming the upload ## 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; }; ``` ```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; } ``` ```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" } } ``` ## 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 # Check for duplicate expense Source: https://docs.contazen.ro/api-reference/endpoints/expenses/check-duplicate POST /expenses/check-duplicate Probe the firm's books for an existing expense that would collide with a prospective payload. Useful before posting a scanned receipt. ## When to use Call this **before** [`POST /expenses`](./create) when the source is unattended (OCR, recurring import, supplier sync). The response tells the UI whether to surface a confirmation dialog ("an expense from this supplier with this reference already exists — continue?") instead of silently posting a duplicate. The endpoint never modifies state. Match resolution mirrors the server-side detector used by the OCR + import flows. ## Request Send either `supplier_id` (preferred — Romanian companies often share name prefixes) or `supplier_data` with the resolved name/CUI from the receipt. The endpoint **never auto-creates** a supplier; if the supplier\_data block doesn't match an existing row, the supplier match is treated as missing and only `reference`-based matches are considered. CzUid of the matched supplier. Preferred when known. Fallback when `supplier_id` is unknown. Resolved by CUI first (with `RO` prefix stripped), then by exact name match. * `name` — supplier name as printed on the document * `cui` — supplier CUI / VAT number, with or without `RO` prefix Document number (factura, chitanță, bon fiscal). Used for both exact and `likely` cross-supplier matches. Issue date in `YYYY-MM-DD`. Combined with supplier + amount for `strong` matches. Net amount (subtotal). Compared with ±0.02 tolerance to forgive rounding. Three-letter ISO code. Defaults to `RON`. ## Match types | `match_type` | Trigger | | ------------ | ----------------------------------------------------------------- | | `exact` | Same supplier + same reference | | `strong` | Same supplier + same date + amount within ±0.02 | | `likely` | Same reference under a different supplier (review before posting) | `exact` and `strong` should usually block; `likely` deserves a confirmation but isn't necessarily a true duplicate (e.g. invoice numbers reset per fiscal year across suppliers). ## Response `duplicate` is `null` when no candidate matches. When populated, the `id` is the CzUid of the existing expense — fetch it via [`GET /expenses/{id}`](./retrieve) for the full record. ```json theme={null} { "data": { "duplicate": { "id": "Q9hjAB", "reference": "65776", "date": "2026-04-25", "amount": 147.53, "currency": "RON", "match_type": "exact" } } } ``` ## Errors * **400** — invalid JSON body or missing required fields when neither `supplier_id` nor `supplier_data.name` is provided * **401** — missing / invalid bearer token * **403** — API key lacks `read` permission on expenses # Create Expense Source: https://docs.contazen.ro/api-reference/endpoints/expenses/create POST /expenses Create a new expense record with minimal required fields and smart defaults for optional fields ## Overview The Create Expense endpoint allows you to add a new expense to your firm's records with just 4 required fields. The API provides smart defaults for all optional fields, making expense creation quick and simple. Three save shapes are supported: * **Flat** — send `amount` + `vat` for a single-line, single-rate expense. Simplest path; covers most utility bills and single-rate receipts. * **Itemized** — send `items[]` when the receipt has multiple line items, each carrying its own rate. The server derives `amount` / `vat` / `vat_breakdown` from the lines. See [Expense Lines & VAT Breakdown](/concepts/expense-lines) for the full model. * **Mix VAT (manual)** — send `vat: "mix"` + `amount` (net subtotal) + `amount_vat_manual` (total VAT in RON) when the receipt mixes multiple VAT rates and per-product detail isn't reliable (typical for noisy bon fiscal OCR). Optionally include `vat_breakdown` so per-rate aggregates persist. The server reconstructs `total = amount + amount_vat_manual` exactly — no derivation, no drift. `amount` + `vat` are required on the flat and mix paths. On the itemized path they're ignored in favor of line-derived totals. ## Duplicate Detection Before persisting, the API scans your firm's existing expenses for a likely duplicate (same supplier + reference, or same supplier + date + amount within a small tolerance). If a match is found, the request is rejected with **HTTP 409** and a payload describing the existing expense — no row is created. To override and force creation anyway, send `?force=1` on the request URL. Clients that want to surface a confirmation dialog to the user before POSTing can pre-check with [`POST /expenses/check-duplicate`](./check-duplicate) using the same supplier / reference / amount fields; it returns the same `duplicate` shape without writing anything. ## Query Parameters Set to `1` to bypass duplicate detection and persist the expense even when a likely duplicate exists. ## Request Body ### Required Fields The CzUid of the supplier for this expense. Alternative: omit and send `supplier_data` to auto-create. Expense date in YYYY-MM-DD format The expense amount (positive number). **Required** on the flat path; ignored when `items` is sent. Either a numeric VAT percentage (`0`, `5`, `9`, `11`, `19`, `21`) **or** the literal string `"mix"` for a multi-rate receipt where the caller supplies the total VAT manually (see [Mix VAT mode](#mix-vat-mode)). **Required** on the flat and mix paths; when `items` is sent the server sets this to the dominant rate. Total VAT amount in receipt currency. **Required when `vat = "mix"`**, ignored otherwise. Must be `>= 0`. The server stores `amount_wvat = amount`, `amount_vat = amount_vat_manual`, `amount_total = amount_wvat + amount_vat_manual` exactly — no rounding inference. ### Optional Fields with Defaults Currency code (e.g., RON, EUR, USD) * Defaults to RON if not provided Payment due date in YYYY-MM-DD format * Defaults to 30 days from today if not provided The ID of the expense category * Optional, defaults to 0 (uncategorized) Reference or invoice number for the expense Detailed description of the expense Whether the amount includes VAT (true) or excludes VAT (false) * Defaults to false if not provided Whether the expense is already paid * Defaults to false if not provided Payment date * Defaults to today if is\_paid is true and paid\_date not provided * Optional even when is\_paid is true Payment type ID: * Defaults to 1 (Cash) if is\_paid is true and payment\_type not provided * Optional even when is\_paid is true * `1` - Cash * `2` - Bank transfer * `3` - Card * `4` - Check * `5` - Promissory note * `6` - Other * `7` - Compensation Whether this expense involves currency exchange Target currency for exchange (required if with\_currency\_exchange is true) Exchange rate (required if with\_currency\_exchange is true) ### Itemized Lines Array of line rows. When present and non-empty, the server persists real line records and derives `amount` / `vat` / `vat_breakdown` from them — the header-level `amount` and `vat` request fields are ignored. Each item is an object with: * `name` (string, required) — display name for the line * `quantity` (number, required, > 0) — supports fractional quantities * `unit_price` (number, required) — **net** unit price (excluding VAT). Negative values are accepted for discount rows (e.g. a Romanian bon fiscal `DISCOUNT 4,40-A` rebate); the sign is preserved through to the line's gross. * `vat_rate` (integer, required) — one of 0, 5, 9, 11, 19, 21 * `description` (string) — optional * `unit_code` (string) — optional UN/ECE code (e.g. `H87`, `KGM`) * `category_id` (string) — optional per-line category override * `product_id` (string) — optional link to firm's product catalog See [Expense Lines & VAT Breakdown](/concepts/expense-lines) for rounding rules and multi-VAT handling. Per-rate breakdown for multi-VAT receipts saved on the flat or mix paths (no `items`). Each entry: `{ rate, net, vat_amount, gross }`. The server stores the array as JSON on the row when two or more distinct rates are present. Ignored when `items` is supplied — in that case the breakdown is derived from the lines. On the **mix path**, supplying `vat_breakdown` with at least two rate tranches makes the server synthesize one persisted line per rate group (`Cheltuială (21%)`, `Cheltuială (11%)`, `Cheltuială (0%)`) so reports and exports keep accurate per-rate totals. Without a breakdown, mix mode falls back to a single line with a synthetic `vat_rate = (amount_vat_manual / amount) × 100`. Auto-create a supplier when `supplier_id` is not supplied. Useful when ingesting supplier data from external sources where the firm hasn't billed with the supplier before. Shape: `{ name: string, cui?: string }`. If a supplier with the same CUI (or name when CUI is missing) already exists on the firm, it's reused instead of creating a duplicate. ## Response Returns the created expense object with all computed fields including VAT calculations and currency conversions. The created expense object with all details Unique CzUid identifier for the expense Reference number or invoice number Expense description Original expense amounts with VAT calculations Accounting amounts in firm's base currency VAT percentage Whether amount includes VAT Expense date Payment due date Payment status Expense status Supplier CzUid Category ID Currency exchange details (if applicable) Creation timestamp Last update timestamp Success message confirming the expense creation ## Validation Rules * **Supplier**: Must exist and be owned by your firm (required) * **Amount**: Must be a positive number (required) * **VAT**: Must be one of: 0, 5, 9, 11, 19, 21, or the string `"mix"` (required) * **amount\_vat\_manual**: Required (>= 0) when `vat = "mix"`, ignored otherwise * **Date**: Must be in YYYY-MM-DD format (required) * **Category**: If provided, must be a valid expense category for your firm * **Currency**: If provided, must be a valid 3-letter ISO code * **Dates**: All dates must be in YYYY-MM-DD format * **Payment Info**: paid\_date and payment\_type are optional with smart defaults ## VAT Calculation The system automatically calculates VAT amounts based on your input: * If `vat = "mix"`: `amount` is treated as the net subtotal, `amount_vat_manual` is the total VAT, and `total = amount + amount_vat_manual` exactly. `with_vat` is forced to `false`. See [Mix VAT mode](#mix-vat-mode) below. * If `with_vat` is **true**: The amount includes VAT, and VAT is extracted from the total * If `with_vat` is **false**: The amount excludes VAT, and VAT is added to get the total ## Mix VAT mode Romanian fiscal receipts (bon fiscal) routinely mix multiple VAT rates on a single document — e.g. food items at 11% alongside non-food at 21% and packaging deposits at 0%. When per-product detail isn't reliable, mix mode lets the caller record the printed totals exactly without inferring a single rate. **Request shape:** ```json theme={null} { "supplier_id": "Q9hjAB", "date": "2026-04-25", "amount": 147.53, "vat": "mix", "amount_vat_manual": 24.15, "currency": "RON", "vat_breakdown": [ { "rate": 21, "net": 80.29, "vat_amount": 16.86, "gross": 97.15 }, { "rate": 11, "net": 66.27, "vat_amount": 7.29, "gross": 73.56 }, { "rate": 0, "net": 0.97, "vat_amount": 0.00, "gross": 0.97 } ] } ``` **What gets stored:** * `amount_wvat = 147.53`, `amount_vat = 24.15`, `amount_total = 171.68` (exact, no derivation) * `vat_breakdown` JSON when two or more rate entries are supplied; per-rate aggregates persist for reports. Presence of this JSON with ≥2 entries is what marks the row as multi-rate — the legacy `vat_percent` field is filled with the dominant rate for back-compat with single-rate consumers. * Without `vat_breakdown`, the receipt is stored as a single-rate row with `vat_percent = (amount_vat_manual / amount) × 100` **When to use mix vs itemized:** * Use **itemized** (`items[]`) when you have reliable per-product names, quantities, unit prices, and rates. * Use **mix** (`vat = "mix"`) when only the printed totals are reliable. ## Currency Exchange If `with_currency_exchange` is enabled: * Original amounts are kept in the expense currency * Account amounts are calculated using the provided exchange rate * Exchange details are stored for audit purposes ```bash Minimal (Required Fields Only) theme={null} curl -X POST "https://api.contazen.ro/v1/expenses" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "supplier_id": "sup_xyz789", "amount": 100.00, "vat": 21, "date": "2024-01-15" }' ``` ```bash With Optional Fields theme={null} curl -X POST "https://api.contazen.ro/v1/expenses" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "supplier_id": "sup_xyz789", "amount": 238.00, "vat": 21, "date": "2024-01-15", "reference": "INV-2024-001", "description": "Office supplies purchase", "category_id": 1, "with_vat": true, "currency": "EUR", "due_date": "2024-02-15", "is_paid": true }' ``` ```bash Itemized (multi-VAT receipt) theme={null} curl -X POST "https://api.contazen.ro/v1/expenses" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "supplier_id": "sup_xyz789", "date": "2026-02-19", "currency": "RON", "items": [ { "name": "Meniul zilei", "quantity": 3, "unit_price": 28.74, "vat_rate": 11 }, { "name": "Caserolă meniu", "quantity": 6, "unit_price": 1.24, "vat_rate": 21 } ] }' ``` ```javascript JavaScript (Minimal) theme={null} // Minimal required fields only const minimalExpense = { supplier_id: "sup_xyz789", amount: 100.00, vat: 21, date: "2024-01-15" }; const response = await fetch('https://api.contazen.ro/v1/expenses', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(minimalExpense) }); const data = await response.json(); console.log('Created expense:', data.expense); ``` ```javascript JavaScript (Full) theme={null} // With optional fields const fullExpense = { supplier_id: "sup_xyz789", amount: 238.00, vat: 21, date: "2024-01-15", reference: "INV-2024-001", description: "Office supplies purchase", category_id: 1, with_vat: true, currency: "EUR", due_date: "2024-02-15", is_paid: true, paid_date: "2024-01-16", payment_type: 2 }; const response = await fetch('https://api.contazen.ro/v1/expenses', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(fullExpense) }); const data = await response.json(); console.log('Created expense:', data.expense); ``` ```php PHP (Minimal) theme={null} // Minimal required fields only $minimalExpense = [ 'supplier_id' => 'sup_xyz789', 'amount' => 100.00, 'vat' => 21, 'date' => '2024-01-15' ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => 'https://api.contazen.ro/v1/expenses', CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($minimalExpense), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Created expense ID: ' . $data['expense']['id']; ``` ```json 201 - Success (Minimal) theme={null} { "success": true, "expense": { "id": "exp_abc123", "reference": null, "description": null, "amount": { "total": "121.00", "without_vat": "100.00", "vat": "21.00", "currency": "RON" }, "account_amount": { "total": "121.00", "without_vat": "100.00", "vat": "21.00", "currency": "RON" }, "vat_percent": 21, "with_vat": false, "date": "2024-01-15", "due_date": "2024-02-14", "paid_date": null, "is_paid": false, "payment_type": null, "status": "unpaid", "supplier_id": "sup_xyz789", "category_id": 0, "user_id": 1, "created_at": "2024-01-15 10:30:00", "updated_at": "2024-01-15 10:30:00" }, "message": "Expense created successfully" } ``` ```json 400 - Validation Error theme={null} { "success": false, "error": { "message": "Validation failed", "type": "invalid_request_error", "code": "validation_error", "fields": { "supplier_id": "Supplier is required", "amount": "Amount must be greater than 0" } } } ``` ```json 409 - Duplicate Expense theme={null} { "success": false, "error": { "message": "Duplicate expense detected", "type": "invalid_request_error", "code": "duplicate_expense", "duplicate": { "id": "d8kZ3Qa", "reference": "FX 26101044850", "date": "2026-02-24", "amount": 584.39, "currency": "RON", "match_type": "supplier_reference" } } } ``` # Delete Expense Source: https://docs.contazen.ro/api-reference/endpoints/expenses/delete DELETE /expenses/{id} Delete an expense record with proper validation and soft delete functionality ## Overview The Delete Expense endpoint allows you to remove an expense from your records. The system performs a soft delete, which means the expense is marked as deleted but preserved in the database for audit and compliance purposes. Deletion is permanent from the user perspective and cannot be undone through the API. Ensure you really want to delete the expense before calling this endpoint. ## Path Parameters The CzUid of the expense to delete ## Business Rules The system will validate that the expense can be safely deleted: * **Accounting periods**: Expenses in closed accounting periods may not be deletable * **Integration locks**: Expenses that have been synchronized with external systems may be protected * **Audit requirements**: Some expenses may be required to be kept for compliance reasons If an expense cannot be deleted due to business rules, the API will return an appropriate error message. ## Response Confirmation message that the expense has been deleted The CzUid of the deleted expense for reference ## Soft Delete Behavior When an expense is deleted: 1. **Status Update**: The expense is marked as deleted in the database 2. **Timestamp**: A deletion timestamp is recorded 3. **List Exclusion**: The expense will no longer appear in standard list endpoints 4. **Audit Trail**: The deletion is logged for audit purposes 5. **Data Preservation**: All expense data is preserved for compliance and reporting ## Impact on Related Data Deleting an expense affects related data: * **Statistics**: The expense is excluded from future statistics calculations * **Totals**: List totals will be recalculated without this expense * **Reports**: The expense will not appear in standard reports * **Attachments**: Any attached files remain in the system but are marked as orphaned ```bash cURL theme={null} curl -X DELETE "https://api.contazen.ro/v1/expenses/exp_abc123" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ```javascript JavaScript theme={null} const expenseId = 'exp_abc123'; const response = await fetch(`https://api.contazen.ro/v1/expenses/${expenseId}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { console.log('Expense deleted successfully:', data.id); } else { console.error('Failed to delete expense:', data.error.message); } ``` ```php PHP theme={null} $expenseId = 'exp_abc123'; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expenses/{$expenseId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); if ($data['success']) { echo 'Expense deleted successfully: ' . $data['id']; } else { echo 'Error: ' . $data['error']['message']; } ``` ```json 200 - Success theme={null} { "success": true, "message": "Expense deleted successfully", "id": "exp_abc123" } ``` ```json 404 - Not Found theme={null} { "success": false, "error": { "message": "Expense not found", "type": "invalid_request_error", "code": "resource_missing", "param": "id" } } ``` ```json 403 - Cannot Delete theme={null} { "success": false, "error": { "message": "Expense cannot be deleted due to business rules", "type": "invalid_request_error", "code": "delete_restricted", "details": "This expense is in a closed accounting period and cannot be modified or deleted" } } ``` ```json 500 - Delete Failed theme={null} { "success": false, "error": { "message": "Failed to delete expense", "type": "api_error", "code": "delete_failed", "details": "An internal error occurred while processing the deletion" } } ``` ## Best Practices ### Before Deleting 1. **Verify the expense**: Use the [Retrieve Expense](/api-reference/endpoints/expenses/retrieve) endpoint to confirm you have the correct expense 2. **Check dependencies**: Ensure the expense is not referenced by other records 3. **Consider alternatives**: Sometimes updating an expense to mark it as cancelled is preferable to deletion ### After Deletion 1. **Update local records**: Remove the expense from any local caches or displays 2. **Refresh totals**: Update any calculated totals in your application 3. **Notify users**: Inform relevant users that the expense has been removed ### Error Handling Always handle potential errors gracefully: * **404 errors**: The expense may have already been deleted or the ID is incorrect * **403 errors**: Business rules prevent deletion - inform the user why * **500 errors**: Technical issues - retry or contact support ### Alternative Actions Consider these alternatives to deletion: * **Void the expense**: Mark it as cancelled while preserving the record * **Reduce amount to zero**: Keep the expense but remove its financial impact * **Archive**: Move to an archived status instead of deleting # Download e-Factura PDF Source: https://docs.contazen.ro/api-reference/endpoints/expenses/efactura-pdf GET /expenses/{id}/efactura-pdf Stream the ANAF e-Factura PDF for an expense imported from the SPV inbox ## Overview Returns the e-Factura PDF for an expense that was imported from the ANAF e-Factura SPV inbox. The PDF is rendered on demand via the ANAF public validator the first time it is requested and cached on disk for subsequent calls. This endpoint requires the same Bearer authentication as the rest of the API. Use the `efactura.pdf_url` field from [Retrieve Expense](/api-reference/endpoints/expenses/retrieve) to discover the URL — it is only present when `efactura.xml_available` is `true`. ## Path Parameters The CzUid of the expense ## Response Returns `application/pdf` binary content with `Content-Disposition: inline`. ## Error Responses Returned when the expense was not imported from SPV (`efactura_not_available`) or its XML file is missing on disk (`efactura_xml_missing`). The ANAF validator failed to convert the XML to PDF (`efactura_pdf_generation_failed`). ```bash cURL theme={null} curl -X GET "https://api.contazen.ro/v1/expenses/exp_abc123/efactura-pdf" \ -H "Authorization: Bearer YOUR_API_KEY" \ -o efactura.pdf ``` ```javascript JavaScript theme={null} const expenseId = 'exp_abc123'; const response = await fetch(`https://api.contazen.ro/v1/expenses/${expenseId}/efactura-pdf`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const blob = await response.blob(); ``` ```php PHP theme={null} $expenseId = 'exp_abc123'; $ch = curl_init("https://api.contazen.ro/v1/expenses/{$expenseId}/efactura-pdf"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer YOUR_API_KEY'], ]); $pdf = curl_exec($ch); curl_close($ch); file_put_contents('efactura.pdf', $pdf); ``` # Export Expenses Source: https://docs.contazen.ro/api-reference/endpoints/expenses/export GET /expenses/export Export expense data to CSV or PDF format with all filtering options available ## Overview The Export Expenses endpoint allows you to download your expense data in structured formats for external analysis, reporting, or archival purposes. You can export all expenses or use the same filtering options available in the List Expenses endpoint. This endpoint returns file data directly rather than JSON, so the response format differs from other API endpoints. ## Query Parameters Export format: * `csv` - Comma-separated values file (Excel compatible) * `pdf` - Portable Document Format (currently not implemented) ### Filtering Parameters All filtering parameters from the [List Expenses](/api-reference/endpoints/expenses/list) endpoint are supported: Filter by supplier CzUid Filter by expense category ID Filter by payment status (paid, unpaid, overdue, registered) Filter by currency code Filter by payment type ID (1-7) Start date for filtering expenses End date for filtering expenses Sort field (created\_at, date, due\_date, amount) Sort order (asc, desc) ## Response Formats ### CSV Export (`format=csv`) The CSV export includes the following columns: | Column | Description | | ------------------ | ------------------------------------------ | | Reference | Reference number or invoice number | | Date | Expense date | | Due Date | Payment due date | | Supplier | Supplier name | | Category | Expense category name | | Description | Expense description | | Amount | Total amount | | Currency | Currency code | | VAT % | VAT percentage | | Amount without VAT | Amount excluding VAT | | VAT Amount | VAT amount | | Status | Payment status (Paid/Unpaid) | | Payment Date | Date when expense was paid (if applicable) | | Payment Type | Payment method name (if paid) | **File characteristics:** * UTF-8 encoding with BOM for Excel compatibility * Comma-separated values * Headers included in first row * Filename format: `expenses_YYYY-MM-DD.csv` ### PDF Export (`format=pdf`) PDF export is currently not implemented. The endpoint will return a JSON error message indicating this feature is not yet available. ## Response Headers ### CSV Export ``` Content-Type: text/csv; charset=utf-8 Content-Disposition: attachment; filename="expenses_2024-01-20.csv" ``` ### Error Responses ``` Content-Type: application/json ``` ## Pagination Behavior Unlike the List Expenses endpoint, the export endpoint **does not use pagination**. It will export all expenses matching your filter criteria in a single file. Be mindful when exporting large datasets. ## Use Cases ### Accounting Integration Export expenses for import into accounting software: ```javascript theme={null} const exportUrl = 'https://api.contazen.ro/v1/expenses/export?format=csv&start_date=2024-01-01&end_date=2024-01-31'; // Download and import into accounting system ``` ### Monthly Reporting Generate monthly expense reports: ```javascript theme={null} const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM const exportUrl = `https://api.contazen.ro/v1/expenses/export?format=csv&start_date=${currentMonth}-01&end_date=${currentMonth}-31`; ``` ### Audit Preparation Export all expenses for a specific period for auditing: ```javascript theme={null} const auditExport = 'https://api.contazen.ro/v1/expenses/export?format=csv&start_date=2024-01-01&end_date=2024-12-31&sort=date&order=asc'; ``` ### Category Analysis Export expenses by category for detailed analysis: ```javascript theme={null} const categoryExport = 'https://api.contazen.ro/v1/expenses/export?format=csv&category_id=1'; ``` ```bash cURL theme={null} # Download CSV export with filtering curl -X GET "https://api.contazen.ro/v1/expenses/export?format=csv&start_date=2024-01-01&end_date=2024-01-31&status=paid" \ -H "Authorization: Bearer YOUR_API_KEY" \ --output expenses_january_2024.csv # Download all unpaid expenses curl -X GET "https://api.contazen.ro/v1/expenses/export?format=csv&status=unpaid" \ -H "Authorization: Bearer YOUR_API_KEY" \ --output unpaid_expenses.csv ``` ```javascript JavaScript theme={null} const exportExpenses = async (filters = {}) => { // Build query parameters const params = new URLSearchParams({ format: 'csv', ...filters }); const response = await fetch(`https://api.contazen.ro/v1/expenses/export?${params}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); if (response.ok) { // Get filename from Content-Disposition header const contentDisposition = response.headers.get('Content-Disposition'); const filename = contentDisposition ? contentDisposition.split('filename=')[1].replace(/"/g, '') : 'expenses_export.csv'; // Get the CSV content const csvContent = await response.text(); // Create download link (browser environment) const blob = new Blob([csvContent], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url); } else { const error = await response.json(); throw new Error(error.error.message); } }; // Usage examples exportExpenses({ start_date: '2024-01-01', end_date: '2024-01-31' }); exportExpenses({ status: 'unpaid', currency: 'RON' }); exportExpenses({ supplier_id: 'sup_abc123' }); ``` ```php PHP theme={null} $filters = [ 'format' => 'csv', 'start_date' => '2024-01-01', 'end_date' => '2024-01-31', 'status' => 'paid' ]; $url = 'https://api.contazen.ro/v1/expenses/export?' . http_build_query($filters); $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY' ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE); if ($httpCode == 200 && strpos($contentType, 'text/csv') !== false) { // Success - save CSV file $filename = 'expenses_export_' . date('Y-m-d') . '.csv'; file_put_contents($filename, $response); echo "Export saved to: {$filename}"; } else { // Error response $error = json_decode($response, true); echo "Export failed: " . $error['error']['message']; } curl_close($curl); ``` ```csv CSV File Content theme={null} Reference,Date,Due Date,Supplier,Category,Description,Amount,Currency,VAT %,Amount without VAT,VAT Amount,Status,Payment Date,Payment Type INV-2024-001,2024-01-15,2024-02-15,Office Depot SRL,Office Expenses,Office supplies purchase,238.00,RON,19,200.00,38.00,Paid,2024-01-20,Bank transfer INV-2024-002,2024-01-16,2024-02-16,Marketing Pro,Marketing & Advertising,Social media campaign,1190.00,RON,19,1000.00,190.00,Unpaid,, EXP-2024-003,2024-01-17,2024-02-17,Travel Agency,Travel & Transportation,Business trip accommodation,595.00,RON,9,545.87,49.13,Paid,2024-01-25,Card ``` ```json Error Response (Invalid Format) theme={null} { "success": false, "error": { "message": "Invalid format. Use csv or pdf", "type": "invalid_request_error", "code": "invalid_format" } } ``` ```json Error Response (PDF Not Implemented) theme={null} { "success": false, "error": { "message": "PDF export not yet implemented", "type": "not_implemented", "code": "feature_not_available" } } ``` ## Best Practices ### File Management 1. **Naming conventions**: Use descriptive filenames with dates 2. **Storage**: Store exported files securely with appropriate access controls 3. **Cleanup**: Regularly clean up old export files to save storage space ### Data Integrity 1. **Filtering**: Use appropriate filters to export only needed data 2. **Sorting**: Sort by relevant fields for easier analysis 3. **Validation**: Verify exported data matches your expectations ### Performance Considerations 1. **Large exports**: For very large datasets, consider using date ranges to split exports 2. **Scheduling**: Schedule exports during off-peak hours for better performance 3. **Monitoring**: Monitor export file sizes and processing times ### Security 1. **Access control**: Ensure only authorized users can export sensitive financial data 2. **Audit logging**: Log export activities for security auditing 3. **Data handling**: Follow your organization's data protection policies for exported files ### Integration Tips 1. **Automation**: Automate regular exports for recurring reporting needs 2. **File processing**: Build workflows to automatically process exported CSV files 3. **Error handling**: Implement proper error handling for failed exports 4. **Notifications**: Set up notifications for successful or failed export operations # List Expenses Source: https://docs.contazen.ro/api-reference/endpoints/expenses/list GET /expenses Retrieve a paginated list of expenses with comprehensive filtering options ## Overview The List Expenses endpoint allows you to retrieve all expenses for your firm with advanced filtering, sorting, and pagination capabilities. You can filter by supplier, category, payment status, date ranges, and more. ## Query Parameters Page number for pagination (minimum: 1) Number of items per page (minimum: 1, maximum: 100) Filter by supplier using their CzUid Filter by expense category ID. If the category has subcategories, all subcategories will be included Filter by payment status * `paid` - Expense has been paid * `unpaid` - Expense is not paid and not overdue * `overdue` - Expense is past its due date * `registered` - Expense is registered but not categorized as paid/unpaid Filter by currency code (e.g., RON, EUR, USD) Filter by payment type ID (1-7): * `1` - Cash * `2` - Bank transfer * `3` - Card * `4` - Check * `5` - Promissory note * `6` - Other * `7` - Compensation Start date for filtering expenses (YYYY-MM-DD format) End date for filtering expenses (YYYY-MM-DD format) Minimum expense total (inclusive). Matched against the original-currency total. Maximum expense total (inclusive). Matched against the original-currency total. Sort field: * `created_at` - Sort by creation date * `date` - Sort by expense date * `due_date` - Sort by due date * `amount` - Sort by amount Sort order: * `asc` - Ascending * `desc` - Descending Comma-separated list of related data to include: * `supplier` - Include full supplier details * `category` - Include full category details * `user` - Include user information ## Response Array of expense objects Unique CzUid identifier for the expense Reference number or invoice number Expense description Original expense amounts Total amount Amount without VAT VAT amount Currency code Accounting amounts (converted to firm's base currency) Total amount in base currency Amount without VAT in base currency VAT amount in base currency Base currency code VAT percentage Whether amount includes VAT Expense date (YYYY-MM-DD) Payment due date (YYYY-MM-DD) Payment date (if paid) Whether expense is paid Payment type information Expense status (paid, unpaid, overdue, registered) Supplier CzUid (when not expanded) Category ID (when not expanded) User ID (when not expanded) Creation timestamp Last update timestamp Pagination information Current page number Items per page Total number of expenses Total number of pages Summary totals for filtered expenses Total original amount Total amount without VAT Total VAT amount Total amount in base currency Total amount without VAT in base currency Total VAT in base currency ```bash cURL theme={null} curl -X GET "https://api.contazen.ro/v1/expenses?page=1&per_page=20&status=unpaid&expand=supplier,category" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.contazen.ro/v1/expenses?page=1&per_page=20&status=unpaid&expand=supplier,category', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); console.log(data.expenses); ``` ```php PHP theme={null} $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => 'https://api.contazen.ro/v1/expenses?page=1&per_page=20&status=unpaid&expand=supplier,category', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); print_r($data['expenses']); ``` ```json Response theme={null} { "success": true, "expenses": [ { "id": "exp_abc123", "reference": "INV-2024-001", "description": "Office supplies", "amount": { "total": "238.00", "without_vat": "200.00", "vat": "38.00", "currency": "RON" }, "account_amount": { "total": "238.00", "without_vat": "200.00", "vat": "38.00", "currency": "RON" }, "vat_percent": 19, "with_vat": true, "date": "2024-01-15", "due_date": "2024-02-15", "paid_date": null, "is_paid": false, "payment_type": null, "status": "unpaid", "supplier": { "id": "sup_xyz789", "name": "Office Depot SRL", "cui": "RO12345678", "address": "Str. Exemplu 123", "city": "Bucharest" }, "category": { "id": 1, "name": "Office Expenses", "parent_id": null }, "user_id": 1, "created_at": "2024-01-15 10:30:00", "updated_at": "2024-01-15 10:30:00" } ], "pagination": { "page": 1, "per_page": 20, "total": 150, "total_pages": 8 }, "totals": { "amount": "15750.00", "amount_without_vat": "13235.29", "vat": "2514.71", "account_amount": "15750.00", "account_amount_without_vat": "13235.29", "account_vat": "2514.71" } } ``` # Scan receipt (OCR) Source: https://docs.contazen.ro/api-reference/endpoints/expenses/ocr-extract POST /expenses/ocr-extract Upload a receipt or invoice image and get a structured prefill payload ready to feed into Create Expense. ## Overview Send a photo of a Romanian receipt (bon fiscal), chitanță, or supplier invoice and receive a structured prefill payload describing the document — supplier match, dates, totals, per-rate VAT breakdown, optionally per-line items, and a payment-method hint. Use the response to prefill an expense form. Persist via [`POST /expenses`](./create) once the user confirms — typically with `vat = "mix"` + `amount_vat_manual` for receipts flagged as multi-rate. ## Request `Content-Type: multipart/form-data` Receipt image — JPEG, PNG, or WebP, up to 10 MB. Mobile clients should compress to JPEG before upload (HEIC is rejected). ## Response (200) The response is a normalized prefill payload, not a stored expense. Persist it via [`POST /expenses`](./create) once the user confirms. Opaque token referencing the uploaded image staged on the server. Pass this to the attachment endpoint when posting the expense so the same image gets attached without a re-upload. Expires after 15 minutes. Resolved supplier match. * `id` — CzUid of an existing supplier matched by CUI/name, or `null` for a new auto-created one * `name`, `cui` — best-effort values from the receipt * `matchedExisting` — `true` when an existing supplier was found Fuzzy-matched category from the firm's list (system + custom merged): `{ id, name }`. Both may be `null` when no match is found. Issue date in `YYYY-MM-DD`. Falls back to today if unreadable. Due date in `YYYY-MM-DD`, or `null` for receipts paid at the point of sale (bon fiscal). When `null`, the form should auto-mark the expense as paid with `paid_date = date` and `payment_type` derived from `paymentMethod` (or `1 = Cash` as fallback). 3-letter ISO code (RON, EUR, USD). Net subtotal (excluding VAT). On the **flat** path this is what the user enters in the "Valoarea" field with `with_vat = false`. Dominant VAT percent. Rounded to one of the Romanian rates. On `vatMode = "mix"`, this is informational only — use `amount_vat_manual` instead. Total VAT amount across all rate tranches. **On `vatMode = "mix"`, send this back as `amount_vat_manual` when posting the expense.** `"flat"` (single rate, send `vat = `) or `"mix"` (two or more non-zero rate tranches, send `vat = "mix"` + `amount_vat_manual = vatAmount`). Gross total printed on the receipt. Always equal to `amount + vatAmount` on the mix path. `true` when only the gross total was printed (typical bon fiscal). On mix mode this is `false` because `amount` is the explicit subtotal. Per-rate breakdown when the receipt mixes more than one rate, otherwise `null`. Each entry: `{ rate, net, gross, vatAmount }` — any of `net` / `gross` may be `null`. On `vatMode = "mix"`, send this array back via `vat_breakdown` when posting so reports keep accurate per-rate totals. Payment method inferred from the document footer. One of `"cash"`, `"card"`, `"transfer"`, `"other"`, or `null` (typical for factură furnizor printed before payment). Map to `payment_type` when posting: * `cash` → `1` (Cash, "Numerar prin bon fiscal") * `card` → `4` (Card) * `transfer` → `3` (Bank transfer / Ordin de plată) * `other` / `null` → caller decides Document / receipt / invoice number (`reference` in the create payload). Free-text description, often `null`. Optional. When present, each entry has `name`, `quantity`, `unit_price` (NET), `vat_rate`. May be absent depending on document quality and structure — clients that ignore this field still get correct totals via the aggregate fields. Pre-localized Romanian strings safe to surface in the form alert. Examples: * `"Furnizor nou"` — supplier auto-created * `"Mai multe cote TVA"` — multi-rate detected, switch UI to mix mode * `"Liniile au fost ignorate ..."` — items omitted; rely on the aggregate fields ## Mapping the response to Create Expense ```jsonc theme={null} // OCR response { "supplier": { "id": "Q9hjAB", "name": "KAUFLAND ROMANIA SCS", "cui": "15991149" }, "category": { "id": "11", "name": "Alte consumabile" }, "date": "2026-04-25", "dueDate": null, "currency": "RON", "amount": 147.53, "vatAmount": 24.15, "vatMode": "mix", "total": 171.68, "vatBreakdown": [ { "rate": 21, "net": 80.29, "gross": 97.15, "vatAmount": 16.86 }, { "rate": 11, "net": 66.27, "gross": 73.56, "vatAmount": 7.29 }, { "rate": 0, "net": 0.97, "gross": 0.97, "vatAmount": 0.00 } ], "paymentMethod": "card", "docNumber": "65776", "warnings": ["Mai multe cote TVA"] } ``` ```jsonc theme={null} // → POST /expenses body { "supplier_id": "Q9hjAB", "category_id": 11, "reference": "65776", "date": "2026-04-25", "due_date": "2026-04-25", // dueDate was null → bon fiscal: due = issue "amount": 147.53, "vat": "mix", "amount_vat_manual": 24.15, "currency": "RON", "vat_breakdown": [ { "rate": 21, "net": 80.29, "gross": 97.15, "vat_amount": 16.86 }, { "rate": 11, "net": 66.27, "gross": 73.56, "vat_amount": 7.29 }, { "rate": 0, "net": 0.97, "gross": 0.97, "vat_amount": 0.00 } ], "is_paid": true, "paid_date": "2026-04-25", "payment_type": 4 // paymentMethod="card" → 4 } ``` The server stores `amount_wvat = 147.53`, `amount_vat = 24.15`, `amount_total = 171.68` exactly. ## Errors * **400 `file_missing`** — no `image` part in the multipart body * **400 `invalid_file_type`** — unsupported extension (must be jpg/jpeg/png/webp) * **400 `invalid_mime_type`** — content sniff didn't match an accepted image MIME * **400 `file_too_large`** — file exceeds 10 MB * **502 `ocr_unavailable`** — extraction failed; safe to retry * **503 `ocr_not_configured`** — OCR is not enabled on this deployment # Mark Expense as Paid Source: https://docs.contazen.ro/api-reference/endpoints/expenses/pay POST /expenses/{id}/pay Mark an unpaid expense as paid by providing payment date and method ## Overview The Mark Expense as Paid endpoint allows you to record payment for an unpaid expense. This is a specialized endpoint that specifically handles the payment workflow, updating the expense status and recording payment details. This endpoint is designed for expenses that are currently unpaid. If an expense is already paid, the request will fail with an appropriate error message. ## Path Parameters The CzUid of the expense to mark as paid ## Request Body The date when the payment was made (YYYY-MM-DD format) The payment method used (1-7): * `1` - Cash * `2` - Bank transfer * `3` - Card * `4` - Check * `5` - Promissory note * `6` - Other * `7` - Compensation ## Response The updated expense object reflecting the payment Expense CzUid Now set to true The payment date provided Payment type information Payment type ID Payment type name Updated to "paid" New modification timestamp Success message confirming the payment has been recorded ## Business Logic When an expense is marked as paid: 1. **Status Update**: The expense status changes from unpaid/overdue to paid 2. **Payment Recording**: Payment date and method are permanently recorded 3. **Audit Trail**: The payment action is logged for audit purposes 4. **Notifications**: Internal notifications may be triggered (if configured) ## Validation Rules * **Expense Exists**: The expense must exist and be owned by your firm * **Payment Status**: The expense must currently be unpaid (not already paid) * **Date Format**: Payment date must be in YYYY-MM-DD format * **Payment Type**: Must be a valid payment type ID (1-7) * **Date Logic**: Payment date should typically not be in the future ## Error Scenarios ### Already Paid If the expense is already marked as paid, you'll receive a 400 error. Use the [Update Expense](/api-reference/endpoints/expenses/update) endpoint if you need to change payment details for an already-paid expense. ### Invalid Payment Type Payment types must match the predefined values. Check the payment\_type parameter documentation for valid options. ### Date Validation Payment dates must be valid dates in the correct format. The system may also validate that payment dates are reasonable (not too far in the past or future). ```bash cURL theme={null} curl -X POST "https://api.contazen.ro/v1/expenses/exp_abc123/pay" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "paid_date": "2024-01-20", "payment_type": 2 }' ``` ```javascript JavaScript theme={null} const expenseId = 'exp_abc123'; const paymentData = { paid_date: "2024-01-20", payment_type: 2 // Bank transfer }; const response = await fetch(`https://api.contazen.ro/v1/expenses/${expenseId}/pay`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(paymentData) }); const data = await response.json(); if (data.success) { console.log('Expense marked as paid:', data.expense.status); } else { console.error('Payment failed:', data.error.message); } ``` ```php PHP theme={null} $expenseId = 'exp_abc123'; $paymentData = [ 'paid_date' => '2024-01-20', 'payment_type' => 2 // Bank transfer ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expenses/{$expenseId}/pay", CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($paymentData), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); if ($data['success']) { echo 'Payment recorded: ' . $data['expense']['paid_date']; } else { echo 'Error: ' . $data['error']['message']; } ``` ```json 200 - Success theme={null} { "success": true, "expense": { "id": "exp_abc123", "reference": "INV-2024-001", "description": "Office supplies purchase", "amount": { "total": "238.00", "without_vat": "200.00", "vat": "38.00", "currency": "RON" }, "account_amount": { "total": "238.00", "without_vat": "200.00", "vat": "38.00", "currency": "RON" }, "vat_percent": 19, "with_vat": true, "date": "2024-01-15", "due_date": "2024-02-15", "paid_date": "2024-01-20", "is_paid": true, "payment_type": { "id": 2, "name": "Bank transfer" }, "status": "paid", "supplier_id": "sup_xyz789", "category_id": 1, "user_id": 1, "created_at": "2024-01-15 10:30:00", "updated_at": "2024-01-20 14:30:00" }, "message": "Expense marked as paid successfully" } ``` ```json 400 - Already Paid theme={null} { "success": false, "error": { "message": "Expense is already marked as paid", "type": "invalid_request_error", "code": "expense_already_paid" } } ``` ```json 400 - Validation Error theme={null} { "success": false, "error": { "message": "Validation failed", "type": "invalid_request_error", "code": "validation_error", "fields": { "paid_date": "Payment date is required", "payment_type": "Invalid payment type. Must be between 1 and 7" } } } ``` ```json 404 - Not Found theme={null} { "success": false, "error": { "message": "Expense not found", "type": "invalid_request_error", "code": "resource_missing", "param": "id" } } ``` ## Use Cases ### Bulk Payment Processing When processing multiple payments (e.g., from bank statements), you can use this endpoint to mark individual expenses as paid: ```javascript JavaScript theme={null} const payments = [ { expenseId: 'exp_abc123', date: '2024-01-20', type: 2 }, { expenseId: 'exp_def456', date: '2024-01-21', type: 1 }, ]; for (const payment of payments) { await markExpensePaid(payment.expenseId, payment.date, payment.type); } ``` ### Integration with Banking APIs This endpoint is ideal for integrating with banking APIs that provide transaction data: ```javascript JavaScript theme={null} // Process bank transactions and mark corresponding expenses as paid bankTransactions.forEach(async (transaction) => { const expense = findMatchingExpense(transaction); if (expense) { await markExpensePaid(expense.id, transaction.date, getPaymentType(transaction.type)); } }); ``` ### Manual Payment Entry For manual payment entry in accounting workflows: ```javascript JavaScript theme={null} const recordPayment = async (expenseId, paymentDetails) => { try { const result = await markExpensePaid(expenseId, paymentDetails.date, paymentDetails.method); showSuccessMessage('Payment recorded successfully'); refreshExpenseList(); } catch (error) { showErrorMessage('Failed to record payment: ' + error.message); } }; ``` # Retrieve Expense Source: https://docs.contazen.ro/api-reference/endpoints/expenses/retrieve GET /expenses/{id} Get detailed information about a specific expense by its CzUid ## Overview The Retrieve Expense endpoint allows you to get complete details about a specific expense record. You can optionally expand related data such as supplier information, category details, and user information. ## Path Parameters The CzUid of the expense to retrieve ## Query Parameters Comma-separated list of related data to include in the response: * `supplier` - Include full supplier details instead of just the ID * `category` - Include full category details instead of just the ID * `user` - Include user information for who created the expense ## Response Complete expense object with all details Unique CzUid identifier for the expense Reference number or invoice number Detailed description of the expense Original expense amounts in the expense currency Total amount Amount without VAT VAT amount Currency code Accounting amounts in the firm's base currency Total amount in base currency Amount without VAT in base currency VAT amount in base currency Base currency code VAT percentage applied to the expense Whether the original amount includes VAT Expense date (YYYY-MM-DD) Payment due date (YYYY-MM-DD) Payment date (null if not paid) Whether the expense has been paid Payment type information (null if not paid) Payment type ID Payment type name Current expense status: `paid`, `unpaid`, `overdue`, or `registered` Supplier CzUid (when not expanded) Full supplier details (when expanded) Supplier CzUid Supplier name Tax identification number Trade register number Address City County Country Category ID (when not expanded) Full category details (when expanded) Category ID Category name Parent category ID (null for root categories) User ID (when not expanded) User information (when expanded) User ID User name Currency exchange information (only if applicable) Original currency Target currency Exchange rate used Attachment information (only if expense has an attachment) Download URL for the attachment File type (pdf or image) Line rows for the expense. Always at least one entry — legacy flat expenses get a single synthesized line built from the header totals so API consumers can render a uniform table. See [Expense Lines & VAT Breakdown](/concepts/expense-lines) for the full model. Line row ID (0 for synthesized legacy lines) Zero-based position Display name Optional detail Line quantity UN/ECE unit (e.g. `H87`) Net unit price VAT percent for this line Line net (2dp, `quantity × unit_price` rounded) Line VAT (2dp) Line gross (2dp) Per-line category override Linked product CzUid Per-rate VAT tranches when the receipt mixes two or more rates. `null` for single-rate expenses. VAT percent Sum of nets at this rate Sum of VAT at this rate Sum of gross at this rate How the category was assigned: `ai` (auto-classified), `supplier` (inherited from the supplier's default), or `manual`. `null` on expenses without a category or legacy rows where provenance wasn't recorded. Auto-classifier confidence in the 0–1 range when `category_source === 'ai'`. `null` otherwise. Populated when the expense was imported from the ANAF e-Factura SPV inbox. `null` for manually created expenses. CzUid of the linked supplier invoice Supplier's invoice number ANAF e-Factura message identifier Whether the e-Factura XML is stored on disk Whether the PDF has already been generated Bearer-authenticated URL that streams the e-Factura PDF (see [Download e-Factura PDF](/api-reference/endpoints/expenses/efactura-pdf)) When the expense was created When the expense was last modified ## Error Responses The expense with the specified ID does not exist or you don't have access to it ```bash cURL theme={null} curl -X GET "https://api.contazen.ro/v1/expenses/exp_abc123?expand=supplier,category,user" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ```javascript JavaScript theme={null} const expenseId = 'exp_abc123'; const expand = ['supplier', 'category', 'user'].join(','); const response = await fetch(`https://api.contazen.ro/v1/expenses/${expenseId}?expand=${expand}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); console.log('Expense details:', data.expense); ``` ```php PHP theme={null} $expenseId = 'exp_abc123'; $expand = 'supplier,category,user'; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expenses/{$expenseId}?expand={$expand}", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); print_r($data['expense']); ``` ```json 200 - Success theme={null} { "success": true, "expense": { "id": "exp_abc123", "reference": "INV-2024-001", "description": "Office supplies purchase including paper, pens, and folders", "amount": { "total": "238.00", "without_vat": "200.00", "vat": "38.00", "currency": "RON" }, "account_amount": { "total": "238.00", "without_vat": "200.00", "vat": "38.00", "currency": "RON" }, "vat_percent": 19, "with_vat": true, "date": "2024-01-15", "due_date": "2024-02-15", "paid_date": null, "is_paid": false, "payment_type": null, "status": "unpaid", "supplier": { "id": "sup_xyz789", "name": "Office Depot SRL", "cui": "RO12345678", "rc": "J40/1234/2020", "address": "Str. Exemplu 123, Sector 1", "city": "Bucharest", "county": "Bucharest", "country": "Romania" }, "category": { "id": 1, "name": "Office Expenses", "parent_id": null }, "user": { "id": 1, "name": "John Doe" }, "attachment": { "url": "https://api.contazen.ro/v1/files/download/att_def456", "type": "pdf" }, "items": [ { "id": 4201, "line_index": 0, "name": "Office supplies", "description": null, "quantity": 1, "unit_code": null, "unit_price": 200.00, "vat_rate": 19, "amount_wvat": 200.00, "amount_vat": 38.00, "amount": 238.00, "category_id": null, "product_id": null } ], "vat_breakdown": null, "category_source": "manual", "category_confidence": null, "efactura": { "supplier_invoice_id": "si_abc123", "invoice_number": "119079", "anaf_message_id": "4891234567", "xml_available": true, "pdf_available": true, "pdf_url": "https://app.contazen.ro/api/v1/expenses/exp_abc123/efactura-pdf" }, "created_at": "2024-01-15 10:30:00", "updated_at": "2024-01-15 10:30:00" } } ``` ```json 404 - Not Found theme={null} { "success": false, "error": { "message": "Expense not found", "type": "invalid_request_error", "code": "resource_missing", "param": "id" } } ``` # Update Expense Source: https://docs.contazen.ro/api-reference/endpoints/expenses/update PUT /expenses/{id} Update an existing expense with new information while preserving data integrity ## Overview The Update Expense endpoint allows you to modify an existing expense record. You can update any field including supplier, category, amounts, payment status, and other details. The system will automatically recalculate VAT and currency conversions based on your changes. Some expenses may not be modifiable if they are locked due to accounting periods or other business rules. The system will return an error if the expense cannot be modified. ## Path Parameters The CzUid of the expense to update ## Request Body All fields are optional - only include the fields you want to update. Existing values will be preserved for any fields not included in the request. Change the supplier for this expense (CzUid) Change the expense category Update the reference or invoice number Update the expense description Update the expense amount (will trigger VAT recalculation) Change whether the amount includes VAT Update the VAT percentage. Either a numeric value (`0`, `5`, `9`, `11`, `19`, `21`) or the string `"mix"` for multi-rate receipts where the caller supplies total VAT manually via `amount_vat_manual`. See [Mix VAT mode on Create](/api-reference/endpoints/expenses/create#mix-vat-mode) for the full shape. Total VAT amount in receipt currency. Required (`>= 0`) when `vat = "mix"`, ignored otherwise. The server stores `amount_wvat = amount`, `amount_vat = amount_vat_manual`, `amount_total = amount + amount_vat_manual`. Change the currency (will trigger currency conversion recalculation) Update the expense date (YYYY-MM-DD) Update the payment due date (YYYY-MM-DD) Change the payment status Update the payment date (only relevant if is\_paid is true) Update the payment type ID (1-7, only relevant if is\_paid is true): * `1` - Cash * `2` - Bank transfer * `3` - Card * `4` - Check * `5` - Promissory note * `6` - Other * `7` - Compensation Enable or disable currency exchange Update the target currency for exchange Replace the expense line rows. Sending `items` **overwrites** all existing lines for this expense — send the full desired set, not a diff. Lines you omit are deleted. When `items` is present the server derives `amount`, `vat`, and `vat_breakdown` from the lines; the corresponding header-level fields on the request body are ignored. Omit `items` entirely to leave the existing lines untouched and update only the header fields you pass. To convert an itemized expense back to flat, send an empty `items: []` alongside new `amount` + `vat`. Per-item shape matches the [Create Expense](/api-reference/endpoints/expenses/create) endpoint. See [Expense Lines & VAT Breakdown](/concepts/expense-lines) for rounding rules. Update the per-rate VAT breakdown for multi-VAT receipts on the flat or mix paths. Send an array of `{ rate, net, vat_amount, gross }` entries (two or more). On the mix path, the breakdown is also used to synthesize one persisted line per rate group. Ignored when `items` is supplied. Update the exchange rate ## Response Returns the updated expense object with all recalculated fields. The updated expense object with all details Expense CzUid (unchanged) Updated reference Updated description Recalculated amounts in expense currency Recalculated amounts in base currency Updated VAT percentage Updated VAT inclusion flag Updated expense date Updated due date Updated payment status Updated payment date Updated payment type info Recalculated expense status Updated supplier CzUid Updated category ID Original user ID (unchanged) Updated currency exchange info Original creation time (unchanged) New modification timestamp Success message confirming the update ## Automatic Recalculations When you update certain fields, the system automatically recalculates related values: ### VAT Recalculation * **Amount changes**: VAT amounts are recalculated based on the new amount and VAT percentage * **VAT percentage changes**: All VAT-related amounts are recalculated * **with\_vat changes**: The relationship between total and VAT-exclusive amounts is recalculated ### Currency Conversion * **Currency changes**: Account amounts are recalculated using current exchange rates * **Exchange rate changes**: Account amounts are recalculated with the new rate ### Status Updates * **Payment status**: The overall expense status is updated based on payment and due date information ## Validation Rules * **Modifiability**: The expense must not be locked by accounting rules * **Supplier**: If changed, must exist and be owned by your firm * **Category**: If changed, must be a valid expense category * **Amount**: If changed, must be a positive number * **Dates**: Must be in YYYY-MM-DD format * **Payment consistency**: If marking as paid, payment\_type is required ```bash cURL theme={null} curl -X PUT "https://api.contazen.ro/v1/expenses/exp_abc123" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "Updated: Office supplies and equipment", "amount": 285.00, "is_paid": true, "paid_date": "2024-01-20", "payment_type": 2 }' ``` ```javascript JavaScript theme={null} const expenseId = 'exp_abc123'; const updateData = { description: "Updated: Office supplies and equipment", amount: 285.00, is_paid: true, paid_date: "2024-01-20", payment_type: 2 }; const response = await fetch(`https://api.contazen.ro/v1/expenses/${expenseId}`, { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await response.json(); console.log('Updated expense:', data.expense); ``` ```php PHP theme={null} $expenseId = 'exp_abc123'; $updateData = [ 'description' => 'Updated: Office supplies and equipment', 'amount' => 285.00, 'is_paid' => true, 'paid_date' => '2024-01-20', 'payment_type' => 2 ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://api.contazen.ro/v1/expenses/{$expenseId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_POSTFIELDS => json_encode($updateData), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $data = json_decode($response, true); curl_close($curl); echo 'Updated expense status: ' . $data['expense']['status']; ``` ```json 200 - Success theme={null} { "success": true, "expense": { "id": "exp_abc123", "reference": "INV-2024-001", "description": "Updated: Office supplies and equipment", "amount": { "total": "285.00", "without_vat": "239.50", "vat": "45.50", "currency": "RON" }, "account_amount": { "total": "285.00", "without_vat": "239.50", "vat": "45.50", "currency": "RON" }, "vat_percent": 19, "with_vat": true, "date": "2024-01-15", "due_date": "2024-02-15", "paid_date": "2024-01-20", "is_paid": true, "payment_type": { "id": 2, "name": "Bank transfer" }, "status": "paid", "supplier_id": "sup_xyz789", "category_id": 1, "user_id": 1, "created_at": "2024-01-15 10:30:00", "updated_at": "2024-01-20 14:45:00" }, "message": "Expense updated successfully" } ``` ```json 403 - Cannot Modify theme={null} { "success": false, "error": { "message": "Expense cannot be modified due to accounting restrictions", "type": "invalid_request_error", "code": "expense_locked" } } ``` ```json 400 - Validation Error theme={null} { "success": false, "error": { "message": "Validation failed", "type": "invalid_request_error", "code": "validation_error", "fields": { "amount": "Amount must be greater than 0", "payment_type": "Payment type is required when expense is marked as paid" } } } ``` ```json 404 - Not Found theme={null} { "success": false, "error": { "message": "Expense not found", "type": "invalid_request_error", "code": "resource_missing", "param": "id" } } ``` # Create an Invoice Series Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/create POST /invoice-series Create a new invoice numbering series # List Invoice Series Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/list GET /invoice-series List all invoice numbering series # List Reservations Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/list-reservations GET /invoice-series/{id}/reservations List pending number reservations owned by the caller mobile device. ## Overview Returns the reservation pool for the authenticated device on this series. Used by the mobile app to reconcile its local pool against the server's view after a long offline period or a local database wipe. Only reservations where `device_id` matches the caller's `user_session.device_id` AND `status = 'pending'` are returned. Expired / consumed / released rows are omitted — those are not usable for new invoices. ## Response Same shape as [Reserve Numbers](/api-reference/endpoints/invoice-series/reserve-numbers). Number of usable reservations the calling device currently owns on this series. # Lock Series to Mobile Device Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/lock POST /invoice-series/{id}/lock Bind an invoice series to the caller mobile device so only that device can issue invoices against it. ## Overview Locking a series binds it to the mobile device that issued the call (identified by the `device_id` stored on its `user_session`). Once locked: * Only that device can issue invoices against the series — online or via pre-reserved numbers when offline. * Web/API-key callers attempting to `POST /invoices` with a locked `series_id` receive a `series_locked_to_device` error. * The locked device can call `POST /invoice-series/{id}/reserve-numbers` to pre-allocate numbers for offline use. This prevents race conditions on the invoice number counter when a mobile device is offline and a web user is also trying to emit invoices on the same series. Requires a mobile JWT session (issued by `/auth/login`). The JWT's backing `user_session` must have a non-empty `device_id` — re-authenticate from the app if `device_id_missing` is returned. ## Response Non-null once the series is locked. Stable device identifier persisted on the mobile app (not a session ID). The `user_session.id` that created the lock. The user who locked the series. Human-readable device label, e.g. `iPhone 15 Pro`. `ios`, `android` or `web`. Unix timestamp (seconds) when the lock was created. Always `true` after a successful lock. # Release Reservation Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/release-reservation POST /invoice-series/reservations/{token}/release Give up a pending reservation the device no longer plans to use. ## Overview Flips a `pending` reservation to `released`. Called by the mobile app when the user discards an offline draft before it was synced. Releasing does **not** return the number to the pool — the series counter already advanced when the reservation was allocated. Released reservations become documented gaps (same outcome as expired ones). Release primarily exists so the server can tell the difference between "device will retry this invoice later" and "user cancelled it intentionally" — useful for audit trails and for other devices that may come back online and expect accurate pool sizes. ## Path parameters The `reservation_token` returned by [Reserve Numbers](/api-reference/endpoints/invoice-series/reserve-numbers). ## Response Returns the updated reservation object with `status = 'released'`. ## Errors | Code | Meaning | | ------------------ | ------------------------------------------------------------------------- | | `resource_missing` | Token does not exist or belongs to another firm. | | `forbidden` | Token belongs to a different device — only the owning device can release. | # Reserve Numbers Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/reserve-numbers POST /invoice-series/{id}/reserve-numbers Pre-allocate N invoice numbers from a locked series so the mobile device can emit invoices offline with correct numbering. ## Overview Pulls `count` sequential numbers off the series counter and hands back one reservation token per slot. The caller mobile device carries these tokens in local storage and spends them when creating invoices offline. When the device later syncs the drafts with `POST /invoices`, it includes `reservation_token` in the body — the backend uses the reservation's pre-allocated `number_idx` instead of incrementing the counter again. This keeps invoice numbers gapless even across long offline periods. The series must be locked to the calling device first (see [Lock Series](/api-reference/endpoints/invoice-series/lock)). Calling this on an unlocked series, or from a different device than the one holding the lock, returns `series_not_locked_to_device`. ## Body Number of reservation slots to allocate. Maximum 50 per call — request again if the device needs more. ## Response Returns a list of reservation objects. Opaque token. Pass as `reservation_token` when emitting the invoice. The pre-allocated sequence number within the series. `pending`, `consumed`, `released`, or `expired`. Device this reservation is bound to. Unix timestamp (seconds). Unix timestamp (seconds). Reservations expire after 30 days by default. Unix timestamp when consumed, else `null`. ## Behavior after TTL If a reservation expires without being consumed, the pre-allocated number becomes a gap in the series. Romanian fiscal practice accepts gaps when documented — keep the reservation TTL generous (default 30 days) and refresh the pool whenever the device comes back online. The cleanup cron flips expired rows to `status = 'expired'` but does not renumber downstream invoices. ## Lifecycle ```text theme={null} ┌──────────────┐ consumed reserve ────► │ pending │ ───────────────► consumed (by POST /invoices) └──────────────┘ │ ├─ released (by POST /invoice-series/reservations/{token}/release) │ └─ expired (by TTL cron) ``` # Set Series as Primary Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/set-primary POST /invoice-series/{id}/set-primary Set an invoice series as the default for its document type # Unlock Series Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/unlock POST /invoice-series/{id}/unlock Release the mobile-device lock on an invoice series. ## Overview Removes the mobile-device binding on a series. After unlocking, any authenticated caller (web, API key, or mobile) can emit invoices on the series again. * The mobile device that owns the lock can always unlock. * A different mobile device needs `force: true` in the body *and* firm-admin rights to override. * Legacy API-key callers bypass the device check and can force-unlock unconditionally (intended for admin tooling). ## Body Required when a caller that does not own the lock wants to break it. Without `force`, the server returns `series_locked_other_device`. ## Response Returns the updated series object. `is_locked` is `false` and `lock` is `null`. Any offline reservations the previous device had not yet synced are still valid: the lock only gates *new* invoice creation. If a locked device has queued invoices in its outbox, coordinate with the user before force-unlocking — their next sync will still succeed if their reservation tokens are still pending on the server. # Update an Invoice Series Source: https://docs.contazen.ro/api-reference/endpoints/invoice-series/update PUT /invoice-series/{id} Update an existing invoice series # Create Invoice Source: https://docs.contazen.ro/api-reference/endpoints/invoices/create POST /invoices Create a new invoice with optional client creation # Delete Invoice Source: https://docs.contazen.ro/api-reference/endpoints/invoices/delete DELETE /invoices/{id} Permanently delete a voided or draft invoice # Download Invoice PDF Source: https://docs.contazen.ro/api-reference/endpoints/invoices/download-pdf GET /invoices/{id}/pdf Download an invoice as PDF # Get Invoice History Source: https://docs.contazen.ro/api-reference/endpoints/invoices/history GET /invoices/{id}/history Audit trail of all changes to a fiscal invoice. ## Overview Returns the ordered `firm_history` events recorded against a fiscal invoice — create, update, storno, send, void, SPV submission, payment, etc. Each entry identifies the acting user. ## Actor attribution When the request is made with a mobile JWT, the acting user is the JWT subject. When made with an API key, the attributed user is the key's owner (the `user_id` stamped on the `api_key` row). Entries prior to 2026-04-13 may report a system `user_id` of `0` — those predate the history attribution fix and cannot be retroactively repaired. # List Invoices Source: https://docs.contazen.ro/api-reference/endpoints/invoices/list GET /invoices Retrieve a paginated list of invoices # Mark Invoice Paid Source: https://docs.contazen.ro/api-reference/endpoints/invoices/pay POST /invoices/{id}/pay Record a payment for an invoice # Estimate Payment Date Source: https://docs.contazen.ro/api-reference/endpoints/invoices/payment-estimate GET /invoices/{id}/payment-estimate Predict when an unpaid invoice is likely to be paid, based on client history # Retrieve Invoice Source: https://docs.contazen.ro/api-reference/endpoints/invoices/retrieve GET /invoices/{id} Retrieve a specific invoice # Send Invoice Source: https://docs.contazen.ro/api-reference/endpoints/invoices/send POST /invoices/{id}/send Send an invoice by email # Send to SPV Source: https://docs.contazen.ro/api-reference/endpoints/invoices/send-to-spv POST /invoices/{id}/send-to-spv Send invoice to Romanian SPV (e-Factura) system # Get Invoice Statistics Source: https://docs.contazen.ro/api-reference/endpoints/invoices/stats GET /invoices/stats Get comprehensive invoice statistics and trends ## Overview This endpoint provides detailed invoice statistics including counts by status, monthly trends, top clients, and revenue breakdown. Perfect for dashboards and analytics. ## Query Parameters Year for statistics (defaults to current year) Specific month (1-12) for detailed monthly statistics Specific quarter (1-4) for quarterly statistics Filter statistics by specific currency ## Response Indicates if the request was successful Overall statistics summary Total number of invoices Total revenue across all currencies (in RON) Average invoice value Percentage of invoices paid (0-100) Invoice counts by status Number of draft invoices Number of sent invoices Number of paid invoices Number of overdue invoices Number of cancelled/voided invoices Monthly revenue trend Month name Month number (1-12) Total revenue for the month Number of invoices for the month Percentage growth from previous month Top 10 clients by revenue Client CzUid Client name Total revenue from this client Number of invoices for this client Percentage of total revenue Revenue breakdown by currency Currency code Total amount in this currency Total converted to RON Number of invoices Breakdown by payment method Number of bank transfer payments Number of cash payments Number of card payments Number of other payment methods ```bash theme={null} curl -X GET https://api.contazen.ro/v1/invoices/stats \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -G -d "year=2024" ``` ```json theme={null} { "success": true, "data": { "overview": { "total_invoices": 245, "total_revenue": 458750.00, "average_invoice_value": 1872.45, "payment_rate": 87.5 }, "by_status": { "draft": 5, "sent": 25, "paid": 195, "overdue": 15, "cancelled": 5 }, "monthly_trend": [ { "month": "January", "month_number": 1, "total": 35000.00, "count": 18, "growth": 0 }, { "month": "February", "month_number": 2, "total": 42000.00, "count": 22, "growth": 20.0 } ], "top_clients": [ { "client_id": "cli_abc123def", "client_name": "Tech Solutions SRL", "total_revenue": 85000.00, "invoice_count": 12, "percentage": 18.5 } ], "by_currency": [ { "currency": "RON", "total": 385000.00, "total_ron": 385000.00, "count": 210 }, { "currency": "EUR", "total": 15000.00, "total_ron": 73750.00, "count": 35 } ], "payment_methods": { "bank_transfer": 165, "cash": 20, "card": 10, "other": 0 } } } ``` # Storno Invoice Source: https://docs.contazen.ro/api-reference/endpoints/invoices/storno POST /invoices/{id}/storno Create a reversal invoice for a fiscal invoice. ## Overview Produces a **storno** (reversal) invoice from a fiscal source invoice. The new invoice mirrors every line of the source with negated amounts, receives its own number from the same series, and is linked back to the source via `storno_bill_id`. Both invoices record a history event — the source gets a `storno` entry and the new reversal gets a `storned` entry. The whole flow runs inside a transaction: if any step fails, nothing is persisted. ## Preconditions * The source must be a **fiscal** invoice (not a draft or proforma). * The source itself must not already be a storno. * The source must not have been storno'd before (a single invoice can only be reversed once). Calls that violate these preconditions return `400` with codes `invoice_cannot_be_storned` or `invoice_already_storned`. ## Deleting vs. storning Storno is the ANAF-compliant way to cancel a fiscal invoice. Prefer `/invoices/{id}/storno` over `DELETE /invoices/{id}` once an invoice has been issued to a client or sent to SPV — deleting a fiscal invoice is blocked by the series-continuity rules. # Get Invoice Totals Source: https://docs.contazen.ro/api-reference/endpoints/invoices/totals GET /invoices/totals Get aggregated invoice totals for a specific period ## Overview This endpoint returns aggregated invoice totals for a specific date range. The totals are grouped by currency and payment status, providing a comprehensive overview of your invoicing metrics. ## Query Parameters Start date for the period in YYYY-MM-DD format End date for the period in YYYY-MM-DD format Filter totals by specific currency code (e.g., RON, EUR, USD) Filter totals by specific client using their CzUid Filter totals by specific invoice series using its CzUid Filter by payment status (0 for unpaid, 1 for paid) Filter by document type (fiscal or proforma) ## Response Indicates if the request was successful Array of total objects grouped by currency Currency code Total invoice amount in this currency Total amount paid in this currency Total amount unpaid in this currency Total VAT amount in this currency Number of invoices in this currency Number of paid invoices in this currency Number of unpaid invoices in this currency Start date of the period End date of the period Grand total converted to RON using current exchange rates ```bash theme={null} curl -X GET https://api.contazen.ro/v1/invoices/totals \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -G -d "start_date=2024-01-01" \ -d "end_date=2024-12-31" ``` ```json theme={null} { "success": true, "data": { "totals": [ { "currency": "RON", "total_amount": 125000.00, "total_paid": 95000.00, "total_unpaid": 30000.00, "total_vat": 21000.00, "invoice_count": 45, "paid_count": 35, "unpaid_count": 10 }, { "currency": "EUR", "total_amount": 15000.00, "total_paid": 12000.00, "total_unpaid": 3000.00, "total_vat": 2520.00, "invoice_count": 8, "paid_count": 6, "unpaid_count": 2 } ], "period": { "start_date": "2024-01-01", "end_date": "2024-12-31" }, "grand_total_ron": 199575.00 } } ``` # Unvoid Invoice Source: https://docs.contazen.ro/api-reference/endpoints/invoices/unvoid POST /invoices/{id}/unvoid Reactivate a voided invoice # Update Invoice Source: https://docs.contazen.ro/api-reference/endpoints/invoices/update PUT /invoices/{id} Update header-level fields on an existing invoice. ## Overview The update endpoint intentionally has a narrow scope: it accepts only header fields plus an optional full-item rewrite. It does **not** support spot-editing a single line — send the whole `items` array if you need to change one row. If you need a structural change to a fiscal invoice (different client, wholesale repricing, etc.), issue `POST /invoices/{id}/storno` followed by a fresh `POST /invoices` rather than mutating the original. ## Legacy field aliases Several request fields have aliases that remain accepted for backwards compatibility. New integrations should prefer the canonical names: | Canonical | Legacy aliases | | --------- | ---------------------- | | `info` | `observation`, `notes` | | `date_at` | `date` | | `due_at` | `due_date` | ## Row-level totals are derived, not accepted Row objects inside `items[]` **must not** carry `price_total` or `account_amount_total` — send the components (`quantity`, `price`, `vat_rate`) and the server recomputes both. Sending totals directly used to work in earlier builds and has been a silent source of rounding drift; the fields are now ignored. ## Exchange-rate handling When `currency` is `RON`, `currency_rate` is forced to `1.000` regardless of what you send. For non-RON invoices, a positive `exchange_rate` propagates into every `account_amount_*` field on both the invoice header and its rows — so the RON-equivalent totals stay consistent with the foreign-currency totals shown on the PDF. ## Edit locks Four conditions block an edit: * `400 invoice_is_storno` — the row is itself a reversal. * `400 invoice_already_storned` — the row has been reversed. * `400 invoice_is_paid` — paid invoice cannot have totals changed (non-total fields like `info` and `due_at` still work). * `403 invoice_locked_efactura` — ANAF has accepted or validated the invoice via SPV. The error payload includes `efactura_status`. `PUT` and `PATCH` are interchangeable — both route to the same handler. # Void Invoice Source: https://docs.contazen.ro/api-reference/endpoints/invoices/void POST /invoices/{id}/void Void an existing invoice # List Payments Source: https://docs.contazen.ro/api-reference/endpoints/payments/list GET /payments List all payments across invoices # Retrieve a Payment Source: https://docs.contazen.ro/api-reference/endpoints/payments/retrieve GET /payments/{id} Get details of a specific payment # Create Product Source: https://docs.contazen.ro/api-reference/endpoints/products/create POST /products Create a new product # Delete Product Source: https://docs.contazen.ro/api-reference/endpoints/products/delete DELETE /products/{id} Delete a product # List Products Source: https://docs.contazen.ro/api-reference/endpoints/products/list GET /products Retrieve a paginated list of products # Retrieve Product Source: https://docs.contazen.ro/api-reference/endpoints/products/retrieve GET /products/{id} Retrieve a specific product by ID # Update Product Source: https://docs.contazen.ro/api-reference/endpoints/products/update PUT /products/{id} Update an existing product # Create a Receipt Source: https://docs.contazen.ro/api-reference/endpoints/receipts/create POST /receipts Issue a chitanță, optionally paired with an invoice # Delete a Receipt Source: https://docs.contazen.ro/api-reference/endpoints/receipts/delete DELETE /receipts/{id} Soft-delete a chitanță and undo any linked invoice payment # List Receipts Source: https://docs.contazen.ro/api-reference/endpoints/receipts/list GET /receipts List chitanțe (cash receipts) for the firm # Retrieve a Receipt Source: https://docs.contazen.ro/api-reference/endpoints/receipts/retrieve GET /receipts/{id} Get details of a specific chitanță # Update a Receipt Source: https://docs.contazen.ro/api-reference/endpoints/receipts/update PUT /receipts/{id} Update an existing chitanță # Delete Supplier Bill Source: https://docs.contazen.ro/api-reference/endpoints/supplier-bills/delete DELETE /supplier-bills/{id} Soft-delete a supplier bill (deleted_at timestamp). ## Overview Performs a soft delete by setting `deleted_at` to the current timestamp. Deleted bills no longer appear in list or retrieve responses (they return 404\). The linked expense is left untouched. # Download e-Factura PDF Source: https://docs.contazen.ro/api-reference/endpoints/supplier-bills/efactura-pdf GET /supplier-bills/{id}/efactura-pdf Stream the ANAF-rendered PDF for a supplier bill. ## Overview Streams the e-Factura PDF for this bill. The PDF is generated on demand the first time it is requested by sending the original SPV XML to the ANAF public validator (`transformare/FACT1`); the result is cached on disk and re-served on subsequent requests. Returns **404** with `code: "efactura_not_available"` when the bill has no associated SPV message (for example, manually created bills). The `efactura_pdf_url` field on the supplier bill object gives you a direct link to this endpoint. # List Supplier Bills Source: https://docs.contazen.ro/api-reference/endpoints/supplier-bills/list GET /supplier-bills List supplier invoices received from the ANAF SPV inbox (e-Factura). ## Overview Returns the supplier invoices that Contazen has imported from your ANAF SPV inbox. This is the API equivalent of the **Facturi primite de la furnizori** view in the web app. Results span every work point under your parent company. Use the filter parameters to narrow by status, date range, supplier (CIF or name), or currency. ## Totals The response includes a `totals` block with both global aggregates (count, tax-exclusive, tax, tax-inclusive, paid, unpaid) and a `by_currency` breakdown — supplier bills frequently mix RON with foreign currencies and summing across them produces meaningless numbers. ## Reciprocal client When a row's supplier CIF matches one of your firm's clients, `client_id` is populated with that client's CzUid. This makes it easy to surface a "your supplier is also your client" hint in the UI. # Mark Bill as Paid Source: https://docs.contazen.ro/api-reference/endpoints/supplier-bills/mark-paid POST /supplier-bills/{id}/mark-paid Record a payment on a supplier bill. Supports partial payments. ## Overview Records a payment on the supplier bill. Pass `payment_amount` lower than the bill total to record a partial payment (status becomes `partial`). When the amount is omitted, the full `tax_inclusive` total is used. `payment_date` defaults to today. ## Linked expense If the bill is linked to a `FirmExpense`, that expense is kept in sync — its `is_paid` flag and `paid_at` date are updated to match. # Mark Bill as Unpaid Source: https://docs.contazen.ro/api-reference/endpoints/supplier-bills/mark-unpaid POST /supplier-bills/{id}/mark-unpaid Reset a supplier bill to unpaid status, clearing payment fields. ## Overview Clears the bill's `payment_date` and `payment_amount`, sets `status` to `unpaid`, and (if the bill is linked to an expense) flips that expense back to unpaid. # Retrieve Supplier Bill Source: https://docs.contazen.ro/api-reference/endpoints/supplier-bills/retrieve GET /supplier-bills/{id} Get a single supplier bill, including supplier balance and reciprocal client. ## Overview Returns one supplier bill by its CzUid. The response includes a `supplier_balance` block aggregating the supplier's outstanding invoices across all your work points, plus the next due invoice (if any). ## Default expansions The detail endpoint expands `supplier`, `expense`, `client`, `efactura_message`, and `lines` by default. Pass `expand=` (empty) to skip all expansions, or `expand=supplier,lines` to choose a subset. # Sync from SPV Source: https://docs.contazen.ro/api-reference/endpoints/supplier-bills/sync POST /supplier-bills/sync Pull fresh supplier invoices from the ANAF SPV inbox. ## Overview Triggers a synchronous SPV pull for the authenticated firm: fetches messages from ANAF, downloads any missing XMLs, and imports received invoices. The call blocks until ANAF responds — typically 5–30 seconds, occasionally more. ## Concurrency A per-firm file lock prevents concurrent syncs. If a sync is already running for the same firm the endpoint returns HTTP **429** with `code: "sync_in_progress"` and a `Retry-After` header. ## Choosing the window Pass `days` (1–60) to fetch a fixed window. When omitted, Contazen picks the window automatically based on the time since the last successful sync (1 → 60 days). # Create Supplier Source: https://docs.contazen.ro/api-reference/endpoints/suppliers/create POST /suppliers Create a new supplier # Delete Supplier Source: https://docs.contazen.ro/api-reference/endpoints/suppliers/delete DELETE /suppliers/{id} Delete a supplier # List Suppliers Source: https://docs.contazen.ro/api-reference/endpoints/suppliers/list GET /suppliers Retrieve a paginated list of all suppliers # Retrieve Supplier Source: https://docs.contazen.ro/api-reference/endpoints/suppliers/retrieve GET /suppliers/{id} Retrieve a specific supplier by ID # Update Supplier Source: https://docs.contazen.ro/api-reference/endpoints/suppliers/update PUT /suppliers/{id} Update an existing supplier # Create VAT Rate Source: https://docs.contazen.ro/api-reference/endpoints/vat-rates/create POST /vat-rates Add a firm-scoped custom VAT rate. ## Overview Adds a custom VAT rate row to `firm_vat`. If a row with the same `percent` already exists, the existing row is returned with `created: false` — the endpoint is idempotent by percent. When `description` is omitted, the standard description from `FirmVat::getOptions()` is used for matching percents (e.g. `19% - TVA`); otherwise a generic `% - TVA` label is generated. ## Preconditions * `percent` must be between `0` and `100`. * Non-VAT-payer firms (`vat_type = none` on the parent) get `400 non_vat_payer` — standard rates from the catalog still appear in the `available` array returned by `GET /vat-rates`, but custom rows are rejected. # Delete VAT Rate Source: https://docs.contazen.ro/api-reference/endpoints/vat-rates/delete DELETE /vat-rates/{id} Remove a firm-scoped VAT rate. ## Overview Removes a custom `firm_vat` row. Fails with `409 in_use` when the rate is still referenced by any bill or notice row — use `GET /vat-rates` and check `can_be_deleted` before calling this endpoint. # List VAT Rates Source: https://docs.contazen.ro/api-reference/endpoints/vat-rates/list GET /vat-rates List firm-scoped VAT rates plus the full Romanian catalog. ## Overview Returns two sets of rates: * **`data`** — custom `firm_vat` rows the firm has added (typically used for non-standard or legacy percents). * **`available`** — the full Romanian VAT catalog filtered for the firm's VAT-payer type, including the 2025-08-01 rate changes. Mobile and web invoice forms merge the two and dedupe by `percent`. `default_percent` mirrors `FirmVat::getDefaultPercent()` and is the rate a freshly-drafted invoice row should use. # Update VAT Rate Source: https://docs.contazen.ro/api-reference/endpoints/vat-rates/update PUT /vat-rates/{id} Update the description of a firm VAT rate. ## Overview Only `description` is mutable. `percent` is immutable — changing it would silently invalidate every bill line that already references this rate and break the historical audit trail. To replace a rate with a new percent, add a new rate and migrate referencing invoices manually. # API Reference Source: https://docs.contazen.ro/api-reference/introduction Complete reference for all Contazen API endpoints ## Overview The Contazen API provides programmatic access to all features of the Contazen invoicing platform. This reference documents every endpoint, parameter, and response. ## Base URL All API endpoints are relative to: ``` https://api.contazen.ro/v1 ``` ## Available Resources Manage your customers and business partners * List, search, and filter clients * Create B2B and B2C clients * Update client information * Track client history Create and manage invoices * Generate fiscal documents * Send invoices via email * Track payment status * E-Factura integration Manage your product catalog * Create products and services * Set pricing and VAT rates * Organize by categories * Track inventory Process and track payments * Record payments * Generate receipts * Payment reconciliation * Payment reminders ## Authentication To test the API endpoints in this documentation, you need to add your API key in the **Authorization** field of the API playground. Enter it as: `Bearer sk_live_YOUR_API_KEY` ## Request Format ### Headers All requests must include: ```http theme={null} Authorization: Bearer YOUR_API_KEY Content-Type: application/json Accept: application/json ``` ### Optional Headers ```http theme={null} Accept-Language: ro ``` ## Response Format All responses follow this structure: ### Success Response ```json theme={null} { "success": true, "data": { // Response data }, "meta": { "version": "v1", "response_time": "12.45ms" } } ``` ### Error Response ```json theme={null} { "success": false, "error": { "message": "Human-readable error message", "type": "error_type", "code": "error_code", "status": 400 }, "meta": { "version": "v1", "response_time": "12.5ms" } } ``` ## Common Parameters ### Pagination Most list endpoints support pagination: | Parameter | Type | Default | Description | | ---------- | ------- | ------- | ------------------------- | | `page` | integer | 1 | Page number | | `per_page` | integer | 50 | Items per page (max: 100) | ### Filtering List endpoints typically support: | Parameter | Type | Description | | ------------ | ------ | --------------------- | | `search` | string | Search term | | `sort` | string | Sort field | | `order` | string | Sort order (asc/desc) | | `start_date` | string | Filter by date range | | `end_date` | string | Filter by date range | ### Expanding Some endpoints support expanding nested objects: ``` GET /api/v1/invoices/inv_123?expand[]=lines&expand[]=payments ``` ## OpenAPI Specification Download our complete OpenAPI specification: Machine-readable API specification for code generation and testing ## Need Help? Import our Postman collection for easy API testing Contact our support team for assistance # Testing Your Integration Source: https://docs.contazen.ro/api-reference/testing Best practices for testing the Contazen API without affecting production data ## Overview The Contazen API does not have a dedicated sandbox environment. However, there are several strategies you can use to safely test your integration without affecting real business data. ## Recommended Testing Strategies ### 1. Create a Test Invoice Series The most effective approach is to create a dedicated invoice series for testing purposes. Log into your Contazen account and navigate to [**Settings → Invoice Series**](https://app.contazen.ro/firm/number/index) Create a new series with a clear test identifier, for example: * Series name: `TEST` * Starting number: `1` * Format: `TEST-{number}` When creating test invoices via API, specify your test series: ```json theme={null} { "series_id": "YOUR_TEST_SERIES_ID", "client_id": "test_client_123", "items": [...] } ``` To manage test invoices in Contazen: **Individual Deletion:** * Navigate to the invoices list * Filter by your TEST series * Click on each invoice to open it * Delete only if the invoice is: * Still in draft status * Already voided (has a storno) * Is a storno invoice itself **Important Notes:** * Contazen does not support bulk deletion of invoices * Paid or partially paid invoices cannot be deleted * For non-draft invoices, create a storno (void) instead * Consider keeping test invoices in draft status for easy cleanup ### 2. Create Test Clients Create dedicated test clients to avoid mixing test data with real customers: ```json theme={null} { "name": "TEST - Demo Company SRL", "cui": "RO12345678", "email": "test@example.com", "address": "Test Address 123", "city": "București, Sector 1" } ``` Prefix test client names with "TEST -" to easily identify and filter them later. ### 3. Use Draft Mode Always create test invoices as drafts first: ```json theme={null} { "is_draft": true, "client_id": "test_client_123", "items": [...] } ``` Draft invoices: * Don't affect invoice numbering * Can be deleted without creating gaps * Won't be submitted to ANAF e-Factura * Can be easily identified and bulk deleted ### 4. E-Factura Test Environment (SPV) For testing e-Factura integration, use the SPV test environment: ```json theme={null} { "efactura_enabled": true, "spv_environment": "test", "client_id": "client_123", "items": [...] } ``` E-Factura testing requires OAuth authentication to be completed. Configure it at: **Settings → E-Factura → ANAF OAuth** #### SPV Environment Options | Environment | Description | Use Case | | ----------- | -------------------------------- | ------------------- | | `live` | Production ANAF system (default) | Real invoices | | `test` | SPV test environment | Integration testing | The SPV test environment allows you to test the full e-Factura flow without submitting real invoices to ANAF. ## Testing Checklist Before going to production, test these scenarios: * ✅ Create draft invoices * ✅ Create final invoices with test series * ✅ Test with different VAT rates (21%, 19%, 11%, 9%, 5%, 0%) * ✅ Test reverse charge (taxare inversă) * ✅ Test multi-currency invoices * ✅ Test with existing products * ✅ Test creating new products * ✅ Create B2B clients (with CUI) * ✅ Create B2C clients (individuals) * ✅ Test CUI validation * ✅ Test duplicate detection * ✅ Test București sector validation * ✅ Update client information * ✅ List and filter clients * ✅ Test with SPV environment first * ✅ Verify OAuth is configured * ✅ Test invoice submission * ✅ Handle submission errors * ✅ Check message retrieval * ✅ Test with all required UBL codes * ✅ Send invoices by email * ✅ Download PDF (both modes) * ✅ Record payments * ✅ Void invoices * ✅ Product CRUD operations * ✅ Test pagination * ✅ Test filtering and search ## Best Practices ### 1. Separate Test Data * Use consistent prefixes: `TEST -` for clients, `TEST` for invoice series * Use a dedicated email for test notifications * Document your test client and invoice IDs for easy cleanup ### 2. Rate Limiting The API allows 100 requests per minute. During testing: * Add delays between requests * Use pagination properly * Cache responses when possible ### 3. Error Handling Test error scenarios: * Invalid API key * Missing required fields * Invalid CUI formats * Duplicate clients * Payment on paid invoices * Deleting used products ### 4. Data Cleanup Create a cleanup script to remove test data: ```javascript theme={null} // Example: Delete all TEST series invoices const testInvoices = await getInvoices({ series: 'TEST', per_page: 100 }); for (const invoice of testInvoices) { if (invoice.is_draft) { await deleteInvoice(invoice.id); } else { await voidInvoice(invoice.id, 'Test cleanup'); } } ``` ## OAuth Configuration for E-Factura E-Factura features require OAuth authentication with ANAF. Without proper OAuth configuration, e-Factura submissions will fail. ### Setup Steps 1. Navigate to **Settings → E-Factura** in your Contazen account 2. Click **Configure ANAF OAuth** 3. Complete the OAuth flow with your ANAF credentials 4. Verify the connection status shows "Connected" ### Testing OAuth Integration ```bash theme={null} # Create an invoice with e-Factura enabled curl -X POST https://api.contazen.ro/v1/invoices \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "client_123", "efactura_enabled": true, "spv_environment": "test", "items": [{ "description": "Test Service", "quantity": 1, "price": 100, "vat_rate": 21 }] }' ``` If OAuth is not configured, you'll receive: ```json theme={null} { "success": false, "error": { "message": "E-Factura OAuth not configured. Please complete OAuth setup at /firm/index/efactura", "type": "configuration_error", "code": "oauth_not_configured" } } ``` ## Production Readiness Before switching to production: 1. ✅ Remove all test data 2. ✅ Switch `spv_environment` to `"live"` (or omit it) 3. ✅ Update API keys if using different ones 4. ✅ Verify OAuth is connected for production 5. ✅ Test with one real invoice first 6. ✅ Monitor error logs closely Remember: There's no way to "undo" a submitted e-Factura invoice in production. Always test thoroughly in SPV environment first. # Authentication Source: https://docs.contazen.ro/authentication Secure your API requests with Bearer token authentication ## Overview The Contazen API uses Bearer token authentication. All API requests must include a valid API key in the `Authorization` header. ## API Keys ### Key Type **Format**: `sk_live_` + 24 characters Use for production. All actions affect real data and can't be undone. ### Creating API Keys Log in to your Contazen account and navigate to [Settings → API Keys](https://app.contazen.ro/settings/api-keys) Click "Create New API Key" and configure: * **Name**: Identify your key (e.g., "Production Server") * **Permissions**: Select required scopes * **IP Restrictions**: Optionally limit to specific IPs Your secret key will only be shown once. Store it securely in your environment variables. ## Testing in Documentation To test API endpoints directly in this documentation: 1. Navigate to any API endpoint page (e.g., [List Clients](/api-reference/endpoints/clients/list)) 2. Look for the **"Try it"** section 3. In the **Authorization** field, enter: `Bearer sk_live_YOUR_API_KEY` 4. Fill in any required parameters 5. Click **"Send Request"** to test with real data ## Making Authenticated Requests Include your API key in the `Authorization` header: ``` Authorization: Bearer YOUR_API_KEY ``` ```bash cURL theme={null} curl https://api.contazen.ro/v1/clients \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const headers = { 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Content-Type': 'application/json' }; fetch('https://api.contazen.ro/v1/clients', { headers }) .then(response => response.json()) .then(data => console.log(data)); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Content-Type': 'application/json' } response = requests.get( 'https://api.contazen.ro/v1/clients', headers=headers ) ``` ```php PHP theme={null} $headers = [ 'Authorization: Bearer sk_live_YOUR_API_KEY', 'Content-Type: application/json' ]; $ch = curl_init('https://api.contazen.ro/v1/clients'); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); ``` ## Security Best Practices Never hardcode API keys in your source code. Use environment variables instead: ```javascript theme={null} // Good const apiKey = process.env.CONTAZEN_API_KEY; // Bad const apiKey = 'sk_live_abc123...'; // Never do this! ``` Limit API key usage to specific IP addresses: * Single IP: `192.168.1.1` * CIDR range: `192.168.1.0/24` * Multiple IPs: Add one per line Create a rotation schedule: 1. Create new API key 2. Update your application 3. Monitor for issues 4. Revoke old key after confirming ## Multi-Work-Point Access API keys have access to all work points (branches) within the same parent company. This allows you to manage data across multiple locations with a single API key. To specify a work point for an API request, include the `work_point_id` parameter: ```bash cURL theme={null} curl https://api.contazen.ro/v1/invoices \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -d "work_point_id=WORK_POINT_ID" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.contazen.ro/v1/invoices', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ work_point_id: 'WORK_POINT_ID', // ... other invoice data }) }); ``` ## Authentication Errors When authentication fails, you'll receive a `401 Unauthorized` response: ```json theme={null} { "success": false, "error": { "message": "Invalid API key", "type": "authentication_error", "code": "invalid_api_key", "status": 401 }, "meta": { "version": "v1", "response_time": "2.3ms" } } ``` ### Common Error Codes | Code | Description | Solution | | -------------------------- | ---------------------------- | ---------------------- | | `missing_api_key` | No Authorization header | Include the header | | `invalid_api_key` | Key doesn't exist | Check your key | | `expired_api_key` | Key has expired | Create a new key | | `ip_restricted` | Request from unauthorized IP | Update IP whitelist | | `insufficient_permissions` | Key lacks required scope | Update key permissions | ## Testing Authentication Verify your setup with this simple test: ```bash cURL theme={null} # Test authentication curl -I https://api.contazen.ro/v1/clients \ -H "Authorization: Bearer YOUR_API_KEY" # Success: HTTP 200 # Failed: HTTP 401 ``` ```javascript Node.js theme={null} // Test authentication async function testAuth() { try { const response = await fetch('https://api.contazen.ro/v1/clients', { headers: { 'Authorization': `Bearer ${process.env.CONTAZEN_API_KEY}` } }); if (response.ok) { console.log('✅ Authentication successful'); } else { console.log('❌ Authentication failed:', response.status); } } catch (error) { console.error('Error:', error); } } testAuth(); ``` ## Troubleshooting Some servers strip the Authorization header. Add to `.htaccess`: ```apache theme={null} RewriteEngine On RewriteCond %{HTTP:Authorization} ^(.*) RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] ``` For browser requests, the API includes CORS headers: ``` Access-Control-Allow-Origin: * Access-Control-Allow-Headers: Authorization, Content-Type ``` ## Need Help? If you're having authentication issues: 1. Double-check your API key 2. Verify you're using a valid API key 3. Check rate limits 4. Review IP restrictions 5. Contact support with your request ID # e-Factura Integration Source: https://docs.contazen.ro/concepts/efactura Complete guide to integrating with the Romanian e-Factura (SPV) system ## Overview e-Factura is Romania's electronic invoicing system managed by ANAF (National Agency for Fiscal Administration). All invoices (both B2B and B2C) must be reported to the SPV (Spațiul Privat Virtual) platform. Contazen provides seamless integration with the e-Factura system, handling the OAuth2 authentication, XML generation, and submission processes automatically. ## Prerequisites Before using e-Factura features through the API, ensure your account is properly configured: 1. **ANAF OAuth2 Setup**: Complete the OAuth2 authorization flow in your [Contazen dashboard](https://app.contazen.ro/firm/index/efactura) 2. **Environment Selection**: Choose between `test` (sandbox) or `live` (production) mode 3. **Proper Firm Registration**: Ensure your firm details (CUI, address, etc.) are accurate ## Setup Process ### 1. Check e-Factura Status First, verify your e-Factura configuration using the Settings endpoint: ```bash cURL theme={null} curl -X GET "https://api.contazen.ro/v1/settings" \ -H "Authorization: Bearer your_api_key_here" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.contazen.ro/v1/settings', { headers: { 'Authorization': 'Bearer your_api_key_here', 'Content-Type': 'application/json' } }); const settings = await response.json(); console.log('e-Factura Status:', settings.data.efactura); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.contazen.ro/v1/settings', headers={'Authorization': 'Bearer your_api_key_here'} ) settings = response.json() print('e-Factura Status:', settings['data']['efactura']) ``` ```php PHP theme={null} $ch = curl_init('https://api.contazen.ro/v1/settings'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer your_api_key_here' ]); $response = curl_exec($ch); $settings = json_decode($response, true); echo 'e-Factura Status: ' . json_encode($settings['data']['efactura']); ``` ### 2. e-Factura Settings Response The settings response includes comprehensive e-Factura status information: ```json theme={null} { "success": true, "data": { "efactura": { "enabled": true, "oauth_configured": true, "test_mode_available": true, "live_mode_available": true, "oauth_created_at": "2024-01-15T10:30:00Z", "oauth_expires_at": "2024-07-15T10:30:00Z", "settings": { "environment": "test", "auto_send": false, "auto_send_timing": "immediately", "send_to_public_institutions": true } } } } ``` ### 3. Manual OAuth Setup If `oauth_configured` is `false`, you'll need to complete the OAuth setup in the Contazen dashboard before using the API endpoints. ## Creating e-Factura Compatible Invoices ### Invoice Requirements For invoices to be eligible for e-Factura submission: 1. **Document Type**: Must be `fiscal` 2. **Client Type**: B2B with valid Romanian CUI or B2C (CNP is optional, use `-` if not available) 3. **Complete Data**: All required fields must be present 4. **Status**: Invoice must be finalized (not draft) 5. **UBL Compliance**: Items should include proper UBL unit codes ### UBL Unit Codes for e-Factura The Romanian e-Factura system requires UBL (Universal Business Language) unit codes for proper classification. Contazen automatically handles this with intelligent defaults and mappings. #### Default Behavior * **Default Unit**: If no `ubl_um` is specified, defaults to **H87** (bucată/piece) * **Auto-mapping**: System maps common Romanian units to UBL codes automatically * **Validation**: Invalid codes generate warnings but don't block invoice creation * **e-Factura Status**: `efactura_enabled` defaults to `true` for fiscal invoices #### Common UBL Unit Codes UBL codes are case-insensitive. Both `H87` and `h87` work correctly. | Code | Description | Romanian | Best Used For | | ------- | ----------- | --------- | -------------------------- | | **H87** | Piece/Item | bucată | Physical products, default | | **HUR** | Hour | oră | Time-based services | | **DAY** | Day | zi | Daily rates, rentals | | **MON** | Month | lună | Monthly subscriptions | | **ANN** | Year | an | Annual licenses | | **KGM** | Kilogram | kilogram | Weight-based products | | **MTR** | Meter | metru | Length measurements | | **LTR** | Liter | litru | Liquids, chemicals | | **KMT** | Kilometer | kilometru | Transport, delivery | ### Example Invoice Creation #### Basic Invoice with UBL Units ```javascript Node.js theme={null} // B2B Invoice Example const b2bInvoiceData = { client_data: { type: "b2b", name: "Example SRL", cui: "RO12345678", address: "Str. Exemplu Nr. 1", city: "București", county: "București", email: "contact@example.com" }, items: [ { description: "Servicii consultanță IT", quantity: 10, price: 150.00, vat_rate: 19, unit: "ore", ubl_um: "HUR" // UBL code for hours } ], document_type: "fiscal", efactura_enabled: true, observations: "Factură pentru servicii IT" }; const response = await fetch('https://api.contazen.ro/v1/invoices', { method: 'POST', headers: { 'Authorization': 'Bearer your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify(b2bInvoiceData) }); const invoice = await response.json(); console.log('Invoice created:', invoice.data.id); // B2C Invoice Example const b2cInvoiceData = { client_data: { type: "b2c", name: "Ion Popescu", cnp: "1234567890123", // Optional - use "-" if not available address: "Str. Exemplu Nr. 1", city: "București", county: "București", email: "ion.popescu@example.com" }, items: [ { description: "Produs electronic", quantity: 1, price: 500.00, vat_rate: 19, unit: "buc", ubl_um: "H87" } ], document_type: "fiscal", efactura_enabled: true }; ``` ```python Python theme={null} import requests # B2B Invoice Example b2b_invoice_data = { "client_data": { "type": "b2b", "name": "Example SRL", "cui": "RO12345678", "address": "Str. Exemplu Nr. 1", "city": "București", "county": "București", "email": "contact@example.com" }, "items": [ { "description": "Servicii consultanță IT", "quantity": 10, "price": 150.00, "vat_rate": 19, "unit": "ore" } ], "document_type": "fiscal", "efactura_enabled": True, "observations": "Factură pentru servicii IT" } response = requests.post( 'https://api.contazen.ro/v1/invoices', headers={'Authorization': 'Bearer your_api_key_here'}, json=b2b_invoice_data ) invoice = response.json() print('Invoice created:', invoice['data']['id']) # B2C Invoice Example b2c_invoice_data = { "client_data": { "type": "b2c", "name": "Ion Popescu", "cnp": "1234567890123", # Optional - use "-" if not available "address": "Str. Exemplu Nr. 1", "city": "București", "county": "București", "email": "ion.popescu@example.com" }, "items": [ { "description": "Produs electronic", "quantity": 1, "price": 500.00, "vat_rate": 19, "unit": "buc", "ubl_um": "H87" } ], "document_type": "fiscal", "efactura_enabled": True } ``` #### Advanced e-Factura Compliance For full e-Factura compliance, include additional UBL classification codes: ```javascript Node.js theme={null} const advancedInvoiceData = { client_data: { type: "b2b", name: "Tech Solutions SRL", cui: "RO98765432", address: "Bd. Magheru Nr. 15", city: "București", county: "București" }, items: [ { description: "Laptop DELL XPS 15", quantity: 2, price: 5000.00, vat_rate: 19, unit: "bucăți", ubl_um: "H87", // UBL unit: piece ubl_nc: "84713000", // Combined Nomenclature code ubl_cpv: "30213100-6", // Common Procurement Vocabulary vat_key: "S" // Standard VAT rate classification }, { description: "Installation and setup service", quantity: 8, price: 200.00, vat_rate: 19, unit: "ore", ubl_um: "HUR", // UBL unit: hour ubl_cpv: "72253000-3" // CPV for computer support services } ], document_type: "fiscal", efactura_enabled: true }; const response = await fetch('https://api.contazen.ro/v1/invoices', { method: 'POST', headers: { 'Authorization': 'Bearer your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify(advancedInvoiceData) }); ``` ```python Python theme={null} advanced_invoice_data = { "client_data": { "type": "b2b", "name": "Tech Solutions SRL", "cui": "RO98765432", "address": "Bd. Magheru Nr. 15", "city": "București", "county": "București" }, "items": [ { "description": "Laptop DELL XPS 15", "quantity": 2, "price": 5000.00, "vat_rate": 19, "unit": "bucăți", "ubl_um": "H87", # UBL unit: piece "ubl_nc": "84713000", # Combined Nomenclature code "ubl_cpv": "30213100-6", # Common Procurement Vocabulary "vat_key": "S" # Standard VAT rate }, { "description": "Installation and setup service", "quantity": 8, "price": 200.00, "vat_rate": 19, "unit": "ore", "ubl_um": "HUR", # UBL unit: hour "ubl_cpv": "72253000-3" # CPV for computer support } ], "document_type": "fiscal", "efactura_enabled": True } ``` ### UBL Field Reference When creating invoice items for e-Factura, you can include these UBL-related fields: | Field | Type | Required | Description | Example | | --------- | ------ | -------- | ----------------------------- | --------------------------- | | `unit` | string | No | Human-readable unit name | `"ore"`, `"bucăți"`, `"kg"` | | `ubl_um` | string | No | UBL unit code for e-Factura | `"HUR"`, `"H87"`, `"KGM"` | | `ubl_nc` | string | No | Combined Nomenclature code | `"84713000"` | | `ubl_cpv` | string | No | Common Procurement Vocabulary | `"30213100-6"` | | `vat_key` | string | No | VAT classification key | `"S"`, `"E"`, `"O"` | **Auto-mapping**: If you only provide `unit` (e.g., "ore"), Contazen automatically maps it to the correct UBL code (`HUR`). The system includes pre-configured mappings for common Romanian units. ### UBL Validation Rules * UBL codes are **case-insensitive** (`HUR`, `hur`, `Hur` all work) * Invalid codes generate warnings but **don't block** invoice creation * System defaults to `H87` if invalid code is provided * All validation happens server-side automatically * System checks your firm's custom unit mappings first * Falls back to built-in mappings for common units: * `"ore"` → `"HUR"` * `"bucăți"`, `"buc"` → `"H87"` * `"kg"`, `"kilogram"` → `"KGM"` * `"metru"`, `"m"` → `"MTR"` * Custom mappings can be configured in your dashboard * Every invoice item **must have** a UBL unit code for e-Factura * System ensures compliance by providing defaults * Discounts automatically use `H87` (piece) * All codes are validated against UBL standards ### UBL Resources and References Complete list of UBL unit codes from PEPPOL documentation EU database for product classification codes CPV codes for public procurement classification Official Romanian e-Factura documentation **Quick Start**: For most use cases, you only need `ubl_um`. The system will handle the rest automatically. Use `HUR` for time-based services and `H87` for physical products. ## Sending Invoices to SPV ### Basic SPV Submission Once you have a fiscal invoice, submit it to the e-Factura system: ```bash cURL theme={null} curl -X POST "https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e/send-to-spv" \ -H "Authorization: Bearer your_api_key_here" \ -H "Content-Type: application/json" ``` ```javascript Node.js theme={null} const response = await fetch(`https://api.contazen.ro/v1/invoices/${invoiceId}/send-to-spv`, { method: 'POST', headers: { 'Authorization': 'Bearer your_api_key_here', 'Content-Type': 'application/json' } }); const result = await response.json(); if (result.success) { console.log('Submitted to SPV:', result.data.submission_id); console.log('Status:', result.data.status); } else { console.error('Submission failed:', result.error.message); } ``` ```python Python theme={null} response = requests.post( f'https://api.contazen.ro/v1/invoices/{invoice_id}/send-to-spv', headers={'Authorization': 'Bearer your_api_key_here'} ) result = response.json() if result['success']: print('Submitted to SPV:', result['data']['submission_id']) print('Status:', result['data']['status']) else: print('Submission failed:', result['error']['message']) ``` ### Environment Override You can override the default environment (test/live) for specific submissions: ```bash theme={null} curl -X POST "https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e/send-to-spv?environment=live" \ -H "Authorization: Bearer your_api_key_here" ``` ### Successful Response ```json theme={null} { "success": true, "data": { "object": "efactura_submission", "submission_id": "spv_abc123def456", "status": "submitted", "submitted_at": "2024-01-20T14:30:00Z", "environment": "test" } } ``` ## Tracking Submission Status ### Using Expand Parameter Retrieve invoice details with e-Factura status: ```bash theme={null} curl -X GET "https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e?expand[]=efactura" \ -H "Authorization: Bearer your_api_key_here" ``` ### Response with e-Factura Status ```json theme={null} { "success": true, "data": { "id": "inv_1a2b3c4d5e", "number": "CTZ-2024-00001", "status": "sent", "efactura": { "object": "efactura_submission", "submission_id": "spv_abc123def456", "status": "submitted", "submitted_at": "2024-01-20T14:30:00Z", "environment": "test", "response_data": { "index_incarcare": "123456789" } } } } ``` ## Voiding e-Factura Invoices ### Standard Void Process When voiding an invoice that was sent to e-Factura, the system automatically sends a cancellation message to ANAF: ```bash cURL theme={null} curl -X POST "https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e/void" \ -H "Authorization: Bearer your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "reason": "Eroare în date client", "void_date": "2024-01-20" }' ``` ```javascript Node.js theme={null} const voidData = { reason: "Eroare în date client", void_date: "2024-01-20" }; const response = await fetch(`https://api.contazen.ro/v1/invoices/${invoiceId}/void`, { method: 'POST', headers: { 'Authorization': 'Bearer your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify(voidData) }); const result = await response.json(); console.log('Invoice voided:', result.success); ``` ### Void Response ```json theme={null} { "success": true, "data": { "id": "inv_1a2b3c4d5e", "status": "cancelled", "void_reason": "Eroare în date client", "void_date": "2024-01-20", "efactura": { "status": "cancelled", "cancellation_sent": true } } } ``` ## Error Handling ### Common e-Factura Errors **Error Code**: `efactura_oauth_not_configured` **Solution**: Complete OAuth setup in dashboard before using API endpoints. **Error Code**: `already_sent_to_spv` **Solution**: Check invoice status before attempting submission. **Error Code**: `invalid_document_type` **Solution**: Only fiscal invoices can be sent to e-Factura. **Error Code**: `invoice_is_draft` **Solution**: Finalize the invoice before submitting to SPV. ### Error Response Example ```json theme={null} { "success": false, "error": { "message": "e-Factura OAuth not configured", "type": "invalid_request_error", "code": "efactura_oauth_not_configured", "status": 400 } } ``` ### Retry Logic for Failures Implement retry logic for temporary failures: ```javascript theme={null} async function submitToSpvWithRetry(invoiceId, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(`https://api.contazen.ro/v1/invoices/${invoiceId}/send-to-spv`, { method: 'POST', headers: { 'Authorization': 'Bearer your_api_key_here', 'Content-Type': 'application/json' } }); const result = await response.json(); if (result.success) { return result; } // Don't retry client errors if (response.status >= 400 && response.status < 500) { throw new Error(result.error.message); } // Retry server errors if (i < maxRetries - 1) { await sleep(Math.pow(2, i) * 1000); // Exponential backoff } } catch (error) { if (i === maxRetries - 1) throw error; } } } ``` ## Best Practices ### 1. Environment Management * **Development**: Always start with `test` environment * **Production**: Switch to `live` only after thorough testing * **Token Management**: Monitor OAuth token expiration dates ### 2. Error Handling ```javascript theme={null} async function createAndSubmitInvoice(invoiceData) { try { // Create invoice const invoice = await createInvoice(invoiceData); // Check if eligible for e-Factura if (invoice.data.document_type === 'fiscal') { // Submit to SPV const submission = await submitToSpv(invoice.data.id); return { invoice: invoice.data, efactura: submission.data }; } return { invoice: invoice.data }; } catch (error) { console.error('Invoice creation/submission failed:', error.message); // Handle specific error types if (error.code === 'efactura_oauth_not_configured') { throw new Error('Please configure e-Factura OAuth in dashboard'); } throw error; } } ``` ### 3. Batch Processing For multiple invoices, implement proper rate limiting: ```javascript theme={null} async function submitMultipleInvoices(invoiceIds) { const results = []; for (const invoiceId of invoiceIds) { try { const result = await submitToSpv(invoiceId); results.push({ invoiceId, success: true, data: result.data }); // Rate limiting - wait between submissions await sleep(1000); } catch (error) { results.push({ invoiceId, success: false, error: error.message }); } } return results; } ``` ### 4. Status Monitoring Regularly check submission status for long-running processes: ```javascript theme={null} async function monitorSubmissionStatus(invoiceId) { const response = await fetch(`https://api.contazen.ro/v1/invoices/${invoiceId}?expand[]=efactura`, { headers: { 'Authorization': 'Bearer your_api_key_here' } }); const invoice = await response.json(); const efacturaStatus = invoice.data.efactura; if (efacturaStatus) { console.log(`Invoice ${invoiceId} - SPV Status: ${efacturaStatus.status}`); return efacturaStatus.status; } return 'not_submitted'; } ``` ## Integration Checklist Complete the e-Factura OAuth setup in your Contazen dashboard Start with test environment to validate your integration Create a fiscal invoice with valid Romanian client data Test the send-to-spv endpoint with your test invoice Implement proper error handling for all scenarios Use the expand parameter to track submission status Switch to live environment when ready for production ## Support and Resources * **Dashboard**: Complete OAuth setup at [app.contazen.ro](https://app.contazen.ro) * **ANAF Documentation**: Official e-Factura technical documentation * **Support**: Contact [contact@contazen.ro](mailto:contact@contazen.ro) for integration help * **Status Page**: Monitor API status and e-Factura system availability ## Compliance Notes * **Mandatory Reporting**: All invoices (B2B and B2C) must be reported to SPV * **Deadlines**: Invoices must be submitted within legal deadlines (typically 5 days) * **Data Accuracy**: Ensure all client and invoice data is accurate before submission * **Record Keeping**: Maintain records of all e-Factura submissions for audit purposes * **B2C Requirements**: CNP is optional for B2C clients - use `-` if not available # Expanding Objects Source: https://docs.contazen.ro/concepts/expanding-objects Include related objects in API responses ## Overview Many objects in the Contazen API contain references to other objects via their IDs. By default, these references are returned as simple ID strings. However, you can request the full objects by using the `expand` parameter. ## Usage Include the `expand` parameter in your request to retrieve related objects: ```bash cURL Array Format theme={null} curl https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -G \ -d expand[]=lines \ -d expand[]=client ``` ```bash cURL Comma-separated Format theme={null} curl https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -G \ -d expand=lines,client ``` ```javascript Node.js theme={null} const response = await fetch('https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e?' + new URLSearchParams({ 'expand[]': ['lines', 'client'] }), { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e', params={'expand': ['lines', 'client']}, headers={'Authorization': 'Bearer sk_live_YOUR_API_KEY'} ) ``` ## Supported Expansions ### Invoices When retrieving invoices, you can expand the following objects: | Field | Description | | ---------- | --------------------------------------------- | | `lines` | Invoice line items with full details | | `payments` | Payment records associated with the invoice | | `client` | Full client object instead of just client\_id | | `efactura` | E-Factura submission details (if applicable) | ### Example Without Expansion ```json theme={null} { "id": "inv_1a2b3c4d5e", "object": "invoice", "client_id": "cli_9z8y7x6w5v", "lines": ["line_1", "line_2"], "total": 119.00 } ``` ### Example With Expansion ```json theme={null} { "id": "inv_1a2b3c4d5e", "object": "invoice", "client": { "id": "9z8y7x6w5v", "object": "client", "name": "Tech Solutions SRL", "email": "contact@techsolutions.ro", "cui": "RO98765432", "address": "Bd. Tehnologie nr. 100", "city": "București, Sectorul 2" }, "lines": [ { "id": "line_1", "description": "Web Development Services", "quantity": 1, "price": 100.00, "vat_rate": 19, "subtotal": 100.00, "vat_amount": 19.00, "total": 119.00 } ], "total": 119.00 } ``` ## Multiple Expansions You can expand multiple objects in a single request: ```bash theme={null} # Expand both client and lines curl https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e?expand[]=client&expand[]=lines \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ## Performance Considerations Expanding objects increases response size and processing time. Only expand objects when you need the additional data. ### Best Practices 1. **Expand selectively** - Only expand objects you actually need 2. **Cache expanded responses** - Expanded objects change less frequently 3. **Use in detail views** - Expand when showing single items, not lists 4. **Consider pagination** - Be mindful of response size when expanding in list endpoints ## List Endpoints The expand parameter works with both individual object endpoints and list endpoints: ```bash theme={null} # Get all invoices with expanded client data curl https://api.contazen.ro/v1/invoices?expand[]=client \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` When using expand with list endpoints, be aware that it will expand the objects for every item in the list, which can significantly increase response size. ## Invalid Expansions If you request an invalid expansion, it will be ignored. The API only expands valid fields: ```bash theme={null} # 'invalid_field' will be ignored curl https://api.contazen.ro/v1/invoices/inv_1a2b3c4d5e?expand[]=client&expand[]=invalid_field \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` The response will include the expanded `client` but ignore `invalid_field`. # Expense Lines & VAT Breakdown Source: https://docs.contazen.ro/concepts/expense-lines Model itemized expenses with per-line VAT rates, mixed-rate receipts, and bit-exact header totals ## Overview Expenses can be stored in two shapes: 1. **Flat** — a single amount plus one VAT rate. Fine for a utility bill, fuel ticket, or any receipt with one subtotal. This is the original shape; you send `amount` + `vat` and the server stores it directly. 2. **Itemized** — an ordered array of `items`, each with its own name, quantity, unit price, and VAT rate. This is how you model restaurant bills, supermarket receipts, or any multi-row supplier invoice where different rates apply to different goods. Both shapes round-trip through the same `Expense` object. The `items` array is always present on retrieve — for flat expenses the server synthesizes a single line so API consumers can render a uniform table. ## When to use itemized lines Send `items` whenever any of these are true: * The receipt has two or more VAT rates (common in Romanian restaurants: 11% food + 21% drinks, or supermarkets that mix 5% / 9% / 19% baskets). * You need per-line categories or per-line product associations for accounting or analytics. * The customer wants the line detail preserved for SAF-T D406 or future audit exports. For everything else, the flat path is simpler and equivalent. ## Request shape ```json Create with items theme={null} POST /expenses { "supplier_id": "sup_xyz789", "date": "2026-02-19", "currency": "RON", "items": [ { "name": "Meniul zilei", "quantity": 3, "unit_price": 28.74, "vat_rate": 11 }, { "name": "Caserolă meniu", "quantity": 6, "unit_price": 1.24, "vat_rate": 21 } ] } ``` When `items` is supplied: * `amount`, `vat`, and `vat_breakdown` on the request body are **ignored** — the server derives all three from the lines. * The expense is persisted with real line rows (`firm_expense_line`). * Header `vat` is set to the *dominant rate* — the rate with the largest net across all lines. Ties resolve to first occurrence. * `vat_breakdown` is populated automatically when two or more distinct rates are present; `null` for single-rate lines. ## Rounding rule — matches printed receipts Line aggregates are rounded to **2 decimals** before they are summed into the header. This mirrors the standard Romanian invoice convention used by `FirmBill` and ensures the header equals the sum of the visible per-line totals bit-exact. For the example above: | Line | Net (qty × unit\_price) | VAT | Gross | | ------------ | ----------------------- | ------------------------------- | ---------- | | Meniul zilei | `3 × 28.74 = 86.22` | `round(86.22 × 0.11, 2) = 9.48` | `95.70` | | Caserolă | `6 × 1.24 = 7.44` | `round(7.44 × 0.21, 2) = 1.56` | `9.00` | | **Header** | **93.66** | **11.04** | **104.70** | Sum of line gross = header gross. No 0.01 drift. This is why clients that display both a per-line table and a header total never see a mismatch. If you need 4-decimal precision for internal ledgers, compute it from the raw `quantity × unit_price` values yourself — the API intentionally exposes only the 2-decimal authoritative values. ## Response shape Retrieve always returns an `items` array — at minimum one entry. ```json GET /expenses/{id} theme={null} { "expense": { "id": "exp_abc123", "amount": { "total": "104.70", "without_vat": "93.66", "vat": "11.04", "currency": "RON" }, "vat_percent": 11, "items": [ { "id": 4201, "line_index": 0, "name": "Meniul zilei", "description": null, "quantity": 3, "unit_code": null, "unit_price": 28.74, "vat_rate": 11, "amount_wvat": 86.22, "amount_vat": 9.48, "amount": 95.70, "category_id": null, "product_id": null }, { "id": 4202, "line_index": 1, "name": "Caserolă meniu", "quantity": 6, "unit_price": 1.24, "vat_rate": 21, "amount_wvat": 7.44, "amount_vat": 1.56, "amount": 9.00, "category_id": null, "product_id": null } ], "vat_breakdown": [ { "rate": 11, "net": 86.22, "vat_amount": 9.48, "gross": 95.70 }, { "rate": 21, "net": 7.44, "vat_amount": 1.56, "gross": 9.00 } ] } } ``` ### Multi-rate without lines If you don't need per-line data but your receipt still mixes VAT rates, send `vat_breakdown` alongside `amount` / `vat` on the flat path: ```json theme={null} { "supplier_id": "sup_xyz789", "date": "2026-02-19", "amount": 104.70, "vat": 11, "vat_breakdown": [ { "rate": 11, "net": 86.22, "vat_amount": 9.48, "gross": 95.70 }, { "rate": 21, "net": 7.44, "vat_amount": 1.56, "gross": 9.00 } ] } ``` The server stores the array as JSON on the row. Retrieve returns the same breakdown and synthesizes a single `items` entry from the header totals. ### Mix VAT mode (manual VAT amount) When even the per-rate net subtotals are unreliable but you do trust the printed totals at the bottom of the receipt (typical OCR output: "TOTAL 171.68 / TVA 24.15"), use **mix mode** — the simplest path that still guarantees `total = amount + amount_vat_manual` exactly: ```json theme={null} { "supplier_id": "sup_xyz789", "date": "2026-04-25", "amount": 147.53, "vat": "mix", "amount_vat_manual": 24.15, "vat_breakdown": [ { "rate": 21, "net": 80.29, "vat_amount": 16.86, "gross": 97.15 }, { "rate": 11, "net": 66.27, "vat_amount": 7.29, "gross": 73.56 }, { "rate": 0, "net": 0.97, "vat_amount": 0.00, "gross": 0.97 } ] } ``` The server stores `amount_wvat = 147.53`, `amount_vat = 24.15`, `amount_total = 171.68`, and synthesizes one persisted line per rate group from `vat_breakdown` so SUM(lines) = header. The `vat` column on the row is stored as `0`; mix mode is signaled by the non-null `vat_breakdown` JSON column. When `vat_breakdown` is omitted, mix mode stores a single row with a synthetic `vat_rate = (amount_vat_manual / amount) × 100`. Reports lose per-rate accuracy but totals stay correct. This is the path the OCR endpoint produces when per-product detail isn't reliable on the source document. ## Update semantics `PUT /expenses/{id}` with `items` **replaces** all line rows for that expense. Send the full desired set, not a diff. Lines you omit are deleted. Omitting `items` from the update body leaves the existing lines alone and updates only the header fields you did pass (description, date, etc.). Switching an expense between flat and itemized: * Flat → itemized: send `items`; header is derived from them, legacy `amount`/`vat` on the body are ignored. * Itemized → flat: send `amount` + `vat` **and** an empty `items: []` to explicitly clear the lines. Otherwise the existing lines persist. ## Field reference Human-readable line name. Shows up in the app's line table. Positive number. Fractional quantities (e.g. `0.5 kg`) are allowed. **Net** unit price (excluding VAT). The server always treats `quantity × unit_price` as the line net; there is no per-line `with_vat` flag. Negative values are accepted for discount rows — e.g. a Romanian bon fiscal `DISCOUNT 4,40-A` rebate is persisted as a line with `unit_price: -4.40`. The sign carries through to the line's net, VAT, and gross, so the expense total matches the receipt. One of `0`, `5`, `9`, `11`, `19`, `21`. Optional longer description rendered under the line name. Optional UN/ECE unit code (e.g. `H87` for pieces, `KGM` for kilograms). Preserved round-trip but not validated. Optional per-line category override. Falls back to the expense-level `category_id` when omitted. Optional link to a product in the firm's catalog. ## e-Factura inbound → lines When an expense is reconciled from an inbound e-Factura (`FirmSupplierInvoice`), the backend reuses the `` rows from the UBL XML to build the canonical line shape. You never need to POST these manually for reconciled bills — the expense already carries `firm_expense_line` rows matching the printed invoice, and `vat_breakdown` is computed from them. If you subsequently PUT `items[]` on such an expense, the e-Factura-derived lines are replaced (standard replace semantics apply). # Invoice Lifecycle Source: https://docs.contazen.ro/concepts/invoice-lifecycle Understanding invoice states and transitions in the Contazen API ## Invoice States The Contazen API manages invoices through several distinct states, each serving a specific purpose in the invoice lifecycle. ### State Overview | Status | Description | Can Edit | Can Void | Can Delete | API Visible | | --------- | --------------------- | -------- | -------- | --------------- | ----------- | | `draft` | Invoice not finalized | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes | | `issued` | Active unpaid invoice | ❌ No | ✅ Yes | ❌ No | ✅ Yes | | `paid` | Fully paid invoice | ❌ No | ✅ Yes | ❌ No | ✅ Yes | | `overdue` | Past due date | ❌ No | ✅ Yes | ❌ No | ✅ Yes | | `voided` | Cancelled invoice | ❌ No | ❌ No | ✅ Conditional\* | ✅ Yes | | (deleted) | Removed from DB | N/A | N/A | N/A | ❌ No (404) | \*Voided invoices can only be deleted if they are the last in their series ## State Transitions ```mermaid theme={null} graph LR A[Draft] --> B[Issued] B --> C[Paid] B --> D[Overdue] B --> E[Voided] C --> E D --> C D --> E E --> F[Deleted] A --> F ``` ## Key Concepts ### Draft Invoices * Created but not finalized * Can be edited freely * No invoice number assigned * Can be deleted without voiding ### Active Invoices * Have been issued with an invoice number * Cannot be edited (immutable for compliance) * Can transition to paid, overdue, or voided states * Must be voided before deletion ### Voided Invoices * Marked as cancelled but retained for audit trail * Remain visible in API with `status: "voided"` * Cannot be edited or reactivated * Can only be deleted if last in series (sequential numbering requirement) ### Deleted Invoices * Completely removed from database * Return 404 errors on all API calls * Cannot be recovered * Only drafts and voided invoices can be deleted ## API Response Fields When retrieving invoices, these fields indicate the current state: ```json theme={null} { "status": "issued", // Current status (draft/issued/paid/overdue/voided) "is_draft": false, // true if draft "is_paid": false, // true if fully paid "is_voided": false, // true if voided/cancelled "document_type": "fiscal", // Type of invoice "number": "CTZ-2024-001" // Invoice number (null for drafts) } ``` ## Best Practices ### For Compliance 1. **Never delete active invoices** - Always void first to maintain audit trail 2. **Preserve sequential numbering** - Only delete the last voided invoice in a series 3. **Keep voided invoices** - Consider not deleting voided invoices for complete audit history ### For Integration 1. **Check status field** - Primary indicator of invoice state 2. **Handle voided invoices** - They appear in lists but with voided status 3. **Expect 404 for deleted** - Deleted invoices are gone permanently 4. **Use appropriate filters** - Filter by status to get specific invoice states ## Common Workflows ### Cancel an Invoice ```bash theme={null} # 1. Void the invoice POST /api/v1/invoices/{id}/void # 2. Optionally delete if last in series DELETE /api/v1/invoices/{id} ``` ### List Active Invoices Only ```bash theme={null} # Get issued unpaid invoices (excludes voided) GET /api/v1/invoices?status=issued # Get all non-voided invoices GET /api/v1/invoices?is_paid=0 GET /api/v1/invoices?is_paid=1 ``` ### List All Including Voided ```bash theme={null} # Default behavior includes voided invoices GET /api/v1/invoices # Filter for only voided invoices GET /api/v1/invoices?status=voided ``` ## Romanian Fiscal Requirements The API enforces Romanian fiscal regulations: * **Sequential Numbering**: Invoice numbers must be sequential without gaps * **Immutable Records**: Issued invoices cannot be modified * **Audit Trail**: Voided invoices are retained for compliance * **E-Factura Integration**: Submitted invoices have additional restrictions The distinction between voided and deleted is crucial for compliance. Voided invoices maintain the audit trail while deleted invoices are permanently removed. # Localization Source: https://docs.contazen.ro/concepts/localization Access the API in multiple languages ## Overview The Contazen API supports localization for error messages and responses. Currently, the API supports: * **English** (`en`) - Default * **Romanian** (`ro`) ## Setting the Language There are three ways to specify your preferred language: ### 1. Accept-Language Header (Recommended) Use the standard HTTP `Accept-Language` header: ```bash cURL theme={null} curl https://api.contazen.ro/v1/clients \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -H "Accept-Language: ro" ``` ```javascript Node.js theme={null} const response = await fetch('https://api.contazen.ro/v1/clients', { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Accept-Language': 'ro' } }); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.contazen.ro/v1/clients', headers={ 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Accept-Language': 'ro' } ) ``` ```php PHP theme={null} $ch = curl_init('https://api.contazen.ro/v1/clients'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer sk_live_YOUR_API_KEY', 'Accept-Language: ro' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); ``` ### 2. Query Parameter Include the `locale` parameter in the URL: ```bash theme={null} curl https://api.contazen.ro/v1/clients?locale=ro \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ### 3. Request Body For POST and PUT requests, include `locale` in the JSON body: ```bash theme={null} curl -X POST https://api.contazen.ro/v1/invoices \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "locale": "ro", "client_id": "9z8y7x6w5v", "items": [...] }' ``` ## Priority If multiple methods are used, the API follows this priority order: 1. Query parameter (`?locale=ro`) 2. Request body (`"locale": "ro"`) 3. Accept-Language header 4. Default to English (`en`) ## Response Header The API includes a `Content-Language` header in all responses to indicate the language used: ```http theme={null} HTTP/1.1 200 OK Content-Type: application/json Content-Language: ro ``` ## Localized Elements The following elements are localized: ### Error Messages ```json English Response theme={null} { "success": false, "error": { "message": "Validation failed", "type": "validation_error", "errors": { "cui": "CUI/CIF is required for B2B clients", "address": "Address is required" } } } ``` ```json Romanian Response theme={null} { "success": false, "error": { "message": "Validarea a eșuat", "type": "validation_error", "errors": { "cui": "CUI/CIF este obligatoriu pentru clienții B2B", "address": "Adresa este obligatorie" } } } ``` ### Field Labels Field validation messages are localized based on the context: | English | Romanian | | -------------------------------------- | -------------------------------------------------- | | Name is required | Numele este obligatoriu | | Invalid email format | Format email invalid | | Invoice not found | Factura nu a fost găsită | | Client not found | Clientul nu a fost găsit | | Due date cannot be before invoice date | Data scadentă nu poate fi înainte de data facturii | ## Examples ### Creating an Invoice with Romanian Errors Request: ```bash theme={null} curl -X POST https://api.contazen.ro/v1/invoices \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -H "Accept-Language: ro" \ -H "Content-Type: application/json" \ -d '{ "items": [] }' ``` Response: ```json theme={null} { "success": false, "error": { "message": "Validarea a eșuat", "type": "validation_error", "code": "invalid_request", "status": 400, "errors": { "items": "Cel puțin un articol este necesar", "client_id": "Clientul este obligatoriu" } }, "meta": { "version": "v1", "response_time": "12.34ms" } } ``` ### Retrieving Non-existent Client Request: ```bash theme={null} curl https://api.contazen.ro/v1/clients/invalid_id?locale=ro \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` Response: ```json theme={null} { "success": false, "error": { "message": "Clientul nu a fost găsit", "type": "not_found_error", "code": "resource_not_found", "status": 404 }, "meta": { "version": "v1", "response_time": "8.91ms" } } ``` ## Language Detection If no language preference is specified, the API attempts to detect the language from the `Accept-Language` header: * If the header contains `ro` (e.g., `ro-RO`, `ro`), Romanian is used * Otherwise, English is used as the default ## Best Practices 1. **Consistency** - Use the same language setting method throughout your application 2. **User Preference** - Store user language preferences and apply them to all API requests 3. **Fallback Handling** - Always handle English responses as a fallback 4. **Header Method** - Prefer the `Accept-Language` header for RESTful compliance ## Notes Success messages and data field names remain in English regardless of the locale setting. Only error messages and validation messages are localized. Not all error messages may be translated. If a translation is missing, the English version will be returned as a fallback. # Multi-Work-Point Access Source: https://docs.contazen.ro/concepts/multi-work-point Manage data across multiple business locations with a single API key ## Overview The Contazen API supports multi-work-point (multi-branch) access, allowing you to manage data across all locations of your business using a single API key. This is particularly useful for businesses with multiple offices, stores, or branches. ## How It Works When you authenticate with an API key, you automatically gain access to: * The work point where the API key was created * All other work points belonging to the same parent company **Work Point** = A physical business location or branch **Parent Company** = The legal entity that owns multiple work points ## Accessing Specific Work Points By default, API requests operate on the work point where the API key was created. To access data from a different work point, include the `work_point_id` parameter in your request. ### Getting Work Point IDs First, retrieve the list of available work points: ```bash cURL theme={null} curl https://api.contazen.ro/v1/settings \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.contazen.ro/v1/settings', { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }); const data = await response.json(); console.log(data.work_points); // List of available work points ``` ```php PHP theme={null} $client = new Contazen\Client('sk_live_YOUR_API_KEY'); $settings = $client->settings->get(); foreach ($settings->work_points as $workPoint) { echo $workPoint->id . ': ' . $workPoint->name . PHP_EOL; } ``` ## Working with Multiple Work Points ### List Invoices from All Work Points To retrieve invoices from all work points, omit the `work_point_id` parameter: ```bash cURL theme={null} curl https://api.contazen.ro/v1/invoices \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.contazen.ro/v1/invoices', { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }); const invoices = await response.json(); // Returns invoices from all accessible work points ``` ### List Invoices from Specific Work Point To retrieve invoices from a specific work point: ```bash cURL theme={null} curl https://api.contazen.ro/v1/invoices?work_point_id=wp_abc123 \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.contazen.ro/v1/invoices?work_point_id=wp_abc123', { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }); ``` ```php PHP theme={null} $client = new Contazen\Client('sk_live_YOUR_API_KEY'); // Configure default work point for all requests $client = Contazen\Client::create('sk_live_YOUR_API_KEY') ->withWorkPointId('wp_abc123') ->build(); // Or specify per request $invoices = $client->invoices->list([ 'work_point_id' => 'wp_abc123' ]); ``` ### Create Invoice for Specific Work Point When creating resources, specify the target work point: ```bash cURL theme={null} curl -X POST https://api.contazen.ro/v1/invoices \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "work_point_id": "wp_abc123", "client_id": "cli_xyz789", "items": [...] }' ``` ```javascript JavaScript theme={null} const invoice = await fetch('https://api.contazen.ro/v1/invoices', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ work_point_id: 'wp_abc123', client_id: 'cli_xyz789', items: [ { description: 'Professional Services', quantity: 1, price: 1000 } ] }) }); ``` ```php PHP theme={null} $client = new Contazen\Client('sk_live_YOUR_API_KEY'); $invoice = $client->invoices->create([ 'work_point_id' => 'wp_abc123', 'client_id' => 'cli_xyz789', 'items' => [ [ 'description' => 'Professional Services', 'quantity' => 1, 'price' => 1000 ] ] ]); ``` ## Common Use Cases ### Consolidated Reporting Generate reports across all business locations: ```javascript theme={null} async function getConsolidatedRevenue(apiKey, startDate, endDate) { const response = await fetch('https://api.contazen.ro/v1/invoices', { headers: { 'Authorization': `Bearer ${apiKey}` } }); const invoices = await response.json(); // Group by work point const revenueByLocation = {}; invoices.data.forEach(invoice => { const wpId = invoice.work_point_id; if (!revenueByLocation[wpId]) { revenueByLocation[wpId] = 0; } revenueByLocation[wpId] += invoice.total; }); return revenueByLocation; } ``` ### Centralized Client Management Share client data across all locations: ```javascript theme={null} async function findClientAcrossAllLocations(apiKey, searchTerm) { const response = await fetch( `https://api.contazen.ro/v1/clients?search=${searchTerm}`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const clients = await response.json(); return clients.data; // Returns clients from all work points } ``` ### Location-Specific Operations Process operations for a specific branch: ```javascript theme={null} class BranchManager { constructor(apiKey, workPointId) { this.apiKey = apiKey; this.workPointId = workPointId; } async createInvoice(clientId, items) { const response = await fetch('https://api.contazen.ro/v1/invoices', { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ work_point_id: this.workPointId, client_id: clientId, items: items }) }); return response.json(); } async getProducts() { const response = await fetch( `https://api.contazen.ro/v1/products?work_point_id=${this.workPointId}`, { headers: { 'Authorization': `Bearer ${this.apiKey}` } } ); return response.json(); } } // Usage const bucharest = new BranchManager('sk_live_KEY', 'wp_bucharest'); const cluj = new BranchManager('sk_live_KEY', 'wp_cluj'); ``` ## Best Practices Configure a default work point in your SDK client to avoid specifying it for every request: ```php theme={null} $client = Contazen\Client::create('sk_live_KEY') ->withWorkPointId('wp_default') ->build(); ``` Work points don't change frequently. Cache the list to reduce API calls: ```javascript theme={null} let workPointCache = null; async function getWorkPoints(apiKey) { if (!workPointCache) { const response = await fetch('https://api.contazen.ro/v1/settings', { headers: { 'Authorization': `Bearer ${apiKey}` } }); const data = await response.json(); workPointCache = data.work_points; } return workPointCache; } ``` Always handle cases where a work point might not be accessible: ```javascript theme={null} try { const invoice = await createInvoice(workPointId, data); } catch (error) { if (error.code === 'work_point_not_found') { console.error('Work point not accessible or does not exist'); } } ``` ## Limitations * API keys can only access work points within the same parent company * Some operations may be restricted to specific work points based on permissions * Work point IDs are immutable and cannot be changed once created ## Related Resources * [Authentication](/authentication) - Learn about API key management * [Settings API](/api-reference/endpoints/settings/get) - Retrieve work point information * [PHP SDK](/sdks#php) - SDK with built-in multi-work-point support # Offline Mode & Mobile Series Lock Source: https://docs.contazen.ro/concepts/offline-mode How the Contazen mobile app issues invoices with correct, gapless numbers while offline. ## The problem An invoice number sequence must be **gapless and non-duplicated** to pass a fiscal audit. That is hard when: * The mobile app is in airplane mode or has a weak signal and still needs to issue a receipt. * The same firm has a web user who could emit an invoice on the same series between the time the phone reserves a number and the time it actually syncs. Contazen solves both with a **device-bound lock** on the invoice series plus **pre-allocated reservations** carried on the device. ## How it works ```mermaid theme={null} sequenceDiagram participant M as Mobile app participant A as API participant D as DB M->>A: POST /invoice-series/{id}/lock (mobile JWT) A->>D: UPDATE firm_number SET locked_device_id = ? A-->>M: series.lock = { device_id, ... } M->>A: POST /invoice-series/{id}/reserve-numbers {count:10} A->>D: BEGIN; nextNumber() x10; INSERT firm_number_reservation x10; COMMIT A-->>M: [ {token, number:101}, {token, number:102}, ... ] Note over M: Phone goes offline — user issues 3 invoices.
App pops tokens 101/102/103 from local pool. M->>A: POST /invoices { reservation_token, number, ... } A->>D: find reservation → pending → set invoice.number_idx = reservation.number_idx A->>D: firm_number_reservation.status = consumed A-->>M: invoice created with number 101 ``` ### Lock a series [`POST /invoice-series/{id}/lock`](/api-reference/endpoints/invoice-series/lock) binds the series to the calling mobile device. While locked: * Only that device can call `POST /invoices` with `series_id` equal to this series. * Only that device can request reservations. * Web / API-key callers that try to emit get `series_locked_to_device` (403). Unlocking requires either the same device or `force: true` from an admin. ### Pre-allocate numbers The mobile app keeps a small pool (10 by default) of reservations in local storage. It tops the pool up whenever online and usage drops below 3: ```bash theme={null} POST /invoice-series/inv_abc/reserve-numbers Authorization: Bearer {jwt} Content-Type: application/json { "count": 10 } ``` The response carries `{token, number}` pairs. The app stores them in SQLite alongside each token's expiry. When the user creates an invoice while offline, the app: 1. Pops the next usable reservation and saves a draft with that number visible (e.g. "Factură #101"). 2. Enqueues a `POST /invoices` mutation in its outbox with `reservation_token` and `force_chronology: false`. 3. The outbox drains on the next network/foreground transition. ### Emit an invoice from a draft ```bash theme={null} POST /invoices Authorization: Bearer {jwt} Idempotency-Key: {localUuid} Content-Type: application/json { "series_id": "inv_abc", "reservation_token": "rsv_0f3a9b...", "client_id": "cli_def", "date": "2026-04-20", "items": [ { "description": "...", "quantity": 1, "price": 100, "vat_rate": 19 } ] } ``` The server verifies the reservation: | Check | Failure code | | ----------------------------------------------- | ---------------------------------------------------------- | | Token exists under the caller's firm | `reservation_missing` | | Token belongs to the same series as `series_id` | `reservation_series_mismatch` | | Token status is `pending` | `reservation_already_consumed` / `reservation_not_pending` | | Token has not expired | `reservation_expired` | | Token belongs to the caller's device | `reservation_device_mismatch` | On success, the invoice is created with `number_idx` equal to the reservation's pre-allocated slot. The reservation flips to `consumed` and records the `bill_id`. ### Discarded drafts If the user deletes an offline draft before it syncs, the app calls [`POST /invoice-series/reservations/{token}/release`](/api-reference/endpoints/invoice-series/release-reservation). The reservation flips to `released` — the number itself is still gone (documented gap), but the audit trail captures intent. ## Gap handling The series counter advances the moment a reservation is created, not when the invoice is actually emitted. That means expired / released reservations can become **gaps** in the sequence. Romanian fiscal regulations accept gaps when a firm can document why they occurred — keep the reservation TTL generous (default 30 days) and have the device refresh the pool often enough that expiry is rare in practice. If your jurisdiction does not accept gaps, disable the reservation feature entirely: leave series unlocked and require the mobile device to be online when emitting. The existing online-only path in `POST /invoices` still works without any changes. ## When to use the lock | Situation | Lock? | | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | Field sales: phone issues invoices at the client's premises, often without signal. | **Yes** — lock the series the sales rep uses. | | Home office: emits from laptop only. | No. | | Shop with both a POS (mobile) and back-office (web) on the same series. | **Yes** — lock to the POS device. Emit back-office invoices on a different series. | | Multi-device field team on the same series. | Not recommended — only one device at a time can hold the lock. Give each sales rep their own series. | ## Error reference (invoice creation) | HTTP | Code | Meaning | | ---- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | 403 | `series_locked_to_device` | Series is locked; caller is not a mobile device. | | 403 | `series_locked_other_device` | Series is locked to a different device than the caller. | | 409 | `reservation_already_consumed` | The token was already used — the outbox must have double-fired. Idempotency-Key should prevent this; check your client. | | 409 | `reservation_expired` | TTL has passed; allocate a fresh batch. | | 409 | `reservation_series_mismatch` | Wrong `series_id` for the token. | | 403 | `reservation_device_mismatch` | Token belongs to another device (same firm). | # Pagination Source: https://docs.contazen.ro/concepts/pagination Navigate through large datasets efficiently ## Overview All list endpoints return paginated results to ensure optimal performance and manageable response sizes. ## Request Parameters The page number to retrieve. Pages are 1-indexed. Number of items per page. Maximum value is 100. ## Response Format Paginated responses include metadata about the results: ```json theme={null} { "success": true, "data": { "object": "list", "data": [...], // Array of items "has_more": true, "total": 245, "page": 1, "per_page": 50, "total_pages": 5 }, "meta": { "version": "v1", "response_time": "23.45ms" } } ``` ### Response Fields | Field | Type | Description | | ------------- | ------- | ------------------------------------- | | `object` | string | Always "list" for paginated responses | | `data` | array | The requested items | | `has_more` | boolean | Whether more pages exist | | `total` | integer | Total number of items | | `page` | integer | Current page number | | `per_page` | integer | Items per page | | `total_pages` | integer | Total number of pages | ## Examples ### Basic Pagination Request the first page with default page size: ```bash theme={null} curl https://api.contazen.ro/v1/clients \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Custom Page Size Request 20 items per page: ```bash theme={null} curl "https://api.contazen.ro/v1/clients?per_page=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Navigate to Specific Page Get page 3 of results: ```bash theme={null} curl "https://api.contazen.ro/v1/clients?page=3&per_page=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Best Practices * Use smaller page sizes (10-20) for real-time UIs * Use larger page sizes (50-100) for batch processing * Consider network latency and processing time * Check `has_more` before fetching next page * Handle empty results gracefully * Validate page numbers are within range For frequently changing data: * Store the last item ID from each page * Use filters to get items after that ID * Prevents missing items due to insertions ## Code Examples ```javascript Node.js theme={null} async function getAllClients(apiKey) { const clients = []; let page = 1; let hasMore = true; while (hasMore) { const response = await fetch( `https://api.contazen.ro/v1/clients?page=${page}&per_page=100`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const data = await response.json(); clients.push(...data.data.data); hasMore = data.data.has_more; page++; } return clients; } ``` ```python Python theme={null} def get_all_clients(api_key): clients = [] page = 1 has_more = True while has_more: response = requests.get( f'https://api.contazen.ro/v1/clients', params={'page': page, 'per_page': 100}, headers={'Authorization': f'Bearer {api_key}'} ) data = response.json() clients.extend(data['data']['data']) has_more = data['data']['has_more'] page += 1 return clients ``` ```php PHP theme={null} function getAllClients($apiKey) { $clients = []; $page = 1; $hasMore = true; while ($hasMore) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "https://api.contazen.ro/v1/clients?page=$page&per_page=100" ); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); $clients = array_merge($clients, $data['data']['data']); $hasMore = $data['data']['has_more']; $page++; curl_close($ch); } return $clients; } ``` ## Performance Tips 1. **Cache Results**: Store paginated results when data doesn't change frequently 2. **Parallel Requests**: Fetch multiple pages simultaneously when order doesn't matter 3. **Progressive Loading**: Load initial page quickly, fetch more as needed 4. **Use Filters**: Combine pagination with filters to reduce dataset size # Error Handling Source: https://docs.contazen.ro/errors Understanding and handling API errors gracefully ## Overview Contazen uses conventional HTTP response codes to indicate the success or failure of an API request. | Range | Meaning | | ----- | ---------------------------------------------- | | `2xx` | Success - Request completed successfully | | `4xx` | Client Error - Invalid request parameters | | `5xx` | Server Error - Something went wrong on our end | ## Error Response Format All error responses follow a consistent structure: ```json theme={null} { "success": false, "error": { "message": "Human-readable error message", "type": "error_type", "code": "error_code", "status": 400 }, "meta": { "version": "v1", "response_time": "12.5ms" } } ``` ## Error Types **Type**: `authentication_error` These errors occur when there's a problem with your API key. | Code | Status | Description | | -------------------------- | ------ | ----------------------------------- | | `missing_api_key` | 401 | No Authorization header provided | | `invalid_api_key` | 401 | API key doesn't exist or is invalid | | `expired_api_key` | 401 | API key has expired | | `ip_restricted` | 401 | Request from unauthorized IP | | `insufficient_permissions` | 403 | API key lacks required permissions | **Example**: ```json theme={null} { "success": false, "error": { "message": "Invalid API key", "type": "authentication_error", "code": "invalid_api_key", "status": 401 } } ``` **Type**: `validation_error` These errors occur when request parameters fail validation. | Code | Status | Description | | ------------------------ | ------ | ------------------------------------- | | `invalid_request` | 400 | General validation failure | | `missing_required_field` | 400 | Required field not provided | | `invalid_field_value` | 400 | Field value doesn't meet requirements | | `invalid_format` | 400 | Data format is incorrect | **Example with field-specific errors**: ```json theme={null} { "success": false, "error": { "message": "Validation failed", "type": "validation_error", "code": "invalid_request", "status": 400, "errors": { "client_data.cui": "Invalid CUI format", "items.0.price": "Price must be greater than 0", "email": "Invalid email address" } } } ``` **Type**: `rate_limit_error` Returned when you exceed the allowed number of requests. | Code | Status | Description | | --------------------- | ------ | ----------------- | | `rate_limit_exceeded` | 429 | Too many requests | **Example**: ```json theme={null} { "success": false, "error": { "message": "Rate limit exceeded", "type": "rate_limit_error", "code": "rate_limit_exceeded", "retry_after": 45, "status": 429 } } ``` Check the response headers for rate limit information: * `X-RateLimit-Limit`: Maximum requests allowed * `X-RateLimit-Remaining`: Requests remaining * `X-RateLimit-Reset`: Reset time (Unix timestamp) **Type**: `resource_error` These errors relate to specific resources. | Code | Status | Description | | ------------------------- | ------ | ------------------------------------ | | `resource_not_found` | 404 | Requested resource doesn't exist | | `resource_already_exists` | 409 | Resource with same identifier exists | | `resource_locked` | 423 | Resource is locked for editing | **Example**: ```json theme={null} { "success": false, "error": { "message": "Client not found", "type": "resource_error", "code": "resource_not_found", "status": 404 } } ``` **Type**: `api_error` These indicate problems on our servers. | Code | Status | Description | | ----------------------- | ------ | ------------------------------- | | `internal_server_error` | 500 | Unexpected server error | | `service_unavailable` | 503 | Service temporarily unavailable | **Example**: ```json theme={null} { "success": false, "error": { "message": "An unexpected error occurred", "type": "api_error", "code": "internal_server_error", "status": 500, "request_id": "req_1a2b3c4d5e" } } ``` ## Handling Errors ### Best Practices Don't assume a request succeeded. Check the HTTP status code and `success` field. Store the full error response, especially the `request_id` for debugging. Implement different handling for different error types. For 5xx errors and rate limits, implement exponential backoff. ### Example Error Handling ```javascript Node.js theme={null} async function makeApiRequest(endpoint, options = {}) { try { const response = await fetch(`${API_BASE}${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', ...options.headers } }); const data = await response.json(); if (!response.ok) { // Handle different error types switch (data.error?.type) { case 'authentication_error': throw new AuthError(data.error.message); case 'validation_error': throw new ValidationError(data.error.message, data.error.errors); case 'rate_limit_error': const retryAfter = data.error.retry_after || 60; throw new RateLimitError(data.error.message, retryAfter); default: throw new ApiError(data.error.message, data.error.code); } } return data; } catch (error) { // Handle network errors if (error instanceof TypeError) { throw new NetworkError('Network request failed'); } throw error; } } // Usage with retry logic async function createInvoiceWithRetry(invoiceData, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await makeApiRequest('/invoices', { method: 'POST', body: JSON.stringify(invoiceData) }); } catch (error) { if (error instanceof RateLimitError) { // Wait and retry await sleep(error.retryAfter * 1000); continue; } if (error instanceof ApiError && i < maxRetries - 1) { // Exponential backoff for server errors await sleep(Math.pow(2, i) * 1000); continue; } throw error; } } } ``` ```python Python theme={null} import time import requests from typing import Dict, Any class ContazenAPIError(Exception): def __init__(self, message: str, error_type: str, code: str): self.message = message self.error_type = error_type self.code = code super().__init__(self.message) class ValidationError(ContazenAPIError): def __init__(self, message: str, errors: Dict[str, str]): super().__init__(message, 'validation_error', 'invalid_request') self.errors = errors class RateLimitError(ContazenAPIError): def __init__(self, message: str, retry_after: int): super().__init__(message, 'rate_limit_error', 'rate_limit_exceeded') self.retry_after = retry_after def make_api_request(endpoint: str, method: str = 'GET', data: Dict[str, Any] = None): headers = { 'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json' } response = requests.request( method=method, url=f"{API_BASE}{endpoint}", headers=headers, json=data ) response_data = response.json() if not response.ok: error = response_data.get('error', {}) error_type = error.get('type') if error_type == 'validation_error': raise ValidationError( error.get('message'), error.get('errors', {}) ) elif error_type == 'rate_limit_error': raise RateLimitError( error.get('message'), error.get('retry_after', 60) ) else: raise ContazenAPIError( error.get('message'), error_type, error.get('code') ) return response_data def create_invoice_with_retry(invoice_data: Dict[str, Any], max_retries: int = 3): for attempt in range(max_retries): try: return make_api_request('/invoices', 'POST', invoice_data) except RateLimitError as e: if attempt < max_retries - 1: time.sleep(e.retry_after) continue raise except ContazenAPIError as e: if e.error_type == 'api_error' and attempt < max_retries - 1: # Exponential backoff time.sleep(2 ** attempt) continue raise ``` ```php PHP theme={null} errorType = $errorType; $this->errorCode = $errorCode; } } class ValidationException extends ContazenAPIException { public $errors; public function __construct($message, $errors) { parent::__construct($message, 'validation_error', 'invalid_request', 400); $this->errors = $errors; } } class RateLimitException extends ContazenAPIException { public $retryAfter; public function __construct($message, $retryAfter) { parent::__construct($message, 'rate_limit_error', 'rate_limit_exceeded', 429); $this->retryAfter = $retryAfter; } } function makeApiRequest($endpoint, $method = 'GET', $data = null) { $ch = curl_init(API_BASE . $endpoint); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . API_KEY, 'Content-Type: application/json' ]); if ($method !== 'GET') { curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); if ($data) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $responseData = json_decode($response, true); if ($httpCode >= 400) { $error = $responseData['error'] ?? []; $errorType = $error['type'] ?? 'unknown_error'; switch ($errorType) { case 'validation_error': throw new ValidationException( $error['message'], $error['errors'] ?? [] ); case 'rate_limit_error': throw new RateLimitException( $error['message'], $error['retry_after'] ?? 60 ); default: throw new ContazenAPIException( $error['message'] ?? 'Unknown error', $errorType, $error['code'] ?? 'unknown', $httpCode ); } } return $responseData; } function createInvoiceWithRetry($invoiceData, $maxRetries = 3) { for ($i = 0; $i < $maxRetries; $i++) { try { return makeApiRequest('/invoices', 'POST', $invoiceData); } catch (RateLimitException $e) { if ($i < $maxRetries - 1) { sleep($e->retryAfter); continue; } throw $e; } catch (ContazenAPIException $e) { if ($e->errorType === 'api_error' && $i < $maxRetries - 1) { // Exponential backoff sleep(pow(2, $i)); continue; } throw $e; } } } ?> ``` ## Common Error Scenarios ```json theme={null} { "error": { "message": "Validation failed", "errors": { "cui": "CUI must be RO followed by 2-10 digits" } } } ``` **Solution**: Ensure CUI starts with "RO" for Romanian companies ```json theme={null} { "error": { "message": "Invoice number already exists", "code": "duplicate_invoice_number" } } ``` **Solution**: Let the system auto-generate numbers ```json theme={null} { "error": { "message": "Validation failed", "errors": { "items": "At least one item is required", "client_id": "Client is required" } } } ``` **Solution**: Check all required fields are provided ```json theme={null} { "error": { "message": "Invalid date format", "errors": { "date": "Date must be in YYYY-MM-DD format" } } } ``` **Solution**: Use ISO 8601 date format ```json theme={null} { "error": { "message": "e-Factura OAuth not configured", "code": "efactura_oauth_not_configured", "type": "invalid_request_error" } } ``` **Solution**: Complete OAuth setup in settings ```json theme={null} { "error": { "message": "Invoice already sent to SPV", "code": "already_sent_to_spv", "current_status": "submitted" } } ``` **Solution**: Check invoice status before sending ```json theme={null} { "error": { "message": "Cannot delete product with associated invoices", "code": "product_has_invoices", "invoice_count": 5 } } ``` **Solution**: Products used in invoices cannot be deleted ## Getting Help If you encounter an error you can't resolve: 1. **Check the error message and code** - They provide specific information about what went wrong 2. **Review the API documentation** - Ensure you're using the correct parameters 3. **Include the request ID** - Found in error responses for server errors 4. **Contact support** - Email [contact@contazen.ro](mailto:contact@contazen.ro) with details # Introduction Source: https://docs.contazen.ro/introduction Build powerful integrations with the Contazen invoicing platform ## Welcome to Contazen API The Contazen API is organized around REST, has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs. ## Getting Started Visit our [Quickstart guide](/quickstart) to get up and running with the Contazen API in minutes. Check out the [API Reference](/api-reference/introduction) to explore all available endpoints and parameters. ## Base URL All API requests should be made to: ``` https://api.contazen.ro/v1 ``` ## Need Help? Get help from our support team at [contact@contazen.ro](mailto:contact@contazen.ro) # Postman Collection Source: https://docs.contazen.ro/postman-collection Import our ready-to-use Postman collection for quick API testing ## Quick Start with Postman Get started with the Contazen API in seconds using our pre-configured Postman collection. Complete collection with all API endpoints and examples ## What's Included Our Postman collection includes: * ✅ **All API endpoints** - Clients, Invoices, Products, and Settings * ✅ **Pre-configured examples** - Ready-to-use request bodies * ✅ **Environment variables** - Easy configuration for your API key * ✅ **Request descriptions** - Inline documentation for each endpoint * ✅ **Test examples** - Sample test scripts for common scenarios ## Import Instructions If you haven't already, [download Postman](https://www.postman.com/downloads/) for your platform. Click the download button above to get the latest collection file. 1. Open Postman 2. Click **Import** in the top left 3. Select the downloaded `Contazen_API.postman_collection.json` file 4. Click **Import** Create a new environment with these variables: | Variable | Value | | ---------- | ---------------------------- | | `base_url` | `https://api.contazen.ro/v1` | | `api_key` | `sk_live_YOUR_API_KEY` | Replace `YOUR_API_KEY` with your actual API key from [Settings](https://app.contazen.ro/settings/api-keys). 1. Select the **Contazen API** environment 2. Navigate to **Clients → List Clients** 3. Click **Send** ## Collection Structure ``` Contazen API v1.0 ├── 🔐 Authentication │ └── Test Authentication ├── 👥 Clients │ ├── List Clients │ ├── Create Client │ ├── Retrieve Client │ ├── Update Client │ └── Delete Client ├── 📄 Invoices │ ├── List Invoices │ ├── Create Invoice │ ├── Retrieve Invoice │ ├── Send Invoice │ ├── Send to SPV │ ├── Mark Invoice Paid │ ├── Download Invoice PDF │ ├── Void Invoice │ ├── Unvoid Invoice │ └── Delete Invoice ├── 📦 Products │ ├── List Products │ ├── Create Product │ ├── Retrieve Product │ ├── Update Product │ └── Delete Product └── ⚙️ Settings └── Get Settings ``` ## Environment Variables Our collection uses environment variables for easy configuration: ```javascript theme={null} // Base URL {{base_url}} → https://api.contazen.ro/v1 // Authentication {{api_key}} → Your API key // Optional test data {{test_client_id}} → ID of a test client {{test_invoice_id}} → ID of a test invoice {{test_product_id}} → ID of a test product ``` ## Invoice Endpoints Explained ### Core Invoice Operations * **List Invoices** - Retrieve all invoices with filtering and pagination * **Create Invoice** - Create new invoices with multiple examples for different scenarios * **Retrieve Invoice** - Get detailed information about a specific invoice ### Email & Delivery * **Send Invoice** - Send invoice by email to the client * **Download Invoice PDF** - Get the invoice as a PDF file or URL ### e-Factura Operations * **Send to SPV** - Submit invoice to SPV test environment for e-Factura testing ### Payment Management * **Mark Invoice Paid** - Record full or partial payments on invoices ### Invoice Status Management * **Void Invoice** - Cancel an invoice following Romanian accounting rules * **Unvoid Invoice** - Restore a previously voided invoice * **Delete Invoice** - Permanently remove draft or voided invoices ## Example Requests ### Create Invoice with Items ```json theme={null} { "client_id": "{{test_client_id}}", "items": [ { "description": "Web Development Services", "quantity": 10, "price": 100, "vat_rate": 19 } ], "due_days": 30 } ``` ### Create B2B Client ```json theme={null} { "name": "Acme Corp SRL", "cui": "RO12345678", "email": "contact@acmecorp.ro", "address": "Str. Exemplu nr. 123", "city": "București, Sectorul 1", "county": "București" } ``` ## Testing Features The collection includes pre-request scripts and tests: ### Pre-request Scripts * Automatic timestamp generation * Dynamic data creation * Environment variable validation ### Test Examples ```javascript theme={null} // Verify successful response pm.test("Status code is 200", function () { pm.response.to.have.status(200); }); // Save response data pm.test("Save client ID", function () { var jsonData = pm.response.json(); pm.environment.set("test_client_id", jsonData.data.id); }); ``` ## Tips for Testing Create a dedicated TEST series to keep test invoices separate from production data. See our [Testing Guide](/api-reference/testing) for details. When testing, use `"is_draft": true` to create draft invoices that can be easily deleted. Use the correct VAT rates: 19% (standard), 9% (reduced), 5% (reduced), or 0% (exempt). The API allows 100 requests per minute. The collection includes retry logic for rate-limited requests. ## Troubleshooting **401 Unauthorized**: Check that your API key is correctly set in the environment variables and includes the `Bearer` prefix in the Authorization header. **SSL Certificate Errors**: If you encounter SSL errors during local testing, you can temporarily disable SSL verification in Postman settings (not recommended for production). ## Updates We regularly update our Postman collection with new endpoints and improvements. Check back periodically for the latest version. Current version: **v1.0** (Last updated: August 2025) ## Need Help? * Check our [API Reference](/api-reference) for detailed endpoint documentation * Review [common errors](/errors) and their solutions * Contact [support@contazen.ro](mailto:support@contazen.ro) for assistance # Quickstart Source: https://docs.contazen.ro/quickstart Start building with the Contazen API in under 5 minutes ## Setup your development environment Get your API key and make your first API call in minutes. Sign up for a free account at [app.contazen.ro](https://app.contazen.ro/register) if you don't have one already. Complete your company profile in the dashboard: 1. Navigate to [Company Settings](https://app.contazen.ro/firm/index/update) 2. Fill in your company details: * Company name and legal information (CUI, RC) * Address and contact information * Bank account details (for invoices) * Logo and branding Complete company setup is required before creating invoices. Navigate to [Settings → API Keys](https://app.contazen.ro/settings/api-keys) in your dashboard to create your API key. You'll receive a **Live key** (`sk_live_...`) for production use. Store it securely - it won't be shown again. Test your setup by listing your clients: ```bash cURL theme={null} curl https://api.contazen.ro/v1/clients \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" ``` ```javascript Node.js theme={null} const apiKey = 'sk_live_YOUR_API_KEY'; const response = await fetch('https://api.contazen.ro/v1/clients', { headers: { 'Authorization': `Bearer ${apiKey}` } }); const data = await response.json(); console.log(data); ``` ```python Python theme={null} import requests api_key = 'sk_live_YOUR_API_KEY' response = requests.get( 'https://api.contazen.ro/v1/clients', headers={'Authorization': f'Bearer {api_key}'} ) print(response.json()) ``` ```php PHP theme={null} $apiKey = 'sk_live_YOUR_API_KEY'; $ch = curl_init('https://api.contazen.ro/v1/clients'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $apiKey ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); print_r($data); ``` ## Create your first invoice Now let's create a complete invoice with a client in one API call: ```bash cURL theme={null} curl -X POST https://api.contazen.ro/v1/invoices \ -H "Authorization: Bearer sk_live_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_data": { "type": "b2b", "name": "Acme Corp SRL", "cui": "RO12345678", "address": "Str. Exemplu nr. 123", "city": "București", "email": "contact@acmecorp.ro" }, "items": [{ "description": "Web Development Services", "quantity": 10, "price": 100, "vat_rate": 21 }] }' ``` ```javascript Node.js theme={null} const createInvoice = async () => { const response = await fetch('https://api.contazen.ro/v1/invoices', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ client_data: { type: 'b2b', name: 'Acme Corp SRL', cui: 'RO12345678', address: 'Str. Exemplu nr. 123', city: 'București', email: 'contact@acmecorp.ro' }, items: [{ description: 'Web Development Services', quantity: 10, price: 100, vat_rate: 21 }] }) }); const invoice = await response.json(); console.log('Invoice created:', invoice.data.number); return invoice; }; ``` ```python Python theme={null} import requests def create_invoice(): url = 'https://api.contazen.ro/v1/invoices' headers = { 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json' } data = { 'client_data': { 'type': 'b2b', 'name': 'Acme Corp SRL', 'cui': 'RO12345678', 'address': 'Str. Exemplu nr. 123', 'city': 'București', 'email': 'contact@acmecorp.ro' }, 'items': [{ 'description': 'Web Development Services', 'quantity': 10, 'price': 100, 'vat_rate': 21 }] } response = requests.post(url, json=data, headers=headers) invoice = response.json() print(f"Invoice created: {invoice['data']['number']}") return invoice ``` ```php PHP theme={null} function createInvoice($apiKey) { $url = 'https://api.contazen.ro/v1/invoices'; $data = [ 'client_data' => [ 'type' => 'b2b', 'name' => 'Acme Corp SRL', 'cui' => 'RO12345678', 'address' => 'Str. Exemplu nr. 123', 'city' => 'București', 'email' => 'contact@acmecorp.ro' ], 'items' => [[ 'description' => 'Web Development Services', 'quantity' => 10, 'price' => 100, 'vat_rate' => 21 ]] ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $apiKey, 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $invoice = json_decode($response, true); echo "Invoice created: " . $invoice['data']['number']; return $invoice; } ``` ## What's next? Learn about API keys and security best practices Understand how to handle API errors gracefully Explore the clients API endpoints Deep dive into invoice creation options ## Example Response A successful invoice creation returns: ```json theme={null} { "success": true, "data": { "id": "inv_1a2b3c4d5e", "object": "invoice", "number": "CTZ-2024-00001", "document_type": "fiscal", "date": "2024-01-15", "due_date": "2024-02-14", "client": { "id": "9z8y7x6w5v", "name": "Acme Corp SRL", "cui": "RO12345678" }, "subtotal": 1000.00, "vat_amount": 210.00, "total": 1210.00, "currency": "RON", "status": "draft", "created_at": 1705334400 }, "meta": { "version": "v1", "response_time": "45.23ms" } } ``` The invoice is created as a draft by default. You can send it to your client using the [send endpoint](/api-reference/endpoints/invoices/send). # Rate Limiting Source: https://docs.contazen.ro/rate-limiting Understanding API rate limits and best practices ## Overview API requests are rate limited to ensure stability and fair usage for all users. ## Rate Limit **100 requests per minute** All API keys are subject to this rate limit to ensure fair usage and stability. ## Rate Limit Headers Every API response includes headers with rate limit information: | Header | Description | | ----------------------- | ---------------------------------------------------- | | `X-RateLimit-Limit` | The maximum number of requests allowed | | `X-RateLimit-Remaining` | The number of requests remaining | | `X-RateLimit-Reset` | The time when the rate limit resets (Unix timestamp) | ## Handling Rate Limits When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: ```json theme={null} { "success": false, "error": { "message": "Rate limit exceeded", "type": "rate_limit_error", "code": "rate_limit_exceeded", "retry_after": 45, "status": 429 } } ``` The `retry_after` field indicates how many seconds to wait before retrying. ## Best Practices When rate limited, implement exponential backoff: ```javascript theme={null} async function makeRequestWithBackoff(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const response = await fetch(url, options); if (response.status !== 429) { return response; } const retryAfter = response.headers.get('Retry-After') || Math.pow(2, i); await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); } throw new Error('Max retries exceeded'); } ``` Cache frequently accessed data to reduce API calls: * Client lists that don't change often * Product catalogs * Static configuration data Where possible, use batch operations: * Create multiple invoices in a single request * Fetch data with appropriate pagination limits Track your API usage: * Log rate limit headers * Set up alerts for high usage * Plan for traffic spikes ## Increasing Rate Limits If you need higher rate limits for your application: 1. Contact support at [contact@contazen.ro](mailto:contact@contazen.ro) 2. Provide details about your use case 3. Include your current usage patterns 4. Explain expected growth We'll work with you to ensure your application has the resources it needs. # SDKs & Libraries Source: https://docs.contazen.ro/sdks Official and community SDKs for the Contazen API ## Official SDKs Speed up your integration with our official SDKs that provide idiomatic interfaces for your programming language of choice. Official PHP SDK for the Contazen API with full type support and modern PHP features ## PHP SDK The official Contazen PHP SDK provides a simple and elegant way to interact with the Contazen API from your PHP applications. ### Installation Install the SDK using Composer: ```bash theme={null} composer require contazen/contazen-php-sdk ``` ### Quick Start ```php theme={null} clients()->create([ 'type' => 'b2b', 'name' => 'Acme Corp SRL', 'cui' => 'RO12345678', 'address' => 'Str. Exemplu nr. 123', 'city' => 'București', 'email' => 'contact@acmecorp.ro' ]); // Create an invoice $invoice = $contazen->invoices()->create([ 'client_id' => $client->id, 'items' => [ [ 'description' => 'Web Development Services', 'quantity' => 10, 'price' => 100.00, 'vat_rate' => 19 ] ] ]); // Send the invoice $contazen->invoices()->send($invoice->id, [ 'email' => 'contact@acmecorp.ro', 'message' => 'Please find attached your invoice.' ]); ``` ### Features The SDK provides full type hints and PHPDoc annotations for better IDE support and code completion. Built-in exception handling with specific exception types for different error scenarios: ```php theme={null} use Contazen\Exceptions\ApiException; use Contazen\Exceptions\ValidationException; use Contazen\Exceptions\AuthenticationException; try { $invoice = $contazen->invoices()->create($data); } catch (ValidationException $e) { // Handle validation errors $errors = $e->getErrors(); } catch (AuthenticationException $e) { // Handle authentication errors } catch (ApiException $e) { // Handle general API errors } ``` Easy pagination through large datasets: ```php theme={null} // Get all invoices with automatic pagination foreach ($contazen->invoices()->all() as $invoice) { echo $invoice->number . PHP_EOL; } // Manual pagination $invoices = $contazen->invoices()->list([ 'limit' => 50, 'starting_after' => 'inv_1a2b3c4d5e' ]); ``` Built-in webhook signature verification: ```php theme={null} use Contazen\Webhook; $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_CONTAZEN_SIGNATURE'] ?? ''; try { $event = Webhook::constructEvent( $payload, $signature, $webhookSecret ); // Handle the event switch ($event->type) { case 'invoice.paid': $invoice = $event->data; // Handle paid invoice break; } } catch (\Exception $e) { // Invalid signature http_response_code(400); exit(); } ``` ### Available Resources The PHP SDK provides access to all Contazen API resources: * **Clients**: Create, retrieve, update, delete, and list clients * **Invoices**: Full invoice management including creation, sending, voiding, and PDF generation * **Products**: Manage your product catalog * **Settings**: Access account and company settings ### Requirements * PHP 7.4 or higher * Composer * ext-curl * ext-json ### Documentation & Support View source code, report issues, and contribute Complete API documentation ## Community SDKs Community SDKs are maintained by the community and may not always be up to date with the latest API changes. We recommend checking their documentation and testing thoroughly before using in production. If you've built an SDK for the Contazen API, please [contact us](mailto:contact@contazen.ro) to have it listed here. ## Building Your Own SDK If an SDK doesn't exist for your language, you can build your own using our [OpenAPI specification](/contazen-api.openapi.yaml). Many languages have tools to generate client libraries from OpenAPI specs: * **JavaScript/TypeScript**: [openapi-generator](https://openapi-generator.tech/) * **Python**: [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) * **Go**: [oapi-codegen](https://github.com/deepmap/oapi-codegen) * **Ruby**: [openapi\_client](https://github.com/openapi-generators/openapi-ruby-client) * **Java**: [openapi-generator](https://openapi-generator.tech/) ### Example: Generating a TypeScript Client ```bash theme={null} npm install @openapitools/openapi-generator-cli -g openapi-generator-cli generate \ -i https://api.contazen.ro/openapi.yaml \ -g typescript-axios \ -o ./contazen-sdk ``` ## Need Help? If you need assistance with any SDK or have questions about integrating with the Contazen API: * Check our [API documentation](/api-reference/introduction) * Review the [authentication guide](/authentication) * Contact our support team at [contact@contazen.ro](mailto:contact@contazen.ro) # Security Source: https://docs.contazen.ro/security Security best practices and features in the Contazen API ## Overview The Contazen API implements multiple layers of security to protect your data and ensure safe integrations. This page outlines our security measures and best practices for using the API securely. ## Authentication ### API Keys * All API requests require authentication using Bearer tokens * API keys start with `sk_live_` for production and `sk_test_` for testing * Keys are tied to specific firms and have granular permissions * Never expose your API keys in client-side code or public repositories ### Best Practices * Rotate your API keys regularly * Use environment variables to store keys * Implement IP whitelisting when possible * Monitor API usage for unusual patterns ## Data Protection ### Encryption * All API traffic must use HTTPS (TLS 1.2 or higher) * Sensitive data is encrypted at rest * Database connections use SSL certificates ## Audit Logging We maintain comprehensive audit logs for security and compliance: * All API requests are logged with timestamps * Authentication attempts tracked * File uploads and modifications recorded * Logs retained for compliance periods ## Compliance The Contazen API complies with: * **GDPR** (General Data Protection Regulation) * **Romanian e-Factura** requirements * **PCI DSS** for payment processing (where applicable) ## Reporting Security Issues If you discover a security vulnerability: 1. **Do NOT** disclose it publicly 2. Email [contact@contazen.ro](mailto:contact@contazen.ro) with details 3. Include steps to reproduce if possible 4. We aim to respond ASAP ## Security Checklist for Integrations When building integrations with our API: * [ ] Store API keys securely (environment variables, secrets management) * [ ] Use HTTPS for all requests * [ ] Implement proper error handling without exposing sensitive details * [ ] Validate all inputs before sending to API * [ ] Implement retry logic with exponential backoff * [ ] Monitor for unusual activity patterns * [ ] Keep dependencies and libraries up to date * [ ] Use the principle of least privilege for API key permissions * [ ] Implement proper logging without storing sensitive data * [ ] Regular security audits of your integration # Versioning Source: https://docs.contazen.ro/versioning API versioning approach ## Current Version The Contazen API is currently at version **v1**, which is included in all API URLs: ``` https://api.contazen.ro/v1/... ``` ## Version Header Every API response includes the version in the metadata: ```json theme={null} { "success": true, "data": { ... }, "meta": { "version": "v1", "response_time": "12.45ms" } } ``` ## Stability The v1 API is stable and production-ready. We are committed to maintaining backward compatibility for all v1 endpoints. ## Backward Compatible Changes We may add the following without changing the API version: * New endpoints * New optional parameters * New response fields * Additional functionality * Additional error codes * Extended enum values * New status types * Enhanced features ## Breaking Changes If we need to make breaking changes in the future, we will: 1. Release a new API version (v2) 2. Maintain v1 alongside the new version 3. Provide clear migration documentation 4. Give advance notice to all API users Examples of breaking changes include: * Removing endpoints or required parameters * Changing response structure * Modifying authentication methods * Altering core business logic ## Best Practices Your integration should gracefully handle new fields in API responses. Don't use strict parsing that fails when encountering unknown fields. Always check the HTTP status code and the `success` field in responses. Don't assume requests succeeded. Monitor your registered email for any API updates or announcements. # Retrieve balance sheet Source: https://docs.contazen.ro/api-reference/endpoints/conta/get-balance-sheet GET /accounting/balance-sheets/{year} Get a balance sheet for a specific year, including all ANAF indicators # Retrieve fiscal profile Source: https://docs.contazen.ro/api-reference/endpoints/conta/get-profile GET /accounting/profile Get the cached ANAF v9 PlatitorTVA snapshot for the authenticated firm # List balance sheets Source: https://docs.contazen.ro/api-reference/endpoints/conta/list-balance-sheets GET /accounting/balance-sheets List all locally cached balance sheets for the firm, ordered by year # Conta overview Source: https://docs.contazen.ro/api-reference/endpoints/conta/overview Public ANAF data for the firm — fiscal profile and annual balance sheets The **Conta** module exposes the firm's public ANAF record so you can build dashboards, alerts, and analytics without scraping or hitting the ANAF webservices yourself. ## What's covered | Resource | Source | Refresh cadence | | ------------------------- | ----------------------------------------- | -------------------------------------------------------------------------- | | **Fiscal profile** | ANAF v9 PlatitorTVA (public, no OAuth) | Manual via `POST /accounting/profile/sync` | | **Annual balance sheets** | ANAF Bilanț webservice (public, no OAuth) | Manual via `POST /accounting/balance-sheets/sync`, single year or backfill | Years 2014 onward are supported. ANAF typically publishes year `N-1` between August and November of year `N`. ## What's not covered The following SPV resources are **PDF-only** at ANAF and not yet exposed through this API: D100/D300/D394 declarations, fișa rol, vector fiscal, certificat fiscal. Track the [roadmap](/changelog) for updates. ## Permissions | Action | Required permission | | ------------------------------------- | ---------------------- | | Read profile + balance sheets (`GET`) | `firm_accounting_view` | | Trigger sync (`POST .../sync`) | `firm_accounting_sync` | Both permissions are granted automatically to firm owners and admin users. ## Rate limits ANAF caps these webservices at \~1 request/second. The platform shares a single throttle across all callers (UI, API, scripts), so a backfill of 10 years takes \~11s wall time. Plan for this when scheduling syncs. # Sync balance sheets Source: https://docs.contazen.ro/api-reference/endpoints/conta/sync-balance-sheets POST /accounting/balance-sheets/sync Fetch a single year, backfill a range, or sync the latest published year # Sync fiscal profile Source: https://docs.contazen.ro/api-reference/endpoints/conta/sync-profile POST /accounting/profile/sync Refresh the fiscal profile from ANAF v9 PlatitorTVA # Enable Currency Source: https://docs.contazen.ro/api-reference/endpoints/currencies/create POST /currencies Enable a currency for the firm. ## Overview Adds a `firm_currency` row for the given ISO-4217 `code`. The server uppercases the input and validates against the static catalog: * `400 invalid_code` — code is not a three-letter alphabetic string. * `400 unsupported_currency` — code is not present in `static_currency`. The endpoint is idempotent: enabling a currency that is already enabled returns the existing row with `created: false`. # Disable Currency Source: https://docs.contazen.ro/api-reference/endpoints/currencies/delete DELETE /currencies/{id} Remove a currency from the firm. ## Overview Removes a `firm_currency` row. Fails with `409 in_use` when any bill, client, or product still references the currency. RON can be removed only if nothing references it — in practice it always stays enabled. # List Currencies Source: https://docs.contazen.ro/api-reference/endpoints/currencies/list GET /currencies List firm-enabled currencies and the catalog available to enable. ## Overview `data` is the set of currencies the firm has enabled for billing templates. `available` is the full ISO-4217 catalog (from `static_currency`) the firm can enable with `POST /currencies`. `default_code` is always `RON`. Currency rows expose `is_used` — if true, attempts to delete the row will be rejected with `409 in_use`. # Enable Language Source: https://docs.contazen.ro/api-reference/endpoints/languages/create POST /languages Enable a bill-template language for the firm. ## Overview Adds a `firm_language` row for the given ISO-639-1 two-letter `code`. The server lowercases the input and validates: * `400 invalid_code` — code is not two alphabetic characters. * `400 unsupported_language` — code is not in `static_language`. The endpoint is idempotent: enabling an already-enabled language returns the existing row with `created: false`. # Disable Language Source: https://docs.contazen.ro/api-reference/endpoints/languages/delete DELETE /languages/{id} Remove a language from the firm. ## Overview Removes a `firm_language` row. Returns `409 is_default` if the caller tries to remove `ro` — Romanian is the hard-coded default language and must stay enabled. # List Languages Source: https://docs.contazen.ro/api-reference/endpoints/languages/list GET /languages List firm-enabled bill-template languages. ## Overview `data` is the firm's enabled `firm_language` rows. `available` is the full catalog from `static_language` the firm can enable with `POST /languages`. `default_code` is always `ro` — Romanian is hard-coded as the default and cannot be removed. Use these codes as the `language` field when creating invoices to control which translations render on the generated PDF. # Get Settings Source: https://docs.contazen.ro/api-reference/endpoints/settings/get GET /settings Retrieve account settings and configuration