Upload Document
curl --request POST \
--url https://api.fifteenth.com/v1beta/accounts/{account_id}/documents \
--header 'Authorization: <authorization>' \
--header 'Content-Type: <content-type>' \
--data '
{
"tax_year": 123,
"description": "<string>"
}
'import requests
url = "https://api.fifteenth.com/v1beta/accounts/{account_id}/documents"
payload = {
"tax_year": 123,
"description": "<string>"
}
headers = {
"Authorization": "<authorization>",
"Content-Type": "<content-type>"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: '<authorization>', 'Content-Type': '<content-type>'},
body: JSON.stringify({tax_year: 123, description: '<string>'})
};
fetch('https://api.fifteenth.com/v1beta/accounts/{account_id}/documents', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fifteenth.com/v1beta/accounts/{account_id}/documents",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'tax_year' => 123,
'description' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>",
"Content-Type: <content-type>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.fifteenth.com/v1beta/accounts/{account_id}/documents"
payload := strings.NewReader("{\n \"tax_year\": 123,\n \"description\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<authorization>")
req.Header.Add("Content-Type", "<content-type>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.fifteenth.com/v1beta/accounts/{account_id}/documents")
.header("Authorization", "<authorization>")
.header("Content-Type", "<content-type>")
.body("{\n \"tax_year\": 123,\n \"description\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fifteenth.com/v1beta/accounts/{account_id}/documents")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<authorization>'
request["Content-Type"] = '<content-type>'
request.body = "{\n \"tax_year\": 123,\n \"description\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": 123,
"account_id": 123,
"filename": "<string>",
"tax_year": 123,
"description": "<string>",
"file_size": 123,
"file_type": "<string>",
"uploaded_at": "<string>",
"400 Bad Request": {},
"413 Payload Too Large": {},
"422 Unprocessable Entity": {}
}Documents
Upload Document
Upload tax documents for AI-powered processing and extraction
POST
/
v1beta
/
accounts
/
{account_id}
/
documents
Upload Document
curl --request POST \
--url https://api.fifteenth.com/v1beta/accounts/{account_id}/documents \
--header 'Authorization: <authorization>' \
--header 'Content-Type: <content-type>' \
--data '
{
"tax_year": 123,
"description": "<string>"
}
'import requests
url = "https://api.fifteenth.com/v1beta/accounts/{account_id}/documents"
payload = {
"tax_year": 123,
"description": "<string>"
}
headers = {
"Authorization": "<authorization>",
"Content-Type": "<content-type>"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: '<authorization>', 'Content-Type': '<content-type>'},
body: JSON.stringify({tax_year: 123, description: '<string>'})
};
fetch('https://api.fifteenth.com/v1beta/accounts/{account_id}/documents', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fifteenth.com/v1beta/accounts/{account_id}/documents",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'tax_year' => 123,
'description' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>",
"Content-Type: <content-type>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.fifteenth.com/v1beta/accounts/{account_id}/documents"
payload := strings.NewReader("{\n \"tax_year\": 123,\n \"description\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<authorization>")
req.Header.Add("Content-Type", "<content-type>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.fifteenth.com/v1beta/accounts/{account_id}/documents")
.header("Authorization", "<authorization>")
.header("Content-Type", "<content-type>")
.body("{\n \"tax_year\": 123,\n \"description\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fifteenth.com/v1beta/accounts/{account_id}/documents")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<authorization>'
request["Content-Type"] = '<content-type>'
request.body = "{\n \"tax_year\": 123,\n \"description\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": 123,
"account_id": 123,
"filename": "<string>",
"tax_year": 123,
"description": "<string>",
"file_size": 123,
"file_type": "<string>",
"uploaded_at": "<string>",
"400 Bad Request": {},
"413 Payload Too Large": {},
"422 Unprocessable Entity": {}
}Overview
The Document Upload endpoint allows you to upload tax documents directly to a client’s Fifteenth account. Documents are stored securely and can be organized by tax year with custom descriptions.Documents are stored securely and can be retrieved using the document ID.
Supported Document Types
Tax Forms
- W-2: Wage and tax statements
- 1099 Series: 1099-INT, 1099-DIV, 1099-B, 1099-R, etc.
- 1098: Mortgage interest statements
- K-1: Partnership, S-Corp, trust distributions
- Prior Year Returns: Previous tax returns
Supporting Documents
- Bank Statements: Account summaries and transactions
- Investment Statements: Brokerage and retirement accounts
- Receipts: Business expenses, charitable donations
- Legal Documents: Divorce decrees, adoption papers
- Business Records: P&L statements, balance sheets
File Requirements
- Formats: PDF, PNG, JPG, JPEG, TIFF
- Size Limit: 25MB per file
- Quality: Minimum 300 DPI for optimal OCR
- Pages: Up to 50 pages per document
Request
Path Parameters
number
required
The unique Fifteenth account identifier to upload documents to.Format: Numeric ID
Example:
Example:
12345Headers
string
required
Bearer token with your Partner API key
string
required
Must be
multipart/form-data for file uploadsForm Data Parameters
file
required
The tax document file to upload.Formats: PDF, PNG, JPG, JPEG, TIFF
Size: Maximum 25MB
Quality: 300 DPI recommended for best OCR results
Size: Maximum 25MB
Quality: 300 DPI recommended for best OCR results
integer
required
Tax year this document relates to.Range: 2020-2025
Example:
Example:
2024string
Human-readable description of the document.Max: 500 characters
Example: “W-2 from ABC Corporation for 2024”
Example: “W-2 from ABC Corporation for 2024”
Response
Success Response
number
Unique document identifier.
number
The account this document belongs to.
string
Original filename of the uploaded document.
integer
Tax year for this document.
string
Document description.
integer
File size in bytes.
string
MIME type of the uploaded file.
string
ISO 8601 timestamp when document was uploaded.
Examples
Basic Document Upload
import requests
account_id = 12345
url = f"https://api.fifteenth.com/v1beta/accounts/{account_id}/documents"
headers = {
"Authorization": "Bearer 15th_your_api_key_here"
}
# Upload W-2 document
with open('w2_2024.pdf', 'rb') as file:
files = {'file': file}
data = {
'tax_year': '2024',
'description': 'W-2 from ABC Corporation'
}
response = requests.post(url, headers=headers, files=files, data=data)
document = response.json()
print(f"Document uploaded: {document['id']}")
print(f"Tax year: {document['tax_year']}")
print(f"Description: {document['description']}")
const accountId = 12345;
const url = `https://api.fifteenth.com/v1beta/accounts/${accountId}/documents`;
const headers = {
'Authorization': 'Bearer 15th_your_api_key_here'
};
// Create form data
const formData = new FormData();
formData.append('file', fileInput.files[0]); // From file input element
formData.append('tax_year', '2024');
formData.append('description', 'W-2 from ABC Corporation');
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: formData
});
const document = await response.json();
console.log(`Document uploaded: ${document.id}`);
console.log(`Tax year: ${document.tax_year}`);
console.log(`Description: ${document.description}`);
curl -X POST "https://api.fifteenth.com/v1beta/accounts/12345/documents" \
-H "Authorization: Bearer 15th_your_api_key_here" \
-F "file=@w2_2024.pdf" \
-F "tax_year=2024" \
-F "description=W-2 from ABC Corporation"
Multiple Document Upload
import requests
import os
from concurrent.futures import ThreadPoolExecutor
def upload_document(account_id, file_path, doc_info):
"""Upload a single document"""
url = f"https://api.fifteenth.com/v1beta/accounts/{account_id}/documents"
headers = {"Authorization": "Bearer 15th_your_api_key_here"}
with open(file_path, 'rb') as file:
files = {'file': file}
response = requests.post(url, headers=headers, files=files, data=doc_info)
return response.json()
# Define documents to upload
documents = [
{
'file_path': 'w2_2024.pdf',
'doc_info': {
'tax_year': '2024',
'description': 'W-2 from employer'
}
},
{
'file_path': '1099_div.pdf',
'doc_info': {
'tax_year': '2024',
'description': 'Dividend income from investments'
}
}
]
# Upload documents in parallel
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [
executor.submit(upload_document, account_id, doc['file_path'], doc['doc_info'])
for doc in documents
]
results = [future.result() for future in futures]
for result in results:
print(f"Uploaded: {result['filename']} - ID: {result['id']}")
async function uploadDocument(accountId, file, docInfo) {
const url = `https://api.fifteenth.com/v1beta/accounts/${accountId}/documents`;
const headers = {
'Authorization': 'Bearer 15th_your_api_key_here'
};
const formData = new FormData();
formData.append('file', file);
// Add document info to form data
Object.keys(docInfo).forEach(key => {
formData.append(key, docInfo[key]);
});
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: formData
});
return await response.json();
}
// Upload multiple documents
const documentUploads = [
{
file: w2File, // File object from input
docInfo: {
tax_year: '2024',
description: 'W-2 from employer'
}
},
{
file: dividendFile,
docInfo: {
tax_year: '2024',
description: 'Dividend income from investments'
}
}
];
const uploadPromises = documentUploads.map(({file, docInfo}) =>
uploadDocument(accountId, file, docInfo)
);
const results = await Promise.all(uploadPromises);
results.forEach(result => {
console.log(`Uploaded: ${result.filename} - ID: ${result.id}`);
});
Response Examples
Successful Upload Response
Response
{
"id": 12345,
"account_id": 12345,
"filename": "w2_2024.pdf",
"tax_year": 2024,
"description": "W-2 from ABC Corporation",
"file_size": 245760,
"file_type": "application/pdf",
"uploaded_at": "2024-01-15T10:35:00Z"
}
Error Responses
object
Invalid file or request parameters.
{
"error": {
"code": "INVALID_FILE_TYPE",
"message": "Unsupported file type. Supported formats: PDF, PNG, JPG, JPEG, TIFF",
"details": {
"provided_type": "application/msword",
"supported_types": ["application/pdf", "image/png", "image/jpeg", "image/tiff"]
}
}
}
object
File exceeds size limit.
{
"error": {
"code": "FILE_TOO_LARGE",
"message": "File size exceeds 25MB limit",
"details": {
"file_size_mb": 32.5,
"max_size_mb": 25
}
}
}
object
File cannot be processed (corrupted, encrypted, etc.).
{
"error": {
"code": "UNPROCESSABLE_FILE",
"message": "File appears to be corrupted or encrypted",
"details": {
"file_analysis": "PDF appears to be password protected",
"suggestion": "Please upload an unencrypted version"
}
}
}
Best Practices
File Optimization
Image Quality
Image Quality
For best results:
- Use 300 DPI or higher resolution
- Ensure good lighting and contrast
- Avoid shadows and glare
- Keep text straight and unrotated
PDF Guidelines
PDF Guidelines
- Use text-based PDFs when possible (not scanned images)
- Ensure PDFs are not password protected
- Combine related pages into single documents
- Compress large PDFs while maintaining quality
Batch Processing
Batch Processing
- Upload related documents together
- Use consistent naming conventions
- Include clear descriptions for organization
Error Handling
def robust_document_upload(account_id, file_path, doc_info, max_retries=3):
"""Upload document with retry logic"""
url = f"https://api.fifteenth.com/v1beta/accounts/{account_id}/documents"
headers = {"Authorization": "Bearer 15th_your_api_key_here"}
for attempt in range(max_retries):
try:
with open(file_path, 'rb') as file:
files = {'file': file}
response = requests.post(url, headers=headers, files=files, data=doc_info)
if response.status_code == 200:
return response.json()
elif response.status_code == 413:
# File too large - don't retry
raise Exception(f"File too large: {response.json()['error']['message']}")
elif response.status_code in [500, 502, 503]:
# Server error - retry
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # Exponential backoff
continue
else:
raise Exception(f"Server error after {max_retries} attempts")
else:
# Client error - don't retry
raise Exception(f"Client error: {response.json()['error']['message']}")
except requests.exceptions.RequestException as e:
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
else:
raise Exception(f"Network error after {max_retries} attempts: {str(e)}")
raise Exception(f"Upload failed after {max_retries} attempts")
Next Steps
Retrieve Document
Get document details and metadata
List Documents
View all documents for an account
Project Status
Monitor how documents contribute to tax projects
Authentication
Learn about API keys and security best practices