Skip to main content

Supplier Monitoring with 483 Intelligence

Use CTWise 483 Intelligence to monitor your suppliers' FDA inspection compliance, assess facility risk, and build data-driven supplier qualification workflows.

Introduction

Why Monitor Suppliers for FDA Inspections?

FDA 483 observations can signal serious compliance issues that may affect:

  • Product Quality: OAI findings often indicate systemic quality problems
  • Supply Continuity: Enforcement actions can disrupt manufacturing
  • Regulatory Risk: Your products may be affected by supplier deficiencies
  • Audit Findings: Regulators expect you to monitor supplier compliance

The Cost of Missed 483s

Missing a critical supplier 483 can result in:

  • Product Recalls: $1M-$10M+ in direct costs
  • Supply Chain Disruption: 6-18 months to qualify alternate suppliers
  • Regulatory Citations: FDA may cite you for inadequate supplier oversight (21 CFR 211.84)
  • Delayed Approvals: New applications may be delayed if supplier has OAI status

Step-by-Step: Setting Up Supplier Monitoring

Step 1: Identify Your Suppliers' FEI Numbers

Every FDA-registered facility has a unique Facility Establishment Identifier (FEI) number. You need FEI numbers to query the 483 Intelligence API.

Where to find FEI numbers:

  • Quality Agreements (Section 1.2 - Facility Information)
  • Supplier Qualification Packages
  • FDA Data Dashboard: datadashboard.fda.gov

You can also search for facilities using the CTWise API:

import os
import requests

API_KEY = os.getenv("CTWISE_API_KEY")
BASE_URL = "https://api.ctwise.ai/v1"

# Search facilities by state
response = requests.get(
f"{BASE_URL}/483/facilities",
headers={"X-Api-Key": API_KEY},
params={"state": "NJ", "limit": 20}
)

for facility in response.json()["results"]:
print(f"{facility['fei_number']}: {facility['legal_name']} ({facility['city']}, {facility['state']})")

Step 2: Get Facility Profiles

Once you have FEI numbers, retrieve detailed facility profiles:

# Your critical suppliers
supplier_feis = [
"1000116488", # Supplier A
"3002809064", # Supplier B
"1000234567", # Supplier C
]

for fei in supplier_feis:
response = requests.get(
f"{BASE_URL}/483/facilities/{fei}",
headers={"X-Api-Key": API_KEY}
)

if response.status_code == 200:
facility = response.json()
print(f"\n{'='*50}")
print(f"Facility: {facility['legal_name']}")
print(f"FEI: {facility['fei_number']}")
print(f"Location: {facility['city']}, {facility['state']}")
print(f"Total Inspections: {facility['total_inspections']}")
print(f"Classifications: OAI={facility['classification_breakdown']['oai_count']}, "
f"VAI={facility['classification_breakdown']['vai_count']}, "
f"NAI={facility['classification_breakdown']['nai_count']}")
print(f"Risk: {facility['risk_assessment']['risk_score']:.1f} "
f"({facility['risk_assessment']['risk_level']})")

Step 3: Check Risk Scores

Get detailed risk scores with 5-factor breakdowns (v2.0 methodology):

for fei in supplier_feis:
response = requests.get(
f"{BASE_URL}/483/risk-scores/{fei}",
headers={"X-Api-Key": API_KEY}
)

if response.status_code == 200:
risk = response.json()

print(f"\nFEI: {risk['fei_number']}")
print(f" Risk Score: {risk['risk_score']:.1f}/100 ({risk['risk_level']})")
print(f" Methodology: {risk['methodology_version']}")

# Show factor breakdown
print(f" Factor Breakdown:")
max_vals = {"oai_ratio": 40, "citation_frequency": 25, "recency": 15, "severity": 10, "peer_benchmark": 10}
for name, value in risk["factors"].items():
print(f" {name}: {value:.1f}/{max_vals[name]}")

Step 4: Review Citation History

Get the citation history for specific facilities:

fei = "1000116488"

response = requests.get(
f"{BASE_URL}/483/facilities/{fei}/citations",
headers={"X-Api-Key": API_KEY},
params={"limit": 50}
)

data = response.json()
print(f"Total citations: {data['total']}\n")

# Group by CFR
from collections import Counter
cfr_counts = Counter(obs["act_cfr_number"] for obs in data["results"])

print("Most cited CFRs:")
for cfr, count in cfr_counts.most_common(5):
print(f" {cfr}: {count} citations")

Step 5: Search for Specific Compliance Concerns

Use semantic search to find relevant observations for your suppliers:

# Search for data integrity issues
response = requests.post(
f"{BASE_URL}/483/observations/search",
headers={
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
},
json={
"query": "data integrity electronic records audit trail failures",
"top_k": 10,
"min_similarity": 0.70
}
)

results = response.json()
for obs in results["results"]:
# Check if any of your suppliers appear
if obs["fei_number"] in supplier_feis:
print(f"ALERT: {obs['legal_name']} ({obs['fei_number']})")
print(f" CFR: {obs['act_cfr_number']}")
print(f" {obs['short_description'][:150]}...")
print(f" Similarity: {obs['similarity_score']:.2f}")
print()

Building a Supplier Risk Dashboard

Combine all API endpoints to create a comprehensive supplier monitoring report:

#!/usr/bin/env python3
"""
Supplier Risk Dashboard

Generates a comprehensive supplier compliance report using CTWise 483 Intelligence.
"""

import os
import requests
import pandas as pd
from datetime import datetime

API_KEY = os.getenv("CTWISE_API_KEY")
BASE_URL = "https://api.ctwise.ai/v1"

def get_supplier_risk_data(fei_list):
"""Fetch risk scores and facility data for a list of suppliers."""
results = []

for fei in fei_list:
# Get risk score
risk_resp = requests.get(
f"{BASE_URL}/483/risk-scores/{fei}",
headers={"X-Api-Key": API_KEY}
)

if risk_resp.status_code != 200:
continue

risk = risk_resp.json()
factors = risk["factors"]

# Also get facility profile for additional context
fac_resp = requests.get(
f"{BASE_URL}/483/facilities/{fei}",
headers={"X-Api-Key": API_KEY}
)
fac = fac_resp.json() if fac_resp.status_code == 200 else {}

results.append({
"fei_number": risk["fei_number"],
"legal_name": fac.get("legal_name", fei),
"risk_score": risk["risk_score"],
"risk_level": risk["risk_level"],
"oai_ratio": factors["oai_ratio"],
"citation_freq": factors["citation_frequency"],
"peer_benchmark": factors["peer_benchmark"],
"total_inspections": fac.get("total_inspections", 0),
"last_inspection": fac.get("last_inspection_date", "N/A"),
})

return pd.DataFrame(results)

def generate_dashboard(fei_list):
"""Generate supplier risk dashboard."""

df = get_supplier_risk_data(fei_list)

if df.empty:
print("No supplier data available.")
return

df = df.sort_values("risk_score", ascending=False)

print("\n" + "="*70)
print("SUPPLIER COMPLIANCE RISK DASHBOARD")
print(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("="*70)

# Summary
print(f"\nTotal Suppliers: {len(df)}")
print(f"Average Risk Score: {df['risk_score'].mean():.1f}")

print(f"\nRisk Distribution:")
for level in ["critical", "high", "medium", "low"]:
count = len(df[df["risk_level"] == level])
if count > 0:
print(f" {level}: {count} suppliers")

# Detail table
print(f"\n{'='*70}")
print(f"{'Supplier':<30} {'Risk':>6} {'Level':<10} {'OAI Ratio':>10} {'Last Inspection':<16}")
print(f"{'-'*30} {'-'*6} {'-'*10} {'-'*10} {'-'*16}")

for _, row in df.iterrows():
name = row["legal_name"][:28]
print(f"{name:<30} {row['risk_score']:>5.1f} {row['risk_level']:<10} "
f"{row['oai_ratio']:>9.1f} {row['last_inspection']:<16}")

# High-risk alerts
high_risk = df[df["risk_level"].isin(["high", "critical"])]
if not high_risk.empty:
print(f"\nACTION REQUIRED: {len(high_risk)} high/critical risk suppliers")
for _, row in high_risk.iterrows():
print(f" - {row['legal_name']}: score {row['risk_score']:.1f} "
f"({row['risk_level']})")

print("\n" + "="*70)

# Export to CSV
csv_path = f"supplier_risk_{datetime.now().strftime('%Y%m%d')}.csv"
df.to_csv(csv_path, index=False)
print(f"Exported to: {csv_path}")

return df

if __name__ == "__main__":
MY_SUPPLIERS = [
"1000116488", # Supplier A
"3002809064", # Supplier B
"1000234567", # Supplier C
"1000345678", # Supplier D
]
generate_dashboard(MY_SUPPLIERS)

Supplier Qualification Workflow

Use risk scores and citation history to build a data-driven supplier qualification process:

def qualify_supplier(fei, supplier_name):
"""Evaluate supplier FDA compliance for qualification decision."""

# Step 1: Get facility profile
facility = requests.get(
f"{BASE_URL}/483/facilities/{fei}",
headers={"X-Api-Key": API_KEY}
).json()

# Step 2: Get risk score (v2.0 with 5-factor breakdown)
risk = requests.get(
f"{BASE_URL}/483/risk-scores/{fei}",
headers={"X-Api-Key": API_KEY}
).json()

# Step 3: Get citation history
citations = requests.get(
f"{BASE_URL}/483/facilities/{fei}/citations",
headers={"X-Api-Key": API_KEY},
params={"limit": 100}
).json()

# Decision logic using risk score and factors
risk_score = risk["risk_score"]
risk_level = risk["risk_level"]
factors = risk["factors"]

if risk_level == "critical":
decision = "REJECT"
reason = "Critical risk level"
elif risk_level == "high" or factors["oai_ratio"] > 25:
decision = "CONDITIONAL"
reason = f"High risk ({risk_score:.1f}) or high OAI ratio ({factors['oai_ratio']:.1f}/40)"
elif risk_level == "medium":
decision = "APPROVE WITH MONITORING"
reason = f"Medium risk ({risk_score:.1f}), enhanced monitoring recommended"
else:
decision = "APPROVE"
reason = f"Low risk ({risk_score:.1f}), standard monitoring"

print(f"\n{'='*60}")
print(f"SUPPLIER QUALIFICATION: {supplier_name}")
print(f"{'='*60}")
print(f"FEI: {fei}")
print(f"Facility: {facility['legal_name']}")
print(f"Location: {facility['city']}, {facility['state']}")
print(f"Risk Score: {risk_score:.1f}/100 ({risk_level})")
print(f"Methodology: {risk['methodology_version']}")
print(f"Top Factor: oai_ratio={factors['oai_ratio']:.1f}/40")
print(f"Citations: {citations['total']}")
print(f"\nDECISION: {decision}")
print(f"Reason: {reason}")
print(f"{'='*60}")

return {"decision": decision, "risk_score": risk_score, "reason": reason}

Best Practices

Review Frequency Recommendations

Supplier CriticalityReview FrequencyWhat to Check
High (Single source, API, sterile)MonthlyRisk scores + recent citations
Medium (Alternate source available)QuarterlyRisk scores
Low (Non-critical, easy substitution)Semi-annuallyRisk scores

Escalation Procedures

Level 1 - Low Risk (risk_level: low)

  • Document in supplier file
  • Standard annual audit schedule

Level 2 - Medium Risk (risk_level: medium)

  • Review within 5 business days
  • Contact supplier for status update
  • QA Manager review

Level 3 - High Risk (risk_level: high)

  • Review within 2 business days
  • Request supplier CAPA plan
  • Conduct supplier audit
  • Executive notification

Level 4 - Critical Risk (risk_level: critical)

  • Review within 24 hours
  • Immediate supplier meeting
  • Assess impact on products
  • Evaluate alternate sourcing
  • VP Quality escalation
  1. Periodic Check (per frequency above)

    • Query risk scores for all monitored suppliers
    • Compare against previous scores
  2. Risk Increase Detected

    • Review citation history for new findings
    • Identify which CFR areas are affected
    • Assess relevance to your products
  3. Supplier Contact (if risk increased)

    • Request explanation for new findings
    • Request CAPA plan if OAI
    • Schedule follow-up
  4. Documentation

    • Record risk scores in supplier file
    • Document assessment and decisions
    • Update supplier qualification status

Next Steps

  1. Gather FEI numbers for your critical suppliers
  2. Run initial risk assessments using the dashboard script
  3. Establish review cadence based on supplier criticality
  4. Build qualification criteria using risk scores as one input

For more information: