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
| Format | Extension | Notes |
|---|---|---|
| Excel | .xlsx | Preferred |
| Parquet | .parquet | Preferred — best performance and data fidelity for large extracts |
| CSV | .csv | Accepted |
| ZIP | .zip | Archive 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
| Parameter | Type | Location | Required | Description |
|---|---|---|---|---|
file | File | Form Data | Yes | The data file or ZIP archive to upload |
businessUnitId | GUID | Query String | Yes | The 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.csvfiles. - 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 2026acme_invoices_2026-W21.xlsx— weekly extract (ISO week 21)acme_transactions_2026-05-19.xlsx— daily extractacme_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, orYYYY-Www) so files order chronologically. - On upload, Deducta automatically appends a processing timestamp to the stored file name (see the
fileNamefield 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
- Error Handling: Always implement proper error handling to catch validation errors, authentication failures, and server errors.
- 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
- Use an accepted format:
- Batching: Send a
.zipinstead 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. - Retry Logic: Implement exponential backoff retry logic for transient failures (5xx errors).
- Logging: Log all upload attempts, including file names, sizes, and response data for troubleshooting.
- Security:
- Never hardcode API keys in source code
- Use environment variables or secure configuration management
- 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.
