← All Articles

AI for Healthcare Admin: Automating Prior Authorizations and Intake Forms

Prior authorizations stall care, frustrate doctors, and bury staff in paperwork. AI can’t fix insurance regulation, but it can eliminate the administrative friction — extracting structured data from forms, validating fields, checking coverage rules, and routing decisions to the right team. Here’s how to build it inside HIPAA constraints.

The problem: manual prior authorization processing

The current workflow looks like this:

  1. Patient needs a procedure.
  2. Doctor’s office calls insurance or fills a form.
  3. Insurance manually reviews the request.
  4. It takes 7–14 days to get an approval (or denial).
  5. Patient can’t schedule; the procedure is delayed.

Where time is wasted on the provider side:

Total: 35 minutes per request × hundreds of requests per week = massive overhead.

The automation: an AI document pipeline

The shape of the workflow:

Prior Auth Form (PDF or web form) ↓ Document Upload (encrypted S3) ↓ Claude: Extract structured data ↓ Validation Layer: Check required fields ↓ Plan Lookup: Query coverage rules (DynamoDB) ↓ Decision Logic: Auto-approve if routine, flag if complex ↓ Route to appropriate team ↓ Patient/Doctor notification

Step 1: Encrypted document storage

All patient data is PHI (Protected Health Information). It must be encrypted at rest and in transit.

resource "aws_s3_bucket" "prior_auth_forms" { bucket = "healthcare-prior-auth-forms-prod" tags = { HIPAA = "true" PHI = "true" } } # Enforce encryption at rest resource "aws_s3_bucket_server_side_encryption_configuration" "prior_auth_forms" { bucket = aws_s3_bucket.prior_auth_forms.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" kms_master_key_id = aws_kms_key.prior_auth.arn } bucket_key_enabled = true } } # Block public access resource "aws_s3_bucket_public_access_block" "prior_auth_forms" { bucket = aws_s3_bucket.prior_auth_forms.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } # Enable versioning for audit trail resource "aws_s3_bucket_versioning" "prior_auth_forms" { bucket = aws_s3_bucket.prior_auth_forms.id versioning_configuration { status = "Enabled" } }

Every document is encrypted, versioned, and auditable.

Step 2: Extract structured data with Claude

Use Claude to parse forms and extract the fields you need:

import base64 import boto3 import json from anthropic import Anthropic from datetime import datetime s3 = boto3.client('s3') dynamodb = boto3.resource('dynamodb') client = Anthropic() def extract_prior_auth_data(bucket: str, key: str): """Extract structured data from a prior auth form. Logs every step for audit.""" audit_table = dynamodb.Table('prior_auth_audit_log') request_id = f"{datetime.now().isoformat()}-{key}" audit_table.put_item(Item={ 'request_id': request_id, 'timestamp': datetime.now().isoformat(), 'document_key': key, 'action': 'extraction_started', 'status': 'in_progress', }) try: doc = s3.get_object(Bucket=bucket, Key=key) content = doc['Body'].read() prompt = """Extract the following from this prior authorization form: - Patient name, ID, DOB - Insurance plan - Procedure code (CPT) and description - Diagnosis codes (ICD-10) - Requesting provider - Date of service requested - Medical necessity justification - Current medications (if applicable) Return as JSON with these exact field names. If a field is not present, return null.""" response = client.messages.create( model='claude-opus-4-1-20250805', max_tokens=1500, messages=[{ 'role': 'user', 'content': [ {'type': 'text', 'text': prompt}, {'type': 'document', 'source': { 'type': 'base64', 'media_type': 'application/pdf', 'data': base64.b64encode(content).decode(), }}, ], }], ) extracted = json.loads(response.content[0].text) audit_table.put_item(Item={ 'request_id': request_id, 'timestamp': datetime.now().isoformat(), 'document_key': key, 'action': 'extraction_completed', 'status': 'success', 'fields_extracted': list(extracted.keys()), }) return extracted except Exception as e: audit_table.put_item(Item={ 'request_id': request_id, 'timestamp': datetime.now().isoformat(), 'document_key': key, 'action': 'extraction_failed', 'status': 'error', 'error': str(e), }) raise

Every extraction is logged for compliance audits. If there’s ever a question about how a decision was made, the audit log shows what data was extracted and when.

Step 3: Validation layer

Check that all required fields are present and correctly formatted:

def validate_prior_auth(data: dict) -> dict: """Returns {valid: bool, errors: [...], warnings: [...]}.""" errors, warnings = [], [] required = ['patient_id', 'insurance_plan', 'procedure_code', 'diagnosis_codes'] for field in required: if not data.get(field): errors.append(f"Missing required field: {field}") if data.get('patient_id') and len(str(data['patient_id'])) < 5: errors.append("Patient ID appears invalid") if data.get('procedure_code') and not is_valid_cpt_code(data['procedure_code']): errors.append("Invalid CPT code") if data.get('diagnosis_codes'): for code in data['diagnosis_codes']: if not is_valid_icd10(code): errors.append(f"Invalid ICD-10 code: {code}") if data.get('medical_necessity') is None: warnings.append("No medical necessity justification provided") if data.get('date_of_service') and is_emergency_timeframe(data['date_of_service']): warnings.append("Urgent/emergency request - expedited review needed") return { 'valid': len(errors) == 0, 'errors': errors, 'warnings': warnings, }

Invalid requests are flagged. Warnings are noted but don’t block processing.

Step 4: Coverage rules and auto-decision

For routine procedures with standard coverage, make automatic decisions; for everything else, route for review:

def check_coverage(data: dict) -> dict: coverage_table = dynamodb.Table('insurance_coverage_rules') plan = data.get('insurance_plan') procedure = data.get('procedure_code') try: response = coverage_table.get_item(Key={ 'plan_id': plan, 'procedure_code': procedure, }) if 'Item' in response: rule = response['Item'] return { 'covered': rule.get('covered', False), 'requires_auth': rule.get('requires_auth', False), 'reason': rule.get('description', 'Standard coverage'), } # Not in rules -- safe default: require manual auth. return {'covered': None, 'requires_auth': True, 'reason': 'Procedure not in standard coverage rules'} except Exception as e: return {'covered': None, 'requires_auth': True, 'reason': f'Coverage check failed: {e}'} def make_decision(data: dict, validation: dict, coverage: dict) -> dict: if not validation['valid']: return {'decision': 'denied', 'reason': 'Incomplete/invalid submission', 'issues': validation['errors'], 'route_to': 'manual_review'} if coverage['covered'] is False: return {'decision': 'denied', 'reason': 'Procedure not covered under plan', 'route_to': 'patient_notification'} if coverage['requires_auth'] is False: return {'decision': 'approved', 'reason': 'Routine procedure, pre-authorized', 'route_to': 'patient_notification'} return {'decision': 'pending_review', 'reason': 'Requires clinical review', 'route_to': 'clinical_team', 'priority': 'high' if validation['warnings'] else 'normal'}

Real outcome: Roughly 40% of requests auto-approve (routine procedures, standard coverage). The remaining 60% are flagged for human review with the data already extracted, validated, and pre-categorized. Clinical staff stop reviewing 100% of requests — they review the 60% that actually need judgment.

Step 5: Notifications and routing

Decisions get routed to the right team via SNS and DynamoDB queues:

def route_and_notify(request: dict, decision: dict): sns = boto3.client('sns') if decision['route_to'] == 'patient_notification': sns.publish( TopicArn='arn:aws:sns:us-east-1:ACCOUNT:prior-auth-approvals', Message=f""" Prior Authorization: {decision['decision'].upper()} Request ID: {request['request_id']} Patient: {request['patient_name']} Procedure: {request['procedure_description']} Reason: {decision['reason']} """) elif decision['route_to'] == 'clinical_team': review_table = dynamodb.Table('prior_auth_clinical_review') review_table.put_item(Item={ 'request_id': request['request_id'], 'created_at': datetime.now().isoformat(), 'priority': decision.get('priority', 'normal'), 'assigned_to': None, 'status': 'pending', 'data': request, }) sns.publish( TopicArn='arn:aws:sns:us-east-1:ACCOUNT:prior-auth-clinical-review', Message=f"New prior auth for clinical review: {request['request_id']}", ) elif decision['route_to'] == 'manual_review': error_queue = dynamodb.Table('prior_auth_error_queue') error_queue.put_item(Item={ 'request_id': request['request_id'], 'created_at': datetime.now().isoformat(), 'reason': decision['reason'], 'issues': decision['issues'], 'status': 'pending_correction', })

HIPAA considerations

What you need on the operations side:

What Claude’s API gives you:

Real numbers

Manual processing: 35 min per form × 100 forms per week = 58 hours per week = $2,900 per week = roughly $150k per year.

With automation:

Build cost: $12–15k. Payback period: 3–4 months. The system pays for itself before the next quarterly review.

Drowning in prior auth? Three Moons Network builds HIPAA-compliant AI systems for healthcare. Email charles@threemoonsnetwork.net and let’s automate the paperwork.

Get the free AI Readiness Checklist

15 questions to diagnose your team’s AI readiness, where you’ll see ROI fastest, and what to tackle first.

✓ Takes 5 minutes ✓ Actionable next steps ✓ No sales pitch

No spam. Unsubscribe anytime.

or

Ready to build AI that actually works?

Let’s talk about how SRE discipline transforms AI from a risky experiment into a reliable business system.

Start the Conversation

About the author

Charles Harvey is the founder of Three Moons Network and a site reliability engineer who builds production-grade AI automation for small businesses — monitoring, cost visibility, and documentation included. He writes about his hands-on AI experiments at floggingclaude.com. Connect on LinkedIn or see the code on GitHub.