Skip to main content

Opportunities Update API

Overview​

The Opportunities Update API allows authenticated users to modify existing opportunities within the Ring Platform. This endpoint supports partial updates, enabling users to modify specific fields without affecting the entire opportunity record.

Endpoint Details​

  • URL: /api/opportunities/update/[id]
  • Method: PATCH
  • Authentication: Required (Bearer token or session)
  • Authorization: Owner, Admin, or Entity Member
  • Rate Limit: 60 requests per minute per user
  • Validation: Comprehensive field validation and business rules

Ring Platform Opportunity Updates​

Update Permissions​

Ring Platform implements a sophisticated permission system for opportunity updates:

Permission Levels​

  • Owner: User who created the opportunity (full update access)
  • Entity Admin: Admin of the entity that owns the opportunity
  • Platform Admin: System administrators (full access)
  • Entity Member: Members of the owning entity (limited updates)

Confidential Opportunity Rules​

  • Only CONFIDENTIAL users can update confidential opportunities
  • Confidential status cannot be changed once set
  • Enhanced audit logging for all confidential opportunity changes
  • Additional security validations for sensitive fields

Field-Level Permissions​

  • Public Fields: Title, description, requirements (all authorized users)
  • Sensitive Fields: Budget, contact info (owner/admin only)
  • System Fields: Created date, ID, audit trail (read-only)

Request Format​

URL Parameters​

  • id (required): Unique identifier of the opportunity to update

Headers​

PATCH /api/opportunities/update/[id]
Content-Type: application/json
Authorization: Bearer <token>

Request Body​

{
"title": "Senior Full-Stack Developer - Updated",
"description": "Updated job description with new requirements...",
"type": "offer",
"category": "technology",
"industry": "software-development",
"location": {
"type": "hybrid",
"city": "San Francisco",
"state": "CA",
"country": "US",
"remote": true
},
"budget": {
"min": 120000,
"max": 180000,
"currency": "USD",
"type": "annual"
},
"requirements": [
"5+ years React/Node.js experience",
"TypeScript proficiency",
"System design experience",
"Team leadership skills"
],
"benefits": [
"Competitive salary",
"Equity package",
"Health insurance",
"Remote work flexibility",
"Professional development budget"
],
"tags": ["react", "nodejs", "typescript", "leadership", "remote"],
"expiresAt": "2025-03-15T23:59:59Z",
"isActive": true,
"contactInfo": {
"email": "hiring@techcorp.com",
"phone": "+1-555-0123",
"preferredMethod": "email"
},
"applicationProcess": {
"method": "external",
"url": "https://techcorp.com/careers/senior-fullstack",
"instructions": "Please apply through our careers page with your portfolio"
}
}

Partial Update Example​

{
"title": "Updated Opportunity Title",
"budget": {
"min": 130000,
"max": 190000
},
"tags": ["react", "nodejs", "typescript", "senior", "remote"]
}

Response Format​

Success Response (200 OK)​

{
"message": "Opportunity updated successfully",
"success": true,
"data": {
"id": "opp_abc123def456",
"title": "Senior Full-Stack Developer - Updated",
"description": "Updated job description with new requirements...",
"type": "offer",
"category": "technology",
"industry": "software-development",
"entityId": "ent_xyz789",
"entityName": "TechCorp Solutions",
"createdBy": "user_123",
"location": {
"type": "hybrid",
"city": "San Francisco",
"state": "CA",
"country": "US",
"remote": true
},
"budget": {
"min": 120000,
"max": 180000,
"currency": "USD",
"type": "annual"
},
"requirements": [
"5+ years React/Node.js experience",
"TypeScript proficiency",
"System design experience",
"Team leadership skills"
],
"benefits": [
"Competitive salary",
"Equity package",
"Health insurance",
"Remote work flexibility",
"Professional development budget"
],
"tags": ["react", "nodejs", "typescript", "leadership", "remote"],
"visibility": "public",
"isActive": true,
"isConfidential": false,
"contactInfo": {
"email": "hiring@techcorp.com",
"phone": "+1-555-0123",
"preferredMethod": "email"
},
"applicationProcess": {
"method": "external",
"url": "https://techcorp.com/careers/senior-fullstack",
"instructions": "Please apply through our careers page with your portfolio"
},
"stats": {
"views": 245,
"applications": 12,
"saves": 18
},
"createdAt": "2025-01-10T14:30:00Z",
"updatedAt": "2025-01-14T16:45:00Z",
"expiresAt": "2025-03-15T23:59:59Z"
}
}

Error Responses​

Unauthorized (401)​

{
"error": "Unauthorized"
}

Forbidden (403)​

{
"error": "Access denied: Forbidden"
}

Not Found (404)​

{
"error": "Opportunity not found"
}

Validation Error (400)​

{
"error": "Validation failed",
"details": {
"budget.min": "Minimum budget must be greater than 0",
"expiresAt": "Expiration date must be in the future",
"tags": "Maximum 10 tags allowed"
}
}

Confidential Permission Error (403)​

{
"error": "Cannot update confidential opportunity: Insufficient permissions"
}

Code Examples​

JavaScript/TypeScript​

interface OpportunityUpdateData {
title?: string;
description?: string;
type?: 'offer' | 'request';
category?: string;
industry?: string;
location?: {
type: 'remote' | 'onsite' | 'hybrid';
city?: string;
state?: string;
country?: string;
remote?: boolean;
};
budget?: {
min?: number;
max?: number;
currency?: string;
type?: 'hourly' | 'daily' | 'monthly' | 'annual' | 'project';
};
requirements?: string[];
benefits?: string[];
tags?: string[];
expiresAt?: string;
isActive?: boolean;
contactInfo?: {
email?: string;
phone?: string;
preferredMethod?: 'email' | 'phone' | 'platform';
};
applicationProcess?: {
method: 'internal' | 'external' | 'email';
url?: string;
email?: string;
instructions?: string;
};
}

async function updateOpportunity(
opportunityId: string,
updateData: OpportunityUpdateData,
authToken: string
): Promise<any> {
const response = await fetch(`/api/opportunities/update/${opportunityId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(updateData)
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to update opportunity');
}

return response.json();
}

// Usage examples
try {
// Partial update - only modify specific fields
const partialUpdate = await updateOpportunity('opp_abc123', {
title: 'Updated Job Title',
budget: {
min: 130000,
max: 190000
},
tags: ['react', 'nodejs', 'senior']
}, userToken);

console.log('Opportunity updated:', partialUpdate.data.title);

// Full update with all fields
const fullUpdate = await updateOpportunity('opp_def456', {
title: 'Senior Backend Engineer',
description: 'We are looking for an experienced backend engineer...',
type: 'offer',
category: 'technology',
industry: 'software-development',
location: {
type: 'remote',
country: 'US',
remote: true
},
budget: {
min: 140000,
max: 200000,
currency: 'USD',
type: 'annual'
},
requirements: [
'Go or Python experience',
'Microservices architecture',
'Cloud platforms (AWS/GCP)'
],
benefits: [
'Competitive salary',
'Stock options',
'Health benefits'
],
tags: ['golang', 'python', 'microservices', 'cloud'],
expiresAt: '2025-04-01T23:59:59Z',
isActive: true,
contactInfo: {
email: 'engineering@company.com',
preferredMethod: 'email'
},
applicationProcess: {
method: 'external',
url: 'https://company.com/careers/backend-engineer',
instructions: 'Please include your GitHub profile'
}
}, userToken);

console.log('Full update completed:', fullUpdate.data.id);

} catch (error) {
console.error('Update failed:', error.message);
}

React Hook for Opportunity Updates​

import { useState } from 'react';

interface UseOpportunityUpdateResult {
updateOpportunity: (id: string, data: OpportunityUpdateData) => Promise<void>;
isUpdating: boolean;
error: string | null;
success: boolean;
}

function useOpportunityUpdate(): UseOpportunityUpdateResult {
const [isUpdating, setIsUpdating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);

const updateOpportunity = async (id: string, data: OpportunityUpdateData) => {
setIsUpdating(true);
setError(null);
setSuccess(false);

try {
const token = localStorage.getItem('authToken');
if (!token) {
throw new Error('Authentication required');
}

const result = await updateOpportunity(id, data, token);
setSuccess(true);

// Optional: trigger refresh of opportunity data
// onUpdate?.(result.data);

} catch (err) {
setError(err instanceof Error ? err.message : 'Update failed');
} finally {
setIsUpdating(false);
}
};

return { updateOpportunity, isUpdating, error, success };
}

// Usage in component
function OpportunityEditForm({ opportunityId, initialData }: OpportunityEditFormProps) {
const { updateOpportunity, isUpdating, error, success } = useOpportunityUpdate();
const [formData, setFormData] = useState(initialData);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

// Only send changed fields
const changes = getChangedFields(initialData, formData);

if (Object.keys(changes).length === 0) {
alert('No changes to save');
return;
}

await updateOpportunity(opportunityId, changes);
};

return (
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Title</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
disabled={isUpdating}
/>
</div>

<div className="form-group">
<label>Description</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
disabled={isUpdating}
/>
</div>

{/* Budget fields */}
<div className="form-group">
<label>Budget Range</label>
<div className="budget-inputs">
<input
type="number"
placeholder="Min"
value={formData.budget?.min || ''}
onChange={(e) => setFormData({
...formData,
budget: { ...formData.budget, min: parseInt(e.target.value) }
})}
disabled={isUpdating}
/>
<input
type="number"
placeholder="Max"
value={formData.budget?.max || ''}
onChange={(e) => setFormData({
...formData,
budget: { ...formData.budget, max: parseInt(e.target.value) }
})}
disabled={isUpdating}
/>
</div>
</div>

{error && <div className="error-message">{error}</div>}
{success && <div className="success-message">Opportunity updated successfully!</div>}

<div className="form-actions">
<button type="submit" disabled={isUpdating}>
{isUpdating ? 'Updating...' : 'Update Opportunity'}
</button>
</div>
</form>
);
}

// Helper function to detect changes
function getChangedFields(original: any, updated: any): any {
const changes: any = {};

for (const key in updated) {
if (JSON.stringify(original[key]) !== JSON.stringify(updated[key])) {
changes[key] = updated[key];
}
}

return changes;
}

cURL Example​

# Update opportunity title and budget
curl -X PATCH "https://ring.ck.ua/api/opportunities/update/opp_abc123def456" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-d '{
"title": "Updated Senior Developer Position",
"budget": {
"min": 130000,
"max": 190000,
"currency": "USD",
"type": "annual"
},
"tags": ["react", "nodejs", "typescript", "senior", "remote"]
}'

# Partial update - change only status
curl -X PATCH "https://ring.ck.ua/api/opportunities/update/opp_def456" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-d '{
"isActive": false
}'

# Update location and remote work options
curl -X PATCH "https://ring.ck.ua/api/opportunities/update/opp_ghi789" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-d '{
"location": {
"type": "hybrid",
"city": "Austin",
"state": "TX",
"country": "US",
"remote": true
}
}'

Python Example​

import requests
from typing import Dict, Any, Optional

class OpportunityUpdater:
def __init__(self, base_url: str, auth_token: str):
self.base_url = base_url.rstrip('/')
self.auth_token = auth_token
self.headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {auth_token}'
}

def update_opportunity(self, opportunity_id: str, update_data: Dict[str, Any]) -> Dict[str, Any]:
"""Update an opportunity with partial data"""
url = f"{self.base_url}/api/opportunities/update/{opportunity_id}"

response = requests.patch(url, json=update_data, headers=self.headers)

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")
elif response.status_code == 404:
raise Exception("Opportunity not found")
else:
error_data = response.json()
raise Exception(f"Update failed: {error_data.get('error', 'Unknown error')}")

def update_budget(self, opportunity_id: str, min_budget: int, max_budget: int, currency: str = "USD") -> Dict[str, Any]:
"""Convenience method to update only budget information"""
update_data = {
"budget": {
"min": min_budget,
"max": max_budget,
"currency": currency,
"type": "annual"
}
}
return self.update_opportunity(opportunity_id, update_data)

def toggle_active_status(self, opportunity_id: str, is_active: bool) -> Dict[str, Any]:
"""Toggle opportunity active status"""
update_data = {"isActive": is_active}
return self.update_opportunity(opportunity_id, update_data)

def update_tags(self, opportunity_id: str, tags: list) -> Dict[str, Any]:
"""Update opportunity tags"""
update_data = {"tags": tags}
return self.update_opportunity(opportunity_id, update_data)

def extend_expiration(self, opportunity_id: str, new_expiration: str) -> Dict[str, Any]:
"""Extend opportunity expiration date"""
update_data = {"expiresAt": new_expiration}
return self.update_opportunity(opportunity_id, update_data)

# Usage examples
updater = OpportunityUpdater("https://ring.ck.ua", "your-auth-token")

try:
# Update multiple fields
result = updater.update_opportunity("opp_abc123", {
"title": "Senior Python Developer - Remote",
"description": "Updated job description with remote work emphasis...",
"location": {
"type": "remote",
"country": "US",
"remote": True
},
"budget": {
"min": 120000,
"max": 180000,
"currency": "USD",
"type": "annual"
},
"tags": ["python", "django", "remote", "senior"],
"benefits": [
"Competitive salary",
"Health insurance",
"401k matching",
"Unlimited PTO",
"Home office stipend"
]
})

print(f"Updated opportunity: {result['data']['title']}")

# Use convenience methods
budget_update = updater.update_budget("opp_def456", 140000, 200000)
print(f"Budget updated for: {budget_update['data']['id']}")

# Toggle status
status_update = updater.toggle_active_status("opp_ghi789", False)
print(f"Deactivated opportunity: {status_update['data']['id']}")

# Update tags
tag_update = updater.update_tags("opp_jkl012", ["react", "typescript", "nextjs", "remote"])
print(f"Tags updated for: {tag_update['data']['id']}")

except Exception as e:
print(f"Error updating opportunity: {e}")

Validation Rules​

Field Validation​

const validationRules = {
title: {
minLength: 10,
maxLength: 200,
required: false // for updates
},
description: {
minLength: 50,
maxLength: 5000,
required: false
},
budget: {
min: {
minimum: 0,
maximum: 10000000
},
max: {
minimum: 0,
maximum: 10000000,
mustBeGreaterThanMin: true
},
currency: {
allowedValues: ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
}
},
tags: {
maxItems: 10,
itemMaxLength: 30
},
expiresAt: {
mustBeFuture: true,
maxDaysFromNow: 365
}
};

Business Rules​

  1. Budget Consistency: Maximum budget must be greater than minimum
  2. Expiration Logic: Cannot set expiration date in the past
  3. Tag Limits: Maximum 10 tags per opportunity
  4. Confidential Updates: Only confidential users can update confidential opportunities
  5. Entity Ownership: Users can only update opportunities owned by their entities

Security Considerations​

Permission Checks​

  • Ownership Validation: Verify user owns the opportunity or has entity permissions
  • Confidential Access: Additional checks for confidential opportunities
  • Field-Level Security: Sensitive fields require higher permissions
  • Audit Logging: All updates are logged for security and compliance

Data Sanitization​

  • Input Validation: All fields are validated and sanitized
  • XSS Prevention: HTML content is escaped
  • SQL Injection: Parameterized queries prevent injection attacks
  • Rate Limiting: Prevent abuse with request rate limits

Changelog​

  • v1.0.0 - Initial implementation with basic update functionality
  • v1.1.0 - Added partial update support and field validation
  • v1.2.0 - Enhanced permission system and confidential opportunity support
  • v1.3.0 - Improved error handling and audit logging