Admin Terminology Management APIs
This document provides integration specifications for the Admin Terminology Management system APIs.
Base Configuration
Authentication
All terminology management endpoints require super admin authentication.
// Headers required for all requests
const headers = {
'Authorization': `Bearer ${superAdminToken}`,
'Content-Type': 'application/json'
}Base URL
/admin/terminologyAPI Endpoints
Available Endpoints
GET /admin/terminology- Dashboard overviewGET /admin/terminology/review- Manual review interfaceGET /admin/terminology/search- Search medicinesGET /admin/terminology/health- System health checkPATCH /admin/terminology/:id- Update medicine codes
1. Terminology Dashboard
Endpoint: GET /admin/terminology
Description: Retrieve terminology system overview with statistics and recent activity.
Request:
const response = await fetch('/admin/terminology', {
method: 'GET',
headers: headers
});Response:
{
"success": true,
"message": "Terminology dashboard data fetched",
"data": {
"stats": {
"total_medicines": 197,
"coded_medicines": 0,
"pending_review": 0,
"fhir_ready": 0,
"by_country": [
{
"country": "IN",
"total": 0,
"coded": 0,
"coverage": 0
},
{
"country": "US",
"total": 197,
"coded": 0,
"coverage": 0.0
},
{
"country": "AE",
"total": 0,
"coded": 0,
"coverage": 0
}
],
"coding_quality": {
"avg_confidence": null,
"high_confidence": 0,
"low_confidence": 197,
"manual_verified": 0
}
},
"recent_coding": [
{
"id": 123,
"name": "Medicine Name",
"coding_status": "complete",
"coding_confidence": 0.95,
"last_coded_at": "2025-10-03T10:30:00.000Z"
}
],
"pending_review": [
{
"id": 124,
"name": "Medicine Name",
"manufacturer": "Manufacturer Name",
"coding_status": "pending",
"coding_confidence": 0.0
}
]
}
}2. Manual Review Interface
Endpoint: GET /admin/terminology/review
Description: Retrieve medicines requiring manual review with filtering and pagination.
Query Parameters:
page(integer): Page number (default: 1)per_page(integer): Items per page (default: 20, max: 100)country(string): Filter by country code (IN/US/AE)confidence_range(string): Filter by confidence (low/medium/high)low: confidence < 0.3medium: confidence between 0.3 and 0.7high: confidence > 0.7
status(string): Filter by coding status (pending/partial/complete/failed)prescription_usage(string): Filter by prescription usage (prescribed)
Request:
const params = new URLSearchParams({
page: 1,
per_page: 20,
country: 'US',
confidence_range: 'low'
});
const response = await fetch(`/admin/terminology/review?${params}`, {
method: 'GET',
headers: headers
});Response:
{
"success": true,
"message": "Review data fetched",
"data": {
"medicines": [
{
"id": 196,
"name": "Aagnil Syrup",
"manufacturer": "Haledew Remedies",
"country": "US",
"med_type": "Syrup",
"prescription_type": "otc",
"snomed_code": null,
"country_specific_code": null,
"ndc_code": null,
"coding_status": "pending",
"coding_confidence": 0.0,
"coding_source": null,
"last_coded_at": null,
"prescription_count": 0,
"fhir_ready": false
}
],
"pagination": {
"page": 1,
"per_page": 20,
"total_count": 197,
"total_pages": 10
},
"filters": {
"country": "US",
"confidence_range": "low",
"status": null,
"prescription_usage": null
}
}
}3. Update Medicine Codes
Endpoint: PATCH /admin/terminology/:id
Description: Manually update terminology codes for a specific medicine.
Path Parameters:
id(integer): Medicine ID
Request Body (at least one field required):
{
"snomed_code": "387458008",
"country_specific_code": "RX789012",
"ndc_code": "54321-987-65",
"coding_status": "complete",
"coding_confidence": 0.98
}Validation Rules:
- Required: At least one coding field must be provided with a non-empty value
- Valid coding fields:
snomed_code,country_specific_code,ndc_code,coding_status,coding_confidence coding_confidence: Must be between 0.0 and 1.0 (inclusive)coding_status: Must be one of:pending,partial,complete,failed- String fields cannot be empty or contain only whitespace
- Automatically sets:
coding_sourceto "manual"last_coded_atto current timestamp- If codes are provided without confidence: defaults to 0.9 confidence and "complete" status
Request:
const medicineId = 196;
const updateData = {
snomed_code: "387458008",
country_specific_code: "RX789012",
ndc_code: "54321-987-65",
coding_status: "complete",
coding_confidence: 0.98
};
const response = await fetch(`/admin/terminology/${medicineId}`, {
method: 'PATCH',
headers: headers,
body: JSON.stringify(updateData)
});Success Response:
{
"success": true,
"message": "Medicine codes updated successfully",
"data": {
"medicine": {
"id": 196,
"name": "Aagnil Syrup",
"snomed_code": "387458008",
"country_specific_code": "RX789012",
"ndc_code": "54321-987-65",
"coding_status": "complete",
"coding_confidence": 0.98,
"coding_source": "manual",
"last_coded_at": "2025-10-03T10:30:00.000Z"
},
"fhir_ready": true,
"coding_summary": {
"snomed_code": "387458008",
"country_specific_code": "RX789012",
"ndc_code": "54321-987-65",
"status": "complete",
"confidence": 0.98,
"source": "manual",
"last_coded_at": "2025-10-03T10:30:00.000Z"
}
}
}Validation Error Examples:
{
"success": false,
"errors": [
{
"source": "request",
"title": "Request At least one coding field must be provided with a non-empty value (snomed_code, country_specific_code, ndc_code, coding_status, or coding_confidence)"
}
]
}{
"success": false,
"errors": [
{
"source": "coding_confidence",
"title": "Coding Confidence must be between 0.0 and 1.0"
}
]
}{
"success": false,
"errors": [
{
"source": "coding_status",
"title": "Coding Status must be one of: pending, partial, complete, failed"
}
]
}4. Search Medicines
Endpoint: GET /admin/terminology/search
Description: Search medicines by name, SNOMED code, or other terminology codes.
Query Parameters:
q(string, required): Search query (minimum 2 characters)country(string): Filter by country code (IN/US/AE)limit(integer): Maximum results (default: 10, max: 50)
Request:
const params = new URLSearchParams({
q: 'Aagnil',
country: 'US',
limit: 10
});
const response = await fetch(`/admin/terminology/search?${params}`, {
method: 'GET',
headers: headers
});Response:
{
"success": true,
"message": "Search completed",
"data": {
"type": "search",
"data": {
"query": "Aagnil",
"limit": 10,
"results": [
{
"id": 196,
"name": "Aagnil Syrup",
"manufacturer": "Haledew Remedies",
"country": "US",
"coding_status": "complete",
"coding_confidence": 0.98,
"snomed_code": "387458008",
"country_specific_code": "RX789012",
"ndc_code": "54321-987-65",
"coding_summary": {
"snomed_code": "387458008",
"country_specific_code": "RX789012",
"ndc_code": "54321-987-65",
"status": "complete",
"confidence": 0.98,
"source": "manual",
"last_coded_at": "2025-09-15T20:08:33.349Z"
},
"fhir_ready": true,
"prescription_count": 0
}
],
"total_found": 1
}
}
}5. System Health Check
Endpoint: GET /admin/terminology/health
Description: Retrieve comprehensive system health status for terminology services.
Request:
const response = await fetch('/admin/terminology/health', {
method: 'GET',
headers: headers
});Response:
{
"success": true,
"message": "System health check completed",
"data": {
"type": "health",
"data": {
"timestamp": "2025-09-15T20:03:06.910Z",
"overall_status": "unhealthy",
"components": {
"database": {
"status": "healthy",
"response_time_ms": 9.01,
"total_medicines": 197,
"coded_medicines": 0,
"coding_coverage": 0.0
},
"terminology_apis": {
"overall_status": "degraded",
"apis": {
"nrces_snomed": {
"status": "unhealthy",
"error": "Failed to open TCP connection to snowstorm.nrces.in:443",
"response_time_ms": 1.43,
"last_checked": "2025-09-15T20:03:06.920Z"
},
"rxnorm": {
"status": "healthy",
"response_time_ms": 1264.89,
"last_checked": "2025-09-15T20:03:08.185Z"
},
"fda_ndc": {
"status": "healthy",
"response_time_ms": 1286.37,
"last_checked": "2025-09-15T20:03:09.472Z"
}
},
"healthy_count": 2,
"total_count": 3
},
"job_queues": {
"status": "healthy",
"queue_size": 0,
"processing": 0,
"retrying": 0,
"dead": 0,
"total_pending": 0
},
"coding_quality": {
"status": "unhealthy",
"coding_coverage": 0.0,
"average_confidence": 0,
"failure_rate": 0.0,
"metrics": {
"total_medicines": 197,
"coded_medicines": 0,
"high_confidence": 1,
"low_confidence": 196,
"failed_coding": 0
}
},
"data_freshness": {
"status": "healthy",
"last_terminology_update": "2025-09-15T20:01:28.866Z",
"days_since_update": 0.0,
"recent_imports": 197,
"recent_updates": 197
}
},
"alerts": [
{
"severity": "warning",
"component": "database",
"message": "Low coding coverage: 0.0% (threshold: 85.0%)"
},
{
"severity": "warning",
"component": "apis",
"message": "Some APIs are down: nrces_snomed"
},
{
"severity": "warning",
"component": "quality",
"message": "Coding quality issues detected"
}
],
"metrics": {
"uptime": 99.9,
"throughput": 1,
"error_rate": 0.0,
"api_performance": {
"avg_response_time_ms": 150,
"success_rate": 99.5
}
}
}
}
}Error Handling
All APIs follow a consistent error response format:
{
"success": false,
"errors": [
{
"source": "field_name",
"title": "Error message description"
}
]
}Common Error Scenarios
Medicine Not Found:
{
"success": false,
"errors": [
{
"source": "medicine",
"title": "Medicine not found"
}
]
}Search Query Too Short:
{
"success": false,
"errors": [
{
"source": "query",
"title": "Query must be at least 2 characters"
}
]
}HTTP Status Codes
- 200 OK: Successful request
- 401 Unauthorized: Invalid or missing authentication token
- 403 Forbidden: Insufficient permissions (requires super admin)
- 404 Not Found: Medicine or resource not found
- 422 Unprocessable Entity: Validation errors in request data
- 500 Internal Server Error: Server-side errors
Integration Examples
React Integration Example
// hooks/useTerminologyAPI.js
import { useState, useEffect } from 'react';
export const useTerminologyDashboard = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchDashboard = async () => {
try {
const response = await fetch('/admin/terminology', {
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
setData(result.data);
} else {
setError(result.errors?.[0]?.title || 'Unknown error');
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchDashboard();
}, []);
return { data, loading, error };
};
// components/TerminologyDashboard.jsx
import React from 'react';
import { useTerminologyDashboard } from '../hooks/useTerminologyAPI';
const TerminologyDashboard = () => {
const { data, loading, error } = useTerminologyDashboard();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="terminology-dashboard">
<h1>Terminology Management</h1>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Medicines</h3>
<p>{data.stats.total_medicines.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>Coded Medicines</h3>
<p>{data.stats.coded_medicines.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>FHIR Ready</h3>
<p>{data.stats.fhir_ready.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>Pending Review</h3>
<p>{data.stats.pending_review.toLocaleString()}</p>
</div>
</div>
<div className="country-breakdown">
<h2>Coverage by Country</h2>
{data.stats.by_country.map(country => (
<div key={country.country} className="country-stat">
<span>{country.country}</span>
<span>{country.coded}/{country.total}</span>
<span>{country.coverage}%</span>
</div>
))}
</div>
</div>
);
};Medicine Review Component
// components/MedicineReview.jsx
import React, { useState, useEffect } from 'react';
const MedicineReview = () => {
const [medicines, setMedicines] = useState([]);
const [filters, setFilters] = useState({
country: '',
confidence_range: '',
page: 1
});
const [pagination, setPagination] = useState({});
const fetchMedicines = async () => {
const params = new URLSearchParams(
Object.fromEntries(
Object.entries(filters).filter(([key, value]) => value !== '')
)
);
const response = await fetch(`/admin/terminology/review?${params}`, {
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
setMedicines(result.data.medicines);
setPagination(result.data.pagination);
}
};
useEffect(() => {
fetchMedicines();
}, [filters]);
const updateMedicine = async (medicineId, updateData) => {
const response = await fetch(`/admin/terminology/${medicineId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updateData)
});
const result = await response.json();
if (result.success) {
// Refresh the list
fetchMedicines();
} else {
console.error('Update failed:', result.errors);
}
};
return (
<div className="medicine-review">
<div className="filters">
<select
value={filters.country}
onChange={(e) => setFilters({...filters, country: e.target.value, page: 1})}
>
<option value="">All Countries</option>
<option value="IN">India</option>
<option value="US">United States</option>
<option value="AE">UAE</option>
</select>
<select
value={filters.confidence_range}
onChange={(e) => setFilters({...filters, confidence_range: e.target.value, page: 1})}
>
<option value="">All Confidence</option>
<option value="low">Low (< 0.3)</option>
<option value="medium">Medium (0.3-0.7)</option>
<option value="high">High (> 0.7)</option>
</select>
</div>
<div className="medicines-list">
{medicines.map(medicine => (
<MedicineCard
key={medicine.id}
medicine={medicine}
onUpdate={updateMedicine}
/>
))}
</div>
<div className="pagination">
<button
disabled={pagination.page === 1}
onClick={() => setFilters({...filters, page: pagination.page - 1})}
>
Previous
</button>
<span>Page {pagination.page} of {pagination.total_pages}</span>
<button
disabled={pagination.page === pagination.total_pages}
onClick={() => setFilters({...filters, page: pagination.page + 1})}
>
Next
</button>
</div>
</div>
);
};
// Medicine card component for updating individual medicines
const MedicineCard = ({ medicine, onUpdate }) => {
const [editing, setEditing] = useState(false);
const [formData, setFormData] = useState({
snomed_code: medicine.snomed_code || '',
country_specific_code: medicine.country_specific_code || '',
ndc_code: medicine.ndc_code || '',
coding_status: medicine.coding_status || 'pending',
coding_confidence: medicine.coding_confidence || ''
});
const handleSubmit = async (e) => {
e.preventDefault();
// Filter out empty values
const updateData = Object.fromEntries(
Object.entries(formData).filter(([key, value]) => value !== '')
);
await onUpdate(medicine.id, updateData);
setEditing(false);
};
return (
<div className="medicine-card">
<div className="medicine-info">
<h3>{medicine.name}</h3>
<p>Manufacturer: {medicine.manufacturer}</p>
<p>Country: {medicine.country}</p>
<p>Status: {medicine.coding_status}</p>
<p>Confidence: {(medicine.coding_confidence * 100).toFixed(1)}%</p>
<p>FHIR Ready: {medicine.fhir_ready ? 'Yes' : 'No'}</p>
</div>
{editing ? (
<form onSubmit={handleSubmit} className="edit-form">
<input
type="text"
placeholder="SNOMED Code"
value={formData.snomed_code}
onChange={(e) => setFormData({...formData, snomed_code: e.target.value})}
/>
<input
type="text"
placeholder="Country Code (RxNorm/CDCI)"
value={formData.country_specific_code}
onChange={(e) => setFormData({...formData, country_specific_code: e.target.value})}
/>
<input
type="text"
placeholder="NDC Code"
value={formData.ndc_code}
onChange={(e) => setFormData({...formData, ndc_code: e.target.value})}
/>
<select
value={formData.coding_status}
onChange={(e) => setFormData({...formData, coding_status: e.target.value})}
>
<option value="pending">Pending</option>
<option value="partial">Partial</option>
<option value="complete">Complete</option>
<option value="failed">Failed</option>
</select>
<input
type="number"
step="0.01"
min="0"
max="1"
placeholder="Confidence (0.0-1.0)"
value={formData.coding_confidence}
onChange={(e) => setFormData({...formData, coding_confidence: e.target.value})}
/>
<div className="form-actions">
<button type="submit">Save</button>
<button type="button" onClick={() => setEditing(false)}>Cancel</button>
</div>
</form>
) : (
<button onClick={() => setEditing(true)}>Edit Codes</button>
)}
</div>
);
};PATCH API Testing
Example Test Cases
// Test 1: Update single field
const response1 = await fetch('/admin/terminology/196', {
method: 'PATCH',
headers: headers,
body: JSON.stringify({ "coding_status": "complete" })
});
// Test 2: Update multiple fields
const response2 = await fetch('/admin/terminology/196', {
method: 'PATCH',
headers: headers,
body: JSON.stringify({
"snomed_code": "387458008",
"country_specific_code": "RX789012",
"ndc_code": "54321-987-65",
"coding_status": "complete",
"coding_confidence": 0.98
})
});
// Test 3: Invalid request (should fail)
const response3 = await fetch('/admin/terminology/196', {
method: 'PATCH',
headers: headers,
body: JSON.stringify({}) // Empty body - will be rejected
});
// Test 4: Invalid confidence (should fail)
const response4 = await fetch('/admin/terminology/196', {
method: 'PATCH',
headers: headers,
body: JSON.stringify({ "coding_confidence": 1.5 }) // > 1.0 - will be rejected
});Security Considerations
- Authentication: All endpoints require super admin authentication
- Input Validation: Comprehensive validation on all parameters and request bodies
- SQL Injection Protection: Parameterized queries throughout
- Empty/Whitespace Validation: Prevents meaningless updates
- Audit Logging: All terminology changes are logged with user information
- Rate Limiting: Prevents API abuse
Performance Notes
- Pagination: Always use pagination for large datasets in review endpoint
- Filtering: Use country and confidence filters to reduce payload sizes
- Caching: Dashboard data is cached; search results may be cached client-side
- Database Queries: Optimized with proper includes and indexing
Background Job System
Automatic Terminology Coding
The system uses Sidekiq background jobs for automatic terminology coding:
Job: TerminologyCodingJob
- Location:
parts/core/services/terminology_coding_job.rb - Queue:
:terminology - Retry Policy: 3 attempts with exponential backoff
- Processing: Batch processing with 0.1s delay between medicines for API rate limiting
When Jobs are Triggered:
- Medicine Import: Automatically queued when medicines are imported via rake tasks
- Manual Processing: Via
rake terminology:process_uncodedto batch process uncoded medicines - Priority Processing: Via
rake terminology:process_priorityfor high-priority medicines
Important Configuration:
Ensure config/environments/development.rb has:
config.active_job.queue_adapter = :sidekiqWithout this, jobs will run in-memory (AsyncAdapter) instead of queueing to Sidekiq.
Monitoring:
- Sidekiq UI:
http://localhost:3000/sidekiq/queues - Check terminology queue status in health endpoint
- View job logs for processing details
Rake Tasks
Available terminology management rake tasks:
# Process all uncoded medicines (batch size: 100)
rake terminology:process_uncoded
# Process high-priority medicines (prescribed medicines first)
rake terminology:process_priority
# Process medicines by country
rake terminology:process_country COUNTRY=US
# Get statistics and available tasks
rake terminology:stats
# Generate detailed quality report
rake terminology:quality_report
# Export medicines for manual review (saves CSV to tmp/)
rake terminology:export_for_review
# Import manual coding corrections
rake terminology:import_corrections CORRECTIONS_FILE=path/to/corrections.csv
# Validate existing medicine codes
rake terminology:validate_codes
# Fix common coding issues
rake terminology:fix_coding_issuesImplementation Notes
This API specification reflects the current implementation and has been tested with real data. All examples show actual request/response formats from the working system.
Current Limitations
-
API Availability: Some external terminology APIs may be unavailable:
- NRCES SNOMED (India):
snowstorm.nrces.inis currently down - RxNorm (US): Active and functional
- FDA NDC (US): Active and functional
- NRCES SNOMED (India):
-
Coding Success Rate: Automatic coding success varies by medicine name complexity:
- Generic/standard names: Higher success rate (>70% confidence)
- Proprietary/complex names: Lower success rate (<30% confidence)
- Manual review recommended for low-confidence results
-
Response Format: Update endpoint returns full medicine object and coding summary (not just success message)
Best Practices
-
Manual Review Workflow:
- Filter by
confidence_range=lowto find medicines needing attention - Verify codes against official terminology sources
- Use
coding_confidenceto indicate certainty of manual coding
- Filter by
-
Batch Processing:
- Use rake tasks for large-scale coding operations
- Monitor Sidekiq queue to track progress
- Check health endpoint to verify system status
-
Error Handling:
- Always check
successfield in responses - Handle validation errors gracefully with user feedback
- Log failed coding attempts for investigation
- Always check