Docs /External File Upload API

External File Upload API

Automatically upload data files to Deducta for processing via a secure, encrypted, API-key authenticated endpoint. Excel and Parquet are preferred, and ZIP archives let you send a batch of files in one request.

Last updated on

Overview

This endpoint lets external clients upload data files to Deducta for processing. Requests are authenticated with an API key, sent over an encrypted (HTTPS) connection, and processed asynchronously.

Accepted Formats

FormatExtensionNotes
Excel.xlsxPreferred
Parquet.parquetPreferred — best performance and data fidelity for large extracts
CSV.csvAccepted
ZIP.zipArchive of one or more of the above — use it to upload a batch at once

Authentication

All requests must include a valid API key in the request headers.

Base URL

https://api-data-ingestion.deducta.ai/m2m

Header Required:

X-API-Key: your-api-key-here

Contact your Deducta representative to obtain an API key for your organization.

Endpoint Details

  • URL: /api/file/upload
  • Method: PUT
  • Content-Type: multipart/form-data

Request Parameters

ParameterTypeLocationRequiredDescription
fileFileForm DataYesThe data file or ZIP archive to upload
businessUnitIdGUIDQuery StringYesThe business unit identifier

Validation Requirements

Your upload must meet the following requirements.

File Requirements

  • File Format: .xlsx, .parquet, .csv, or .zip. Excel and Parquet are preferred.
  • File Size: 50 MB by default. For a .zip, the limit applies to the compressed archive rather than its expanded contents. Higher limits can be arranged per request — contact your Deducta representative.
  • File Content: File must not be empty (size > 0 bytes)

Parameter Requirements

  • businessUnitId: Must be a valid GUID. It cannot be empty or the all-zero GUID:
    00000000-0000-0000-0000-000000000000

ZIP Archives

Upload a .zip when you have a batch of files to send in a single request — for example, one extract per period or per entity.

  • An archive may contain one or more .xlsx, .parquet, or .csv files.
  • Deducta expands the archive on receipt and processes as required.
  • The 50 MB limit applies to the compressed archive, so a batch that compresses well can carry considerably more raw data than a single uncompressed file.
  • Name the files inside the archive using the same convention you would use for an individual upload — those names are what identify the data once it reaches processing.

File Naming

The API accepts any file name, but we strongly recommend a consistent, descriptive name rather than a generic or random one. A good name identifies who the data is from and which period it covers, which makes uploads easy to trace, reconcile, and troubleshoot.

Use a structure such as:

{company}_{dataset}_{period}.xlsx

Examples:

  • acme_spend_2026-05.xlsx — monthly extract for May 2026
  • acme_invoices_2026-W21.xlsx — weekly extract (ISO week 21)
  • acme_transactions_2026-05-19.xlsx — daily extract
  • acme_spend_2026-H1.zip — archive of the monthly extracts for the first half of 2026

Tips:

  • Use lowercase and separate parts with underscores or hyphens — avoid spaces.
  • Use a sortable date format (YYYY-MM, YYYY-MM-DD, or YYYY-Www) so files order chronologically.
  • On upload, Deducta automatically appends a processing timestamp to the stored file name (see the fileName field in the success response), so you don't need to add one yourself.

Response Format

Success Response (200 OK)

{
  "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "fileName": "your_file_uploaded_20260119T143025.xlsx",
  "businessUnitId": "7d9e4b2a-8c3f-4e1d-9a6b-5f8c2d1e3a4b"
}

Error Responses

400 Bad Request

{
  "error": "Error message describing the validation failure"
}

Common validation errors:

  • "businessUnitId is required and cannot be empty"
  • "No file provided or file is empty"
  • "File size exceeds maximum allowed size of 50MB"
  • "Only .xlsx, .csv, .parquet, and .zip files are accepted"

401 Unauthorized

Authentication failed - invalid or missing API key.

500 Internal Server Error

{
  "error": "Failed to process file",
  "details": "Detailed error message"
}

Sample Scripts

cURL Example

#!/bin/bash

API_KEY="your-api-key-here"
BUSINESS_UNIT_ID="your-business-unit-id-here"
FILE_PATH="/path/to/your/file.xlsx"
API_URL="https://api-data-ingestion.deducta.ai/m2m/api/file/upload"

curl -X PUT "${API_URL}?businessUnitId=${BUSINESS_UNIT_ID}" \
  -H "X-API-Key: ${API_KEY}" \
  -F "file=@${FILE_PATH}"
Python Example
import mimetypes
import requests
from pathlib import Path

# Configuration
API_KEY = "your-api-key-here"
BUSINESS_UNIT_ID = "your-business-unit-id-here"
FILE_PATH = "/path/to/your/file.xlsx"
API_URL = "https://api-data-ingestion.deducta.ai/m2m/api/file/upload"

ACCEPTED_EXTENSIONS = {".xlsx", ".parquet", ".csv", ".zip"}
MAX_FILE_SIZE_MB = 50

def upload_file(file_path, business_unit_id, api_key):
    """
    Upload a data file to the Deducta Data Ingestion API.

    Accepts .xlsx, .parquet, .csv, and .zip files; Excel and Parquet are
    preferred. Use a .zip archive to upload a batch of files in one request.

    Args:
        file_path (str): Path to the file to upload
        business_unit_id (str): GUID of the business unit
        api_key (str): API key for authentication

    Returns:
        dict: Response containing fileId, fileName, and businessUnitId

    Raises:
        requests.exceptions.HTTPError: If the request fails
    """
    # Validate file exists
    file = Path(file_path)
    if not file.exists():
        raise FileNotFoundError(f"File not found: {file_path}")

    # Validate file format
    if file.suffix.lower() not in ACCEPTED_EXTENSIONS:
        raise ValueError(f"Unsupported file format: {file.suffix}")

    # Check file size — for a .zip this is the size of the compressed archive
    file_size_mb = file.stat().st_size / (1024 * 1024)
    if file_size_mb > MAX_FILE_SIZE_MB:
        raise ValueError(
            f"File size ({file_size_mb:.2f}MB) exceeds maximum allowed size of {MAX_FILE_SIZE_MB}MB"
        )

    # Prepare the request
    headers = {
        "X-API-Key": api_key
    }

    params = {
        "businessUnitId": business_unit_id
    }

    # Open and upload the file (content type is inferred from the file name)
    content_type = mimetypes.guess_type(file.name)[0] or 'application/octet-stream'
    with open(file_path, 'rb') as f:
        files = {
            'file': (file.name, f, content_type)
        }

        response = requests.put(
            API_URL,
            headers=headers,
            params=params,
            files=files
        )

    # Raise exception for HTTP errors
    response.raise_for_status()

    return response.json()

# Example usage
if __name__ == "__main__":
    try:
        result = upload_file(FILE_PATH, BUSINESS_UNIT_ID, API_KEY)
        print("Upload successful!")
        print(f"File ID: {result['fileId']}")
        print(f"File Name: {result['fileName']}")
        print(f"Business Unit ID: {result['businessUnitId']}")
    except FileNotFoundError as e:
        print(f"Error: {e}")
    except ValueError as e:
        print(f"Validation Error: {e}")
    except requests.exceptions.HTTPError as e:
        print(f"HTTP Error: {e}")
        if e.response.status_code == 400:
            print(f"Validation failed: {e.response.json()}")
        elif e.response.status_code == 401:
            print("Authentication failed - check your API key")
        elif e.response.status_code == 500:
            print(f"Server error: {e.response.json()}")
    except Exception as e:
        print(f"Unexpected error: {e}")
C# Example
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Text.Json;

public class FileUploadClient
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private const long MaxFileSizeBytes = 50 * 1024 * 1024; // 50MB

    private static readonly HashSet<string> AcceptedExtensions = new(StringComparer.OrdinalIgnoreCase)
    {
        ".xlsx", ".parquet", ".csv", ".zip"
    };

    public FileUploadClient(string baseUrl, string apiKey)
    {
        _httpClient = new HttpClient { BaseAddress = new Uri(baseUrl) };
        _apiKey = apiKey;
    }

    public async Task<FileUploadResponse> UploadFileAsync(
        string filePath,
        Guid businessUnitId)
    {
        // Validate file exists
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException("File not found", filePath);
        }

        // Validate file format
        var extension = Path.GetExtension(filePath);
        if (!AcceptedExtensions.Contains(extension))
        {
            throw new ArgumentException($"Unsupported file format: {extension}");
        }

        // Validate file size — for a .zip this is the size of the compressed archive
        var fileInfo = new FileInfo(filePath);
        if (fileInfo.Length > MaxFileSizeBytes)
        {
            throw new ArgumentException(
                $"File size ({fileInfo.Length / 1024 / 1024}MB) exceeds maximum allowed size of 50MB");
        }

        // Validate businessUnitId
        if (businessUnitId == Guid.Empty)
        {
            throw new ArgumentException("businessUnitId cannot be empty");
        }

        // Prepare the multipart form data
        using var content = new MultipartFormDataContent();
        using var fileStream = File.OpenRead(filePath);
        using var streamContent = new StreamContent(fileStream);

        streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

        content.Add(streamContent, "file", Path.GetFileName(filePath));

        // Prepare the request
        var requestUri = $"/api/file/upload?businessUnitId={businessUnitId}";
        var request = new HttpRequestMessage(HttpMethod.Put, requestUri)
        {
            Content = content
        };
        request.Headers.Add("X-API-Key", _apiKey);

        // Send the request
        var response = await _httpClient.SendAsync(request);

        // Parse response
        var responseContent = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException(
                $"Upload failed with status {response.StatusCode}: {responseContent}");
        }

        return JsonSerializer.Deserialize<FileUploadResponse>(responseContent);
    }
}

public class FileUploadResponse
{
    public Guid FileId { get; set; }
    public string FileName { get; set; }
    public Guid BusinessUnitId { get; set; }
}

// Example usage
public class Program
{
    public static async Task Main(string[] args)
    {
        var apiKey = "your-api-key-here";
        var businessUnitId = Guid.Parse("your-business-unit-id-here");
        var filePath = @"C:\path\to\your\file.xlsx";
        var apiUrl = "https://api-data-ingestion.deducta.ai/m2m";

        var client = new FileUploadClient(apiUrl, apiKey);

        try
        {
            var result = await client.UploadFileAsync(filePath, businessUnitId);

            Console.WriteLine("Upload successful!");
            Console.WriteLine($"File ID: {result.FileId}");
            Console.WriteLine($"File Name: {result.FileName}");
            Console.WriteLine($"Business Unit ID: {result.BusinessUnitId}");
        }
        catch (FileNotFoundException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine($"Validation Error: {ex.Message}");
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"HTTP Error: {ex.Message}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Unexpected error: {ex.Message}");
        }
    }
}

Best Practices

  1. Error Handling: Always implement proper error handling to catch validation errors, authentication failures, and server errors.
  2. File Validation:Validate files on the client side before uploading to avoid unnecessary network traffic:
    • Use an accepted format: .xlsx, .parquet, .csv, or .zip
    • Verify file size is within your agreed limit (50 MB by default, measured on the compressed archive for a .zip)
    • Ensure file is not empty
  3. Batching: Send a .zip instead of many single-file requests when you have several files for the same business unit — fewer round trips, and compression lets you fit more raw data under the size limit.
  4. Retry Logic: Implement exponential backoff retry logic for transient failures (5xx errors).
  5. Logging: Log all upload attempts, including file names, sizes, and response data for troubleshooting.
  6. Security:
    • Never hardcode API keys in source code
    • Use environment variables or secure configuration management
  7. Timeouts: Set appropriate timeouts for large file uploads (recommended: 5-10 minutes for 50MB files).

Support

For API key requests, technical issues, or questions, please contact your Deducta representative.

We use cookies to analyse site traffic. See our cookie policy for details.