Opportunities Upload API
Overviewβ
The Opportunities Upload API allows authenticated users to upload files and attachments related to opportunities within the Ring Platform. This endpoint supports job descriptions, requirements documents, application forms, and other opportunity-related files.
Endpoint Detailsβ
- URL:
/api/opportunities/upload
- Method:
POST
- Authentication: Required (Bearer token or session)
- Authorization: MEMBER, CONFIDENTIAL, or ADMIN level
- Content Type:
multipart/form-data
- File Size Limit: 25MB per file
- Supported Formats: Documents, images, presentations, archives
Ring Platform Opportunity File Managementβ
File Types and Use Casesβ
Ring Platform supports various file types for opportunity documentation and requirements:
Supported File Categoriesβ
- Job Descriptions: PDF, DOC, DOCX (detailed role specifications)
- Application Forms: PDF, DOC (custom application templates)
- Requirements: PDF, TXT (technical specifications, skill requirements)
- Company Materials: PDF, PPT, PPTX (company overviews, culture docs)
- Images: JPG, PNG (office photos, team pictures, work environment)
- Archives: ZIP, RAR (code samples, portfolio requirements)
Permission-Based Accessβ
- MEMBER: Can upload files for their entity's opportunities
- CONFIDENTIAL: Can upload files for confidential opportunities
- ADMIN: Can upload files for any opportunity
- Enhanced Security: Confidential opportunity files have additional protection
Request Formatβ
Headersβ
POST /api/opportunities/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
Form Data Parametersβ
file
(required): The file to uploadopportunityId
(optional): Associate file with specific opportunityfileType
(optional): Category of file (description, application, requirement, etc.)description
(optional): File description or purposeisPublic
(optional): Whether file should be publicly accessible (default: true)
Example Form Dataβ
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('opportunityId', 'opp_abc123def456');
formData.append('fileType', 'description');
formData.append('description', 'Detailed job description for Senior Developer role');
formData.append('isPublic', 'true');
Response Formatβ
Success Response (200 OK)β
{
"success": true,
"url": "https://blob.vercel-storage.com/opportunities/opp_abc123/user_456_1705234567890_job-description.pdf",
"downloadUrl": "https://blob.vercel-storage.com/opportunities/opp_abc123/user_456_1705234567890_job-description.pdf?download=1",
"filename": "job-description.pdf",
"size": 1048576,
"contentType": "application/pdf",
"opportunityId": "opp_abc123def456",
"fileType": "description",
"description": "Detailed job description for Senior Developer role",
"isPublic": true,
"uploadedAt": "2025-01-14T16:45:00Z",
"uploadedBy": "user_456",
"metadata": {
"pages": 3,
"format": "PDF",
"encrypted": false
}
}
Error Responsesβ
Unauthorized (401)β
{
"error": "Unauthorized"
}
Insufficient Permissions (403)β
{
"error": "Access denied: Insufficient permissions to upload opportunity files"
}
No File Provided (400)β
{
"error": "No file provided"
}
File Too Large (413)β
{
"error": "File size exceeds the maximum allowed limit"
}
Unsupported File Type (415)β
{
"error": "Invalid file type"
}
Code Examplesβ
JavaScript/TypeScriptβ
interface OpportunityFileUploadResult {
success: boolean;
url: string;
downloadUrl: string;
filename: string;
size: number;
contentType: string;
opportunityId?: string;
fileType?: string;
description?: string;
isPublic: boolean;
uploadedAt: string;
uploadedBy: string;
metadata?: {
pages?: number;
format?: string;
encrypted?: boolean;
};
}
async function uploadOpportunityFile(
file: File,
authToken: string,
options: {
opportunityId?: string;
fileType?: string;
description?: string;
isPublic?: boolean;
} = {}
): Promise<OpportunityFileUploadResult> {
const formData = new FormData();
formData.append('file', file);
if (options.opportunityId) {
formData.append('opportunityId', options.opportunityId);
}
if (options.fileType) {
formData.append('fileType', options.fileType);
}
if (options.description) {
formData.append('description', options.description);
}
if (options.isPublic !== undefined) {
formData.append('isPublic', options.isPublic.toString());
}
const response = await fetch('/api/opportunities/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to upload file');
}
return response.json();
}
// Usage examples
try {
// Upload job description
const jobDescFile = document.getElementById('jobDescInput').files[0];
const jobDescResult = await uploadOpportunityFile(jobDescFile, userToken, {
opportunityId: 'opp_abc123',
fileType: 'description',
description: 'Comprehensive job description with requirements',
isPublic: true
});
console.log('Job description uploaded:', jobDescResult.url);
// Upload application form
const appFormFile = document.getElementById('appFormInput').files[0];
const appFormResult = await uploadOpportunityFile(appFormFile, userToken, {
opportunityId: 'opp_abc123',
fileType: 'application',
description: 'Custom application form for candidates',
isPublic: true
});
console.log('Application form uploaded:', appFormResult.url);
// Upload confidential requirements (private file)
const reqFile = document.getElementById('reqInput').files[0];
const reqResult = await uploadOpportunityFile(reqFile, userToken, {
opportunityId: 'opp_confidential_456',
fileType: 'requirement',
description: 'Confidential technical requirements',
isPublic: false
});
console.log('Requirements uploaded:', reqResult.url);
} catch (error) {
console.error('Upload failed:', error.message);
}
React Component for Opportunity File Uploadβ
import { useState, useRef } from 'react';
interface OpportunityFileUploaderProps {
opportunityId?: string;
fileType?: string;
onUploadComplete?: (result: OpportunityFileUploadResult) => void;
acceptedTypes?: string;
maxSize?: number; // in bytes
allowPrivateFiles?: boolean;
}
function OpportunityFileUploader({
opportunityId,
fileType,
onUploadComplete,
acceptedTypes = ".pdf,.doc,.docx,.txt,.jpg,.png,.ppt,.pptx,.zip",
maxSize = 25 * 1024 * 1024, // 25MB
allowPrivateFiles = false
}: OpportunityFileUploaderProps) {
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [description, setDescription] = useState('');
const [isPublic, setIsPublic] = useState(true);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// Validate file size
if (file.size > maxSize) {
setError(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`);
return;
}
setIsUploading(true);
setError(null);
setUploadProgress(0);
try {
const token = localStorage.getItem('authToken');
if (!token) {
throw new Error('Authentication required');
}
// Simulate upload progress
const progressInterval = setInterval(() => {
setUploadProgress(prev => Math.min(prev + 10, 90));
}, 100);
const result = await uploadOpportunityFile(file, token, {
opportunityId,
fileType,
description: description || `${fileType || 'File'} for opportunity`,
isPublic
});
clearInterval(progressInterval);
setUploadProgress(100);
onUploadComplete?.(result);
// Reset form
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
setDescription('');
setIsPublic(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Upload failed');
} finally {
setIsUploading(false);
setTimeout(() => setUploadProgress(0), 1000);
}
};
return (
<div className="opportunity-file-uploader">
<div className="upload-form">
<div className="form-group">
<label>File Description</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={`Describe this ${fileType || 'file'}...`}
disabled={isUploading}
/>
</div>
{allowPrivateFiles && (
<div className="form-group">
<label>
<input
type="checkbox"
checked={isPublic}
onChange={(e) => setIsPublic(e.target.checked)}
disabled={isUploading}
/>
Make file publicly accessible
</label>
</div>
)}
<div className="upload-area">
<input
ref={fileInputRef}
type="file"
accept={acceptedTypes}
onChange={handleFileSelect}
disabled={isUploading}
className="file-input"
/>
<div className="upload-prompt">
<div className="upload-icon">π</div>
<p>
{isUploading
? `Uploading... ${uploadProgress}%`
: `Choose ${fileType || 'file'} to upload`
}
</p>
<p className="upload-hint">
Max size: {maxSize / (1024 * 1024)}MB
</p>
</div>
{isUploading && (
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${uploadProgress}%` }}
/>
</div>
)}
</div>
</div>
{error && (
<div className="error-message">
{error}
</div>
)}
</div>
);
}
// Usage in opportunity management form
function OpportunityFilesManager({ opportunityId }: { opportunityId: string }) {
const [opportunityFiles, setOpportunityFiles] = useState<OpportunityFileUploadResult[]>([]);
const handleFileUpload = (result: OpportunityFileUploadResult) => {
setOpportunityFiles(prev => [...prev, result]);
console.log('File uploaded successfully:', result.filename);
};
return (
<div className="opportunity-files-manager">
<h3>Opportunity Files</h3>
<div className="file-upload-sections">
<div className="upload-section">
<h4>Job Description</h4>
<OpportunityFileUploader
opportunityId={opportunityId}
fileType="description"
acceptedTypes=".pdf,.doc,.docx"
onUploadComplete={handleFileUpload}
/>
</div>
<div className="upload-section">
<h4>Application Form</h4>
<OpportunityFileUploader
opportunityId={opportunityId}
fileType="application"
acceptedTypes=".pdf,.doc,.docx"
onUploadComplete={handleFileUpload}
/>
</div>
<div className="upload-section">
<h4>Requirements & Specifications</h4>
<OpportunityFileUploader
opportunityId={opportunityId}
fileType="requirement"
acceptedTypes=".pdf,.doc,.docx,.txt"
allowPrivateFiles={true}
onUploadComplete={handleFileUpload}
/>
</div>
<div className="upload-section">
<h4>Company Materials</h4>
<OpportunityFileUploader
opportunityId={opportunityId}
fileType="company"
acceptedTypes=".pdf,.ppt,.pptx,.jpg,.png"
onUploadComplete={handleFileUpload}
/>
</div>
</div>
<div className="uploaded-files">
<h4>Uploaded Files</h4>
{opportunityFiles.map((file, index) => (
<div key={index} className="file-item">
<div className="file-info">
<span className="file-name">{file.filename}</span>
<span className="file-type">{file.fileType}</span>
<span className="file-size">{(file.size / 1024).toFixed(1)} KB</span>
<span className={`file-visibility ${file.isPublic ? 'public' : 'private'}`}>
{file.isPublic ? 'π Public' : 'π Private'}
</span>
</div>
<div className="file-actions">
<a href={file.url} target="_blank" rel="noopener noreferrer">
View
</a>
<a href={file.downloadUrl} download>
Download
</a>
</div>
</div>
))}
</div>
</div>
);
}
cURL Exampleβ
# Upload job description
curl -X POST "https://ring.ck.ua/api/opportunities/upload" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-F "file=@job-description.pdf" \
-F "opportunityId=opp_abc123def456" \
-F "fileType=description" \
-F "description=Comprehensive job description for Senior Developer" \
-F "isPublic=true"
# Upload confidential requirements
curl -X POST "https://ring.ck.ua/api/opportunities/upload" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-F "file=@technical-requirements.pdf" \
-F "opportunityId=opp_confidential_789" \
-F "fileType=requirement" \
-F "description=Confidential technical specifications" \
-F "isPublic=false"
# Upload application form
curl -X POST "https://ring.ck.ua/api/opportunities/upload" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-F "file=@application-form.docx" \
-F "opportunityId=opp_abc123def456" \
-F "fileType=application" \
-F "description=Custom application form for candidates"
# Expected response
# {
# "success": true,
# "url": "https://blob.vercel-storage.com/opportunities/opp_abc123/user_456_1705234567890_job-description.pdf",
# "filename": "job-description.pdf",
# "size": 1048576,
# "contentType": "application/pdf",
# "opportunityId": "opp_abc123def456",
# "fileType": "description",
# "uploadedAt": "2025-01-14T16:45:00Z"
# }
Python Exampleβ
import requests
from typing import Dict, Any, Optional
class OpportunityFileUploader:
def __init__(self, base_url: str, auth_token: str):
self.base_url = base_url.rstrip('/')
self.auth_token = auth_token
self.headers = {
'Authorization': f'Bearer {auth_token}'
}
def upload_file(
self,
file_path: str,
opportunity_id: Optional[str] = None,
file_type: Optional[str] = None,
description: Optional[str] = None,
is_public: bool = True
) -> Dict[str, Any]:
"""Upload a file for an opportunity"""
url = f"{self.base_url}/api/opportunities/upload"
# Prepare form data
files = {'file': open(file_path, 'rb')}
data = {}
if opportunity_id:
data['opportunityId'] = opportunity_id
if file_type:
data['fileType'] = file_type
if description:
data['description'] = description
data['isPublic'] = str(is_public).lower()
try:
response = requests.post(
url,
headers=self.headers,
files=files,
data=data
)
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
raise Exception("Unauthorized - check your authentication token")
elif response.status_code == 403:
raise Exception("Forbidden - insufficient permissions to upload opportunity files")
elif response.status_code == 400:
error_data = response.json()
raise Exception(f"Bad request: {error_data.get('error', 'Unknown error')}")
elif response.status_code == 413:
raise Exception("File too large - exceeds 25MB limit")
elif response.status_code == 415:
raise Exception("Unsupported file type")
else:
error_data = response.json()
raise Exception(f"Upload failed: {error_data.get('error', 'Unknown error')}")
finally:
files['file'].close()
def upload_job_description(self, file_path: str, opportunity_id: str) -> Dict[str, Any]:
"""Convenience method for uploading job descriptions"""
return self.upload_file(
file_path,
opportunity_id=opportunity_id,
file_type='description',
description='Job description document'
)
def upload_application_form(self, file_path: str, opportunity_id: str) -> Dict[str, Any]:
"""Convenience method for uploading application forms"""
return self.upload_file(
file_path,
opportunity_id=opportunity_id,
file_type='application',
description='Application form for candidates'
)
def upload_requirements(self, file_path: str, opportunity_id: str, is_confidential: bool = False) -> Dict[str, Any]:
"""Convenience method for uploading requirements"""
return self.upload_file(
file_path,
opportunity_id=opportunity_id,
file_type='requirement',
description='Technical requirements and specifications',
is_public=not is_confidential
)
# Usage examples
uploader = OpportunityFileUploader("https://ring.ck.ua", "your-auth-token")
try:
# Upload job description
job_desc_result = uploader.upload_job_description(
"./documents/senior-dev-job-description.pdf",
"opp_abc123"
)
print(f"Job description uploaded: {job_desc_result['url']}")
# Upload application form
app_form_result = uploader.upload_application_form(
"./forms/application-form.docx",
"opp_abc123"
)
print(f"Application form uploaded: {app_form_result['url']}")
# Upload confidential requirements
req_result = uploader.upload_requirements(
"./requirements/technical-specs.pdf",
"opp_confidential_456",
is_confidential=True
)
print(f"Requirements uploaded: {req_result['url']}")
# Batch upload multiple files for an opportunity
files_to_upload = [
{
"path": "./docs/job-description.pdf",
"type": "description",
"desc": "Senior Developer job description"
},
{
"path": "./forms/application.docx",
"type": "application",
"desc": "Application form"
},
{
"path": "./specs/technical-requirements.pdf",
"type": "requirement",
"desc": "Technical requirements"
},
{
"path": "./company/company-overview.pptx",
"type": "company",
"desc": "Company overview presentation"
}
]
opportunity_id = "opp_abc123"
upload_results = []
for file_info in files_to_upload:
try:
result = uploader.upload_file(
file_info["path"],
opportunity_id=opportunity_id,
file_type=file_info["type"],
description=file_info["desc"]
)
upload_results.append({
'filename': result['filename'],
'url': result['url'],
'type': result['fileType'],
'status': 'success'
})
except Exception as e:
upload_results.append({
'filename': file_info["path"],
'error': str(e),
'status': 'error'
})
# Summary
successful = [r for r in upload_results if r['status'] == 'success']
failed = [r for r in upload_results if r['status'] == 'error']
print(f"Batch upload complete: {len(successful)} successful, {len(failed)} failed")
for result in successful:
print(f"β
{result['filename']} ({result['type']})")
for result in failed:
print(f"β {result['filename']}: {result['error']}")
except Exception as e:
print(f"Error uploading file: {e}")
File Management and Securityβ
Permission-Based Accessβ
- MEMBER Level: Can upload files for their entity's opportunities
- CONFIDENTIAL Level: Can upload files for confidential opportunities
- ADMIN Level: Can upload files for any opportunity
- File Visibility: Public files accessible to all, private files restricted
Security Featuresβ
- File Validation: Server-side validation of file types and sizes
- Malware Scanning: Automatic scanning of uploaded files
- Access Logging: Track all file uploads and downloads
- Encryption: Confidential files encrypted at rest
File Organizationβ
- Unique Naming: Automatic file naming prevents conflicts
- Directory Structure: Files organized by opportunity and user
- Metadata Tracking: File purpose, upload date, and permissions
- Version Control: Handle file updates and replacements
Related Endpointsβ
- Create Opportunity - Create new opportunities
- Update Opportunity - Modify opportunity information
- Get Opportunity - Retrieve opportunity details
- Entities Upload - Upload entity files
Changelogβ
- v1.0.0 - Initial implementation with basic file upload
- v1.1.0 - Added file type categorization and privacy controls
- v1.2.0 - Enhanced security for confidential opportunities
- v1.3.0 - Improved performance and metadata tracking