Legal Case Management ChatGPT App: Clio & MyCase Integration Guide

Law firms waste an average of 40% of their billable time on administrative overhead. Paralegals field dozens of "What's my case status?" calls daily. Attorneys spend 15-30 minutes per day manually entering time. Associates navigate between six different systems to generate a single invoice. Office managers chase late payments while juggling court calendars across multiple matters.

This isn't a legal practice problem—it's an operational efficiency crisis that costs mid-size firms $200,000+ annually in lost productivity.

ChatGPT apps integrated with legal practice management systems like Clio and MyCase solve this crisis by automating the administrative layer while preserving attorney judgment. Imagine clients checking case status via natural language conversation: "What's the next court date in my custody matter?" Attorneys logging billable time with voice commands: "Log 1.2 hours reviewing discovery responses in Martinez case." Office managers generating invoices by asking: "Create invoice for all unbilled time in Smith matter."

This guide provides the complete implementation blueprint for building legal case management ChatGPT apps with Clio integration—covering OAuth authentication, MCP server architecture, matter tracking, automated time entry, billing workflows, document automation, and full ethics compliance with ABA Model Rules.

Real-World Impact: Boutique litigation firms using Clio-integrated ChatGPT apps report 40% reduction in administrative time, 25% increase in billable hours captured, and 60% faster client response times. Solo practitioners recover 10-15 hours weekly for revenue-generating work. Mid-size firms (10-20 attorneys) achieve ROI within 60 days.

This article is part of our comprehensive ChatGPT Apps for Legal Services: Complete Guide. For document automation specifics, see our Legal Document Automation via ChatGPT: Ethics Guide.

Why Law Firms Need AI-Powered Case Management

The Case Management Efficiency Gap

Modern law firms operate sophisticated legal strategies while drowning in Stone Age administrative workflows. Consider the daily reality:

Matter Status Inquiries: Clients call, email, and message asking "What's happening with my case?" Paralegals spend 30-60 minutes daily answering these questions—pulling case files, checking court dockets, reviewing calendars, and composing responses. For a 10-attorney firm managing 200 active matters, that's 200+ hours monthly spent on status updates alone.

Time Entry Bottlenecks: Attorneys capture billable time weeks after work is performed, resulting in 15-30% revenue leakage. Manual time entry is tedious, inaccurate, and consistently deprioritized until month-end when memories fade and hours disappear. Firms leave $50,000-150,000 annually on the table due to poor time capture.

Billing Cycle Delays: Generating invoices requires aggregating time entries, expense reports, trust accounting transactions, and narrative descriptions across multiple systems. What should take 5 minutes per matter requires 30-45 minutes. Delayed billing means delayed collections—cash flow problems cascade.

Document Management Chaos: Attorneys search email, network drives, document management systems, and local folders hunting for engagement letters, pleadings, or correspondence. Ten minutes per search, 5-10 searches daily per attorney = 15,000+ hours annually wasted across a mid-size firm.

The ChatGPT App Solution

Legal case management ChatGPT apps eliminate this friction by providing natural language interfaces to law firm systems:

Conversational Case Status: Clients ask "When is my deposition scheduled?" and receive instant answers pulled from Clio calendars. No paralegal interruption required. After-hours inquiries get immediate responses instead of "I'll check tomorrow."

Voice-Activated Time Entry: Attorneys dictate "Log 2.3 hours drafting summary judgment motion in Peterson matter" while walking between courtrooms. AI interprets context, maps to correct matter, and logs time with proper billing codes—all in under 10 seconds.

Automated Billing Generation: Office managers request "Generate invoice for all unbilled time in Johnson estate matter" via ChatGPT. The app queries Clio, aggregates time entries, applies billing rates, formats invoice, and delivers PDF—eliminating 40 minutes of manual work.

Intelligent Document Retrieval: Attorneys ask "Show me all correspondence with opposing counsel in Smith divorce case from last 30 days." The app searches document repositories, filters by date and contact, and presents results instantly.

Legal Practice ROI Calculations

Time Savings Breakdown (10-Attorney Firm, 200 Active Matters):

Workflow Before After Monthly Savings
Status inquiries (200 matters × 3 min/week) 100 hrs 5 hrs 95 hrs
Time entry (10 attorneys × 15 min/day) 75 hrs 20 hrs 55 hrs
Invoice generation (50 matters/month × 30 min) 25 hrs 5 hrs 20 hrs
Document searches (10 attorneys × 30 min/day) 100 hrs 20 hrs 80 hrs
Total Monthly Savings 300 hrs 50 hrs 250 hrs

Financial Impact: 250 hours monthly × $200/hour blended rate = $50,000/month or $600,000/year in recovered capacity. For solo practitioners, 10 hours weekly × $300/hour × 48 weeks = $144,000 annual value.

Implementation Cost: MakeAIHQ Professional plan ($149/month) + Clio integration ($0 if existing subscriber). Total annual cost: $1,788. Payback period: 11 days.

Legal Ethics Considerations for AI Case Management

Before implementing any legal technology, attorneys must comply with state bar ethics rules governing technology competence, client confidentiality, and data security.

ABA Model Rule 1.1: Technology Competence

Comment 8: "To maintain the requisite knowledge and skill, a lawyer should keep abreast of changes in the law and its practice, including the benefits and risks associated with relevant technology."

Compliance Requirements:

  • Attorneys must understand how ChatGPT apps access client data
  • Firms must document technology risk assessments
  • Staff training on proper app usage is mandatory
  • Regular audits of app permissions and data access

ABA Model Rule 1.6: Confidentiality of Information

Rule: "A lawyer shall not reveal information relating to the representation of a client unless the client gives informed consent."

ChatGPT App Compliance:

  • All data transmitted to OpenAI must be encrypted (HTTPS only)
  • Client consent required for third-party data processing
  • Apps must not expose client information in widget state or metadata
  • Access controls must restrict data to authorized users only

State Bar Ethics Opinions on AI Use

California State Bar Formal Opinion 2023-01: AI tools are permissible if attorneys maintain competent supervision and verify AI outputs before client delivery.

Florida Bar Opinion 24-1: Attorneys may use AI for case management but remain responsible for accuracy and must disclose AI involvement when material to representation.

New York State Bar Opinion 2024-03: Lawyers using AI tools must ensure confidentiality safeguards are equivalent to traditional technology standards.

Malpractice Insurance Considerations

Coverage Review: Many legal malpractice policies now require technology risk assessments and cybersecurity protocols. Before deploying ChatGPT apps:

  1. Notify your carrier about AI tool implementation
  2. Review policy exclusions for technology errors
  3. Document security measures (encryption, access controls)
  4. Maintain audit logs of all AI-assisted workflows

Risk Mitigation: Clearly disclose AI involvement in engagement letters, maintain attorney oversight of all AI-generated outputs, and implement robust backup systems for critical case data.

// Ethics Compliance Checklist Implementation
const ETHICS_COMPLIANCE_CHECKLIST = {
  technology_competence: {
    staff_trained: true,
    risk_assessment_documented: true,
    annual_review_scheduled: true
  },
  confidentiality_safeguards: {
    https_only: true,
    client_consent_obtained: true,
    access_controls_implemented: true,
    audit_logging_enabled: true
  },
  supervision_protocols: {
    attorney_review_required: true,
    ai_disclosure_in_engagement: true,
    output_verification_process: true
  },
  insurance_compliance: {
    carrier_notified: true,
    policy_reviewed: true,
    cybersecurity_documented: true
  }
};

// Validate ethics compliance before app deployment
function validateEthicsCompliance(checklist) {
  const categories = Object.keys(checklist);
  for (const category of categories) {
    const items = Object.entries(checklist[category]);
    const failed = items.filter(([key, value]) => !value);

    if (failed.length > 0) {
      throw new Error(
        `Ethics compliance failure in ${category}: ${failed.map(([k]) => k).join(', ')}`
      );
    }
  }

  return { compliant: true, validated_at: new Date().toISOString() };
}

For comprehensive legal ethics guidance, consult the American Bar Association Model Rules of Professional Conduct and your state bar's ethics opinions on technology use.

Prerequisites for Clio Integration

Clio Subscription Requirements

Clio Practice Management Tier Required: Clio Manage or Clio Suite (API access not available on basic tiers)

Pricing: Starting at $49/user/month (Clio Manage Starter) to $129/user/month (Clio Suite)

API Access: Included with all paid plans, no additional API fees

Alternative Platforms: This guide focuses on Clio, but the architecture applies equally to:

  • MyCase ($39-79/user/month, API available)
  • PracticePanther ($49-89/user/month, API available)
  • Rocket Matter ($59-99/user/month, API available)

Clio API Credentials Setup

Step 1: Create Clio Developer Account

  1. Navigate to Clio App Directory Developer Portal
  2. Click "Register Your Application"
  3. Sign in with Clio admin credentials
  4. Complete developer registration form

Step 2: Register Your ChatGPT App

Application Name: [Your Firm Name] Case Management Assistant
Description: AI-powered case management for matter tracking, time entry, and billing
Redirect URIs: https://chatgpt.com/connector_platform_oauth_redirect
                https://platform.openai.com/apps-manage/oauth
Website: https://yourfirm.com

Step 3: Configure OAuth Scopes

Select minimum necessary permissions (principle of least privilege):

✅ contacts:read       (client information lookup)
✅ matters:read        (case status queries)
✅ matters:write       (update matter status)
✅ calendar:read       (court date lookups)
✅ calendar:write      (schedule events)
✅ activities:read     (time entry history)
✅ activities:write    (log new time)
✅ documents:read      (document retrieval)
✅ documents:write     (upload documents)
✅ bills:read          (invoice queries)
✅ bills:write         (generate invoices)

Step 4: Obtain API Credentials

After registration, Clio provides:

  • Client ID: abc123def456... (public identifier)
  • Client Secret: xyz789secret... (NEVER expose in client-side code)

Store these securely in environment variables:

# .env file (NEVER commit to version control)
CLIO_CLIENT_ID=abc123def456...
CLIO_CLIENT_SECRET=xyz789secret...
CLIO_REDIRECT_URI=https://chatgpt.com/connector_platform_oauth_redirect

Rate Limiting and Quota Management

Clio API Rate Limits:

  • 10 requests per second per OAuth access token
  • 5,000 requests per hour per application
  • 100,000 requests per day per application

For typical law firm usage (50-200 active matters), these limits are more than sufficient. A firm making 1,000 daily API calls uses only 1% of quota.

Rate Limit Best Practices:

// Implement exponential backoff for rate limit errors
async function clioAPICall(endpoint, options = {}) {
  const maxRetries = 3;
  let attempt = 0;

  while (attempt < maxRetries) {
    try {
      const response = await fetch(`https://app.clio.com/api/v4/${endpoint}`, {
        ...options,
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
          ...options.headers
        }
      });

      if (response.status === 429) {
        // Rate limited - wait and retry
        const retryAfter = parseInt(response.headers.get('Retry-After') || '5');
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        attempt++;
        continue;
      }

      return await response.json();
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      attempt++;
      await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
    }
  }
}

Attorney Supervision Protocols

Ethics Requirement: All AI-generated outputs must be reviewed by supervising attorney before client delivery.

Recommended Workflow:

  1. ChatGPT app generates draft (status update, time entry, document)
  2. System flags for attorney review with confidence score
  3. Attorney reviews, edits, approves before execution
  4. System logs approval for audit trail compliance
// Attorney approval workflow
const APPROVAL_REQUIRED_ACTIONS = [
  'updateMatterStatus',
  'sendClientCommunication',
  'generateInvoice',
  'uploadDocument'
];

function requiresAttorneyApproval(action, confidence) {
  return APPROVAL_REQUIRED_ACTIONS.includes(action) || confidence < 0.85;
}

async function executeWithApproval(action, data, attorneyId) {
  const approvalId = await createApprovalRequest({
    action,
    data,
    requestedBy: attorneyId,
    status: 'pending'
  });

  // Send notification to attorney dashboard
  await notifyAttorney(attorneyId, {
    message: `Approval required for ${action}`,
    approvalId,
    previewData: data
  });

  // Wait for approval (in practice, this is async callback)
  return { status: 'pending_approval', approvalId };
}

Step 1: Clio OAuth 2.1 Authentication

OAuth 2.1 with PKCE (Proof Key for Code Exchange) is the only approved authentication method for Clio API access. This ensures secure authorization without exposing client secrets.

OAuth Flow Architecture

┌─────────────┐                ┌──────────────┐                ┌─────────────┐
│   ChatGPT   │                │  MCP Server  │                │  Clio API   │
│     App     │                │  (Your App)  │                │             │
└──────┬──────┘                └──────┬───────┘                └──────┬──────┘
       │                              │                               │
       │  1. User clicks "Connect    │                               │
       │     Clio Account"           │                               │
       ├────────────────────────────>│                               │
       │                              │                               │
       │                              │  2. Generate PKCE challenge  │
       │                              │     code_verifier (random)   │
       │                              │     code_challenge (SHA256)  │
       │                              │                               │
       │                              │  3. Redirect to Clio auth    │
       │                              ├─────────────────────────────>│
       │                              │     /oauth/authorize?        │
       │                              │     client_id=...            │
       │                              │     redirect_uri=...         │
       │                              │     code_challenge=...       │
       │                              │                               │
       │  4. User authorizes app     │                               │
       │<────────────────────────────┼───────────────────────────────┤
       │                              │                               │
       │  5. Redirect with auth code │                               │
       ├────────────────────────────>│                               │
       │     ?code=abc123...         │                               │
       │                              │                               │
       │                              │  6. Exchange code for token  │
       │                              ├─────────────────────────────>│
       │                              │     POST /oauth/token        │
       │                              │     code=abc123...           │
       │                              │     code_verifier=...        │
       │                              │                               │
       │                              │  7. Return access token      │
       │                              │<─────────────────────────────┤
       │                              │     access_token: xyz...     │
       │                              │     refresh_token: refresh...│
       │                              │     expires_in: 28800        │
       │                              │                               │
       │  8. Store tokens securely   │                               │
       │     (encrypted database)    │                               │
       │<────────────────────────────┤                               │

Implementation Code

// server.js - MCP Server OAuth Handler
import crypto from 'crypto';

// OAuth configuration
const CLIO_AUTH_URL = 'https://app.clio.com/oauth/authorize';
const CLIO_TOKEN_URL = 'https://app.clio.com/oauth/token';
const CLIO_CLIENT_ID = process.env.CLIO_CLIENT_ID;
const CLIO_CLIENT_SECRET = process.env.CLIO_CLIENT_SECRET;
const REDIRECT_URI = process.env.CLIO_REDIRECT_URI;

// PKCE helper functions
function generateCodeVerifier() {
  return crypto.randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier) {
  return crypto.createHash('sha256').update(verifier).digest('base64url');
}

// Step 1: Initiate OAuth flow
export async function initiateClioAuth(userId) {
  // Generate PKCE parameters
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = generateCodeChallenge(codeVerifier);
  const state = crypto.randomBytes(16).toString('hex');

  // Store verifier and state temporarily (5 min expiry)
  await storeAuthState(userId, { codeVerifier, state }, 300);

  // Build authorization URL
  const authUrl = new URL(CLIO_AUTH_URL);
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('client_id', CLIO_CLIENT_ID);
  authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.set('state', state);
  authUrl.searchParams.set('code_challenge', codeChallenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');

  return { authUrl: authUrl.toString() };
}

// Step 2: Handle OAuth callback
export async function handleClioCallback(code, state, userId) {
  // Retrieve stored auth state
  const storedState = await getAuthState(userId);

  if (!storedState || storedState.state !== state) {
    throw new Error('Invalid state parameter - possible CSRF attack');
  }

  // Exchange authorization code for access token
  const tokenResponse = await fetch(CLIO_TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIO_CLIENT_ID,
      client_secret: CLIO_CLIENT_SECRET,
      code_verifier: storedState.codeVerifier
    })
  });

  if (!tokenResponse.ok) {
    const error = await tokenResponse.text();
    throw new Error(`Token exchange failed: ${error}`);
  }

  const tokens = await tokenResponse.json();

  // Store tokens securely (encrypted)
  await storeClioTokens(userId, {
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresAt: Date.now() + (tokens.expires_in * 1000)
  });

  // Clean up temporary auth state
  await deleteAuthState(userId);

  return { success: true, message: 'Clio account connected successfully' };
}

// Step 3: Token refresh (8-hour access token expiry)
export async function refreshClioToken(userId) {
  const tokens = await getClioTokens(userId);

  if (!tokens.refreshToken) {
    throw new Error('No refresh token available - user must re-authorize');
  }

  const refreshResponse = await fetch(CLIO_TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'refresh_token',
      refresh_token: tokens.refreshToken,
      client_id: CLIO_CLIENT_ID,
      client_secret: CLIO_CLIENT_SECRET
    })
  });

  if (!refreshResponse.ok) {
    throw new Error('Token refresh failed - user must re-authorize');
  }

  const newTokens = await refreshResponse.json();

  await storeClioTokens(userId, {
    accessToken: newTokens.access_token,
    refreshToken: newTokens.refresh_token,
    expiresAt: Date.now() + (newTokens.expires_in * 1000)
  });

  return newTokens.access_token;
}

// Helper: Get valid access token (refreshes if expired)
export async function getValidAccessToken(userId) {
  const tokens = await getClioTokens(userId);

  if (!tokens) {
    throw new Error('No Clio tokens found - user must authorize first');
  }

  // Refresh if token expires in next 5 minutes
  if (tokens.expiresAt - Date.now() < 300000) {
    return await refreshClioToken(userId);
  }

  return tokens.accessToken;
}

For complete OAuth 2.1 implementation details, see our OAuth 2.1 for ChatGPT Apps: Complete Guide.

Step 2: Build MCP Server for Legal Operations

The Model Context Protocol (MCP) server exposes legal practice management capabilities as tools that ChatGPT can invoke. Each tool represents a discrete law firm workflow: retrieving matter details, logging time, updating status, uploading documents, generating invoices, or scheduling events.

Legal MCP Server Architecture

// mcp-server.js - Legal Case Management MCP Server
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

const server = new Server(
  {
    name: 'legal-case-management-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Define legal practice management tools
const LEGAL_TOOLS = [
  {
    name: 'getMatterDetails',
    description: 'Retrieve case status, court dates, and matter information from Clio',
    inputSchema: {
      type: 'object',
      properties: {
        matterId: {
          type: 'string',
          description: 'Clio matter ID or client matter number'
        },
        clientName: {
          type: 'string',
          description: 'Client name for matter lookup (if matterId unknown)'
        }
      }
    }
  },
  {
    name: 'addTimeEntry',
    description: 'Log billable time to a matter with activity description and billing rate',
    inputSchema: {
      type: 'object',
      properties: {
        matterId: { type: 'string', description: 'Matter ID' },
        hours: { type: 'number', description: 'Billable hours (decimal format)' },
        description: { type: 'string', description: 'Work description' },
        date: { type: 'string', description: 'Work date (YYYY-MM-DD)' },
        billingRate: { type: 'number', description: 'Hourly rate (optional, defaults to attorney rate)' }
      },
      required: ['matterId', 'hours', 'description']
    }
  },
  {
    name: 'updateMatterStatus',
    description: 'Update case status (active, pending, closed) with attorney approval',
    inputSchema: {
      type: 'object',
      properties: {
        matterId: { type: 'string', description: 'Matter ID' },
        status: {
          type: 'string',
          enum: ['active', 'pending', 'closed'],
          description: 'New matter status'
        },
        statusNote: { type: 'string', description: 'Reason for status change' }
      },
      required: ['matterId', 'status']
    }
  },
  {
    name: 'uploadDocument',
    description: 'Upload document to matter with metadata and version tracking',
    inputSchema: {
      type: 'object',
      properties: {
        matterId: { type: 'string', description: 'Matter ID' },
        documentName: { type: 'string', description: 'Document filename' },
        documentUrl: { type: 'string', description: 'URL to document file' },
        documentType: {
          type: 'string',
          description: 'Document category (pleading, correspondence, discovery, etc.)'
        }
      },
      required: ['matterId', 'documentName', 'documentUrl']
    }
  },
  {
    name: 'createInvoice',
    description: 'Generate invoice for unbilled time and expenses on a matter',
    inputSchema: {
      type: 'object',
      properties: {
        matterId: { type: 'string', description: 'Matter ID' },
        billingPeriodStart: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
        billingPeriodEnd: { type: 'string', description: 'End date (YYYY-MM-DD)' },
        includeExpenses: { type: 'boolean', description: 'Include unbilled expenses' }
      },
      required: ['matterId']
    }
  },
  {
    name: 'scheduleCalendarEvent',
    description: 'Schedule court dates, depositions, client meetings with calendar integration',
    inputSchema: {
      type: 'object',
      properties: {
        matterId: { type: 'string', description: 'Matter ID' },
        eventType: {
          type: 'string',
          enum: ['court_hearing', 'deposition', 'client_meeting', 'deadline'],
          description: 'Event type'
        },
        title: { type: 'string', description: 'Event title' },
        startTime: { type: 'string', description: 'Start time (ISO 8601)' },
        endTime: { type: 'string', description: 'End time (ISO 8601)' },
        location: { type: 'string', description: 'Event location or courtroom' },
        attendees: {
          type: 'array',
          items: { type: 'string' },
          description: 'Attendee email addresses'
        }
      },
      required: ['matterId', 'eventType', 'title', 'startTime']
    }
  }
];

// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return { tools: LEGAL_TOOLS };
});

// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case 'getMatterDetails':
        return await getMatterDetails(args.matterId, args.clientName);

      case 'addTimeEntry':
        return await addTimeEntry(args);

      case 'updateMatterStatus':
        return await updateMatterStatus(args);

      case 'uploadDocument':
        return await uploadDocument(args);

      case 'createInvoice':
        return await createInvoice(args);

      case 'scheduleCalendarEvent':
        return await scheduleCalendarEvent(args);

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    return {
      content: [{
        type: 'text',
        text: `Error: ${error.message}`
      }],
      isError: true
    };
  }
});

// Start server
const transport = new StdioServerTransport();
await server.connect(transport);

Client Privilege Protection

Critical Requirement: All MCP server responses must protect attorney-client privileged information and comply with ABA Model Rule 1.6 confidentiality requirements.

// Sanitize privileged information from responses
function sanitizePrivilegedData(data, userRole) {
  // Only attorneys and authorized staff see privileged communications
  if (!['attorney', 'paralegal'].includes(userRole)) {
    delete data.privilegedNotes;
    delete data.strategyMemos;
    delete data.attorneyWorkProduct;
  }

  // Never include sensitive fields in widget metadata
  const { ssn, bankAccount, creditCard, ...safeData } = data;

  return safeData;
}

// Audit logging for compliance
async function logAPIAccess(userId, action, matterId, result) {
  await auditLog.create({
    timestamp: new Date(),
    userId,
    action,
    matterId,
    success: result.success,
    ipAddress: request.ip,
    userAgent: request.headers['user-agent']
  });
}

For comprehensive MCP server development guidance, see our MCP Server Development: Complete Guide.

Step 3: Matter Management Implementation

Matter status queries are the most common law firm workflow—clients, attorneys, and staff need real-time case information without navigating complex practice management dashboards.

Conversational Matter Lookup

User Query Examples:

  • "What's the status of my divorce case?"
  • "When is the next court date in the Martinez matter?"
  • "Show me all active family law cases"
  • "What documents are missing in the Smith estate matter?"

Implementation Code

// matter-management.js - Matter status and tracking
async function getMatterDetails(matterId, clientName) {
  const accessToken = await getValidAccessToken(request.userId);

  // If matterId not provided, search by client name
  if (!matterId && clientName) {
    const searchResults = await fetch(
      `https://app.clio.com/api/v4/matters.json?query=${encodeURIComponent(clientName)}`,
      {
        headers: { 'Authorization': `Bearer ${accessToken}` }
      }
    );

    const matters = await searchResults.json();

    if (matters.data.length === 0) {
      return {
        content: [{
          type: 'text',
          text: `No matters found for client "${clientName}". Please verify spelling or provide matter number.`
        }]
      };
    }

    // If multiple matches, ask user to clarify
    if (matters.data.length > 1) {
      return {
        content: [{
          type: 'text',
          mimeType: 'text/html+skybridge',
          text: formatMultipleMattersList(matters.data)
        }]
      };
    }

    matterId = matters.data[0].id;
  }

  // Fetch comprehensive matter data
  const [matter, calendar, tasks, documents] = await Promise.all([
    clioAPICall(`matters/${matterId}.json`),
    clioAPICall(`calendar_entries.json?matter_id=${matterId}&limit=10`),
    clioAPICall(`tasks.json?matter_id=${matterId}&status=incomplete`),
    clioAPICall(`documents.json?matter_id=${matterId}&limit=5&order=created_at(desc)`)
  ]);

  // Format response with structured data
  return {
    content: [
      {
        type: 'text',
        mimeType: 'text/html+skybridge',
        text: formatMatterStatusWidget(matter, calendar, tasks, documents)
      }
    ],
    _meta: {
      structuredContent: {
        type: 'matter_status',
        matterId: matter.data.id,
        matterNumber: matter.data.display_number,
        clientName: matter.data.client.name,
        status: matter.data.status,
        practiceArea: matter.data.practice_area,
        responsibleAttorney: matter.data.responsible_attorney.name,
        nextCourtDate: calendar.data[0]?.date || null,
        pendingTasks: tasks.data.length,
        lastActivity: matter.data.updated_at
      }
    }
  };
}

// Format matter status as inline card widget
function formatMatterStatusWidget(matter, calendar, tasks, documents) {
  const nextEvent = calendar.data[0];
  const urgentTasks = tasks.data.filter(t =>
    new Date(t.due_date) - Date.now() < 7 * 24 * 60 * 60 * 1000
  );

  return `
    <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 16px; background: #f9f9f9; border-radius: 8px; max-width: 400px;">
      <h3 style="margin: 0 0 12px 0; font-size: 18px; color: #333;">
        ${matter.data.display_number}: ${matter.data.description}
      </h3>

      <div style="margin-bottom: 12px;">
        <strong>Client:</strong> ${matter.data.client.name}<br/>
        <strong>Status:</strong> <span style="color: ${getStatusColor(matter.data.status)}; font-weight: 600;">${matter.data.status.toUpperCase()}</span><br/>
        <strong>Attorney:</strong> ${matter.data.responsible_attorney.name}
      </div>

      ${nextEvent ? `
        <div style="background: #fff3cd; padding: 12px; border-radius: 6px; margin-bottom: 12px; border-left: 4px solid #ffc107;">
          <strong style="color: #856404;">📅 Next Event</strong><br/>
          <span style="font-size: 14px;">${nextEvent.summary}</span><br/>
          <span style="font-size: 13px; color: #666;">${formatDate(nextEvent.start_at)} at ${formatTime(nextEvent.start_at)}</span>
        </div>
      ` : ''}

      ${urgentTasks.length > 0 ? `
        <div style="background: #f8d7da; padding: 12px; border-radius: 6px; margin-bottom: 12px; border-left: 4px solid #dc3545;">
          <strong style="color: #721c24;">⚠️ Urgent Tasks (${urgentTasks.length})</strong>
          ${urgentTasks.slice(0, 3).map(task => `
            <div style="font-size: 13px; margin-top: 6px;">
              • ${task.name} <span style="color: #666;">(Due ${formatDate(task.due_date)})</span>
            </div>
          `).join('')}
        </div>
      ` : ''}

      <div style="font-size: 13px; color: #666; margin-top: 12px;">
        <strong>Recent Documents:</strong><br/>
        ${documents.data.slice(0, 3).map(doc => `
          <div style="margin-top: 4px;">• ${doc.name}</div>
        `).join('')}
      </div>

      <div style="margin-top: 16px; display: flex; gap: 8px;">
        <button onclick="window.openai.triggerTool('viewFullCalendar', {matterId: '${matter.data.id}'})"
                style="flex: 1; padding: 8px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 14px; cursor: pointer;">
          View Calendar
        </button>
        <button onclick="window.openai.triggerTool('viewDocuments', {matterId: '${matter.data.id}'})"
                style="flex: 1; padding: 8px; background: #28a745; color: white; border: none; border-radius: 4px; font-size: 14px; cursor: pointer;">
          Documents
        </button>
      </div>
    </div>
  `;
}

function getStatusColor(status) {
  const colors = {
    'active': '#28a745',
    'pending': '#ffc107',
    'closed': '#6c757d'
  };
  return colors[status] || '#333';
}

function formatDate(isoDate) {
  return new Date(isoDate).toLocaleDateString('en-US', {
    month: 'short',
    day: 'numeric',
    year: 'numeric'
  });
}

function formatTime(isoDate) {
  return new Date(isoDate).toLocaleTimeString('en-US', {
    hour: 'numeric',
    minute: '2-digit',
    hour12: true
  });
}

Widget Design Notes:

  • Max 2 CTAs per card (OpenAI requirement)
  • System fonts only (no custom fonts allowed)
  • Inline display mode for quick status checks
  • Accessible colors with WCAG AA contrast ratios

For widget design best practices, see our ChatGPT Widget Development: Complete Guide.

Step 4: Automated Time Tracking

Manual time entry is the #1 source of revenue leakage in law firms. Attorneys bill by the hour but capture only 70-85% of actual time worked due to:

  • Delayed entry: Logging time weeks after work is performed
  • Memory decay: Forgetting exact hours or rounding down
  • Entry friction: Tedious timekeeping interfaces discourage real-time logging

Voice-Activated Time Entry

ChatGPT apps solve this with natural language time logging:

Attorney dictates: "Log 2.3 hours for reviewing discovery responses in Martinez v. Johnson matter, performed today"

AI interprets:

  • Hours: 2.3
  • Activity: "Reviewing discovery responses"
  • Matter: "Martinez v. Johnson" (maps to Clio matter ID)
  • Date: Today's date
  • Attorney: Authenticated user

Implementation

// time-tracking.js - Automated time entry with AI assistance
async function addTimeEntry(args) {
  const { matterId, hours, description, date, billingRate } = args;
  const accessToken = await getValidAccessToken(request.userId);

  // Validate matter exists and user has access
  const matter = await clioAPICall(`matters/${matterId}.json`, accessToken);
  if (!matter.data) {
    throw new Error(`Matter ${matterId} not found or access denied`);
  }

  // Get attorney billing rate from user profile
  const user = await clioAPICall(`users/who_am_i.json`, accessToken);
  const rate = billingRate || user.data.rate;

  // Create time entry in Clio
  const timeEntry = await fetch('https://app.clio.com/api/v4/activities.json', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      data: {
        type: 'TimeEntry',
        matter: { id: matterId },
        user: { id: user.data.id },
        date: date || new Date().toISOString().split('T')[0],
        quantity: hours,
        price: rate,
        note: description
      }
    })
  });

  if (!timeEntry.ok) {
    const error = await timeEntry.json();
    throw new Error(`Failed to create time entry: ${error.error}`);
  }

  const result = await timeEntry.json();

  // Calculate billable amount
  const billableAmount = hours * rate;

  return {
    content: [{
      type: 'text',
      text: `✅ Time logged successfully\n\n` +
            `Matter: ${matter.data.display_number} - ${matter.data.description}\n` +
            `Hours: ${hours}\n` +
            `Rate: $${rate}/hr\n` +
            `Amount: $${billableAmount.toFixed(2)}\n` +
            `Description: ${description}\n\n` +
            `Entry ID: ${result.data.id}`
    }],
    _meta: {
      structuredContent: {
        type: 'time_entry_confirmation',
        entryId: result.data.id,
        hours,
        rate,
        billableAmount,
        matterId,
        date: result.data.date
      }
    }
  };
}

// Activity-based time entry suggestions
async function suggestTimeEntry(activityDescription) {
  // AI analyzes common billing descriptions and suggests hours
  const suggestions = {
    'client call': { hours: 0.3, category: 'C - Client Communication' },
    'court hearing': { hours: 2.0, category: 'L - Litigation' },
    'draft motion': { hours: 3.5, category: 'L - Litigation' },
    'review discovery': { hours: 2.0, category: 'D - Discovery' },
    'research': { hours: 1.5, category: 'R - Research' },
    'email correspondence': { hours: 0.2, category: 'C - Client Communication' }
  };

  // Fuzzy match activity to suggestions
  const match = Object.keys(suggestions).find(key =>
    activityDescription.toLowerCase().includes(key)
  );

  return match ? suggestions[match] : { hours: 1.0, category: 'G - General' };
}

// Batch time entry for repetitive tasks
async function batchTimeEntry(matterId, entries) {
  const accessToken = await getValidAccessToken(request.userId);
  const user = await clioAPICall(`users/who_am_i.json`, accessToken);

  // Create all entries in parallel (Clio allows batch operations)
  const results = await Promise.all(
    entries.map(entry =>
      fetch('https://app.clio.com/api/v4/activities.json', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          data: {
            type: 'TimeEntry',
            matter: { id: matterId },
            user: { id: user.data.id },
            date: entry.date,
            quantity: entry.hours,
            price: entry.rate || user.data.rate,
            note: entry.description
          }
        })
      })
    )
  );

  const totalHours = entries.reduce((sum, e) => sum + e.hours, 0);
  const totalBillable = entries.reduce((sum, e) => sum + (e.hours * (e.rate || user.data.rate)), 0);

  return {
    content: [{
      type: 'text',
      text: `✅ Batch time entry complete\n\n` +
            `Entries created: ${entries.length}\n` +
            `Total hours: ${totalHours}\n` +
            `Total billable: $${totalBillable.toFixed(2)}`
    }]
  };
}

Time Tracking Best Practices:

  • Real-time entry: Encourage immediate logging via mobile ChatGPT
  • Activity templates: Pre-define common tasks with standard hours
  • Approval workflows: Require partner review for entries over 5 hours
  • Audit trails: Log all time entry edits with timestamp and user ID

Step 5: Document Automation and E-Signature

Legal document generation via ChatGPT apps accelerates production of engagement letters, retainer agreements, simple contracts, and standard pleadings from hours to minutes.

Template-Based Document Generation

// document-automation.js - Generate legal documents from templates
async function generateLegalDocument(templateId, variables, matterId) {
  const accessToken = await getValidAccessToken(request.userId);

  // Fetch template from document management system
  const template = await getDocumentTemplate(templateId);

  // Substitute variables (client name, matter number, dates, etc.)
  const generatedDocument = template.content
    .replace(/{{client_name}}/g, variables.clientName)
    .replace(/{{matter_number}}/g, variables.matterNumber)
    .replace(/{{attorney_name}}/g, variables.attorneyName)
    .replace(/{{engagement_date}}/g, variables.engagementDate)
    .replace(/{{billing_rate}}/g, variables.billingRate);

  // Upload to Clio document repository
  const documentBlob = new Blob([generatedDocument], { type: 'application/pdf' });
  const formData = new FormData();
  formData.append('parent_id', matterId);
  formData.append('parent_type', 'Matter');
  formData.append('name', `${template.name} - ${variables.clientName}.pdf`);
  formData.append('document', documentBlob);

  const uploadResponse = await fetch('https://app.clio.com/api/v4/documents.json', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${accessToken}` },
    body: formData
  });

  const uploadResult = await uploadResponse.json();

  // Initiate DocuSign workflow if e-signature required
  if (template.requiresSignature) {
    const envelopeId = await initiateDocuSignWorkflow({
      documentId: uploadResult.data.id,
      signers: variables.signers,
      subject: `Please sign: ${template.name}`,
      message: `Your attorney has prepared a ${template.name} for your review and signature.`
    });

    return {
      content: [{
        type: 'text',
        text: `✅ Document generated and sent for signature\n\n` +
              `Template: ${template.name}\n` +
              `Recipients: ${variables.signers.map(s => s.email).join(', ')}\n` +
              `DocuSign Envelope: ${envelopeId}\n\n` +
              `Track signature status in your DocuSign account.`
      }]
    };
  }

  return {
    content: [{
      type: 'text',
      text: `✅ Document generated successfully\n\n` +
            `Template: ${template.name}\n` +
            `Saved to: ${matter.data.display_number}/Documents\n` +
            `Document ID: ${uploadResult.data.id}\n\n` +
            `View in Clio dashboard.`
    }],
    _meta: {
      structuredContent: {
        type: 'document_generated',
        documentId: uploadResult.data.id,
        documentName: uploadResult.data.name,
        matterId
      }
    }
  };
}

// Clause library integration
async function insertClauseFromLibrary(documentId, clauseId, position) {
  const clause = await getClauseLibraryItem(clauseId);

  // Insert clause at specified position (beginning, end, or after section)
  const document = await getDocument(documentId);
  const updatedContent = insertClauseAtPosition(document.content, clause.text, position);

  // Update document in repository
  await updateDocument(documentId, updatedContent);

  return {
    content: [{
      type: 'text',
      text: `✅ Clause "${clause.name}" inserted successfully`
    }]
  };
}

// DocuSign e-signature integration
async function initiateDocuSignWorkflow({ documentId, signers, subject, message }) {
  const DOCUSIGN_API_BASE = 'https://demo.docusign.net/restapi/v2.1';
  const accountId = process.env.DOCUSIGN_ACCOUNT_ID;
  const accessToken = await getDocuSignAccessToken();

  // Download document from Clio
  const document = await downloadClioDocument(documentId);

  // Create DocuSign envelope
  const envelopeDefinition = {
    emailSubject: subject,
    documents: [{
      documentBase64: document.base64,
      name: document.name,
      fileExtension: 'pdf',
      documentId: '1'
    }],
    recipients: {
      signers: signers.map((signer, index) => ({
        email: signer.email,
        name: signer.name,
        recipientId: String(index + 1),
        routingOrder: String(index + 1),
        tabs: {
          signHereTabs: [{
            documentId: '1',
            pageNumber: '1',
            xPosition: '100',
            yPosition: '100'
          }]
        }
      }))
    },
    status: 'sent'
  };

  const envelopeResponse = await fetch(
    `${DOCUSIGN_API_BASE}/accounts/${accountId}/envelopes`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(envelopeDefinition)
    }
  );

  const envelope = await envelopeResponse.json();
  return envelope.envelopeId;
}

For comprehensive document automation implementation, see our Legal Document Automation via ChatGPT: Ethics Guide.

Step 6: Client Billing Automation

Invoice generation is one of the most time-consuming law firm workflows—aggregating unbilled time entries, applying billing rates, formatting invoices, and delivering to clients.

Automated Invoice Generation

// billing-automation.js - Generate and deliver client invoices
async function createInvoice(args) {
  const { matterId, billingPeriodStart, billingPeriodEnd, includeExpenses } = args;
  const accessToken = await getValidAccessToken(request.userId);

  // Fetch unbilled time entries for billing period
  const timeEntries = await clioAPICall(
    `activities.json?matter_id=${matterId}&billed=false&date_performed_start=${billingPeriodStart}&date_performed_end=${billingPeriodEnd}`,
    accessToken
  );

  // Fetch unbilled expenses if requested
  let expenses = { data: [] };
  if (includeExpenses) {
    expenses = await clioAPICall(
      `expenses.json?matter_id=${matterId}&billed=false&date_start=${billingPeriodStart}&date_end=${billingPeriodEnd}`,
      accessToken
    );
  }

  // Calculate totals
  const timeTotal = timeEntries.data.reduce((sum, entry) =>
    sum + (entry.quantity * entry.price), 0
  );
  const expenseTotal = expenses.data.reduce((sum, exp) =>
    sum + exp.total, 0
  );
  const subtotal = timeTotal + expenseTotal;
  const tax = 0; // Configure based on jurisdiction
  const total = subtotal + tax;

  // Create Clio bill
  const bill = await fetch('https://app.clio.com/api/v4/bills.json', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      data: {
        matter: { id: matterId },
        date: billingPeriodEnd,
        due_date: calculateDueDate(billingPeriodEnd, 30), // 30-day terms
        state: 'draft' // Requires attorney approval before sending
      }
    })
  });

  const billResult = await bill.json();

  // Attach time entries and expenses to bill
  await Promise.all([
    ...timeEntries.data.map(entry =>
      clioAPICall(`activities/${entry.id}.json`, accessToken, {
        method: 'PATCH',
        body: JSON.stringify({ data: { bill: { id: billResult.data.id } } })
      })
    ),
    ...expenses.data.map(expense =>
      clioAPICall(`expenses/${expense.id}.json`, accessToken, {
        method: 'PATCH',
        body: JSON.stringify({ data: { bill: { id: billResult.data.id } } })
      })
    )
  ]);

  return {
    content: [{
      type: 'text',
      text: `✅ Invoice created (DRAFT - requires attorney approval)\n\n` +
            `Bill ID: ${billResult.data.id}\n` +
            `Matter: ${matterId}\n` +
            `Billing Period: ${billingPeriodStart} to ${billingPeriodEnd}\n\n` +
            `Time Charges: $${timeTotal.toFixed(2)} (${timeEntries.data.length} entries)\n` +
            `Expenses: $${expenseTotal.toFixed(2)} (${expenses.data.length} items)\n` +
            `Subtotal: $${subtotal.toFixed(2)}\n` +
            `Total: $${total.toFixed(2)}\n\n` +
            `⚠️ Review and approve in Clio before sending to client.`
    }],
    _meta: {
      structuredContent: {
        type: 'invoice_draft',
        billId: billResult.data.id,
        total,
        timeEntries: timeEntries.data.length,
        expenses: expenses.data.length,
        requiresApproval: true
      }
    }
  };
}

function calculateDueDate(billingDate, termsDays) {
  const due = new Date(billingDate);
  due.setDate(due.getDate() + termsDays);
  return due.toISOString().split('T')[0];
}

// Payment reminder automation
async function sendPaymentReminder(billId, daysOverdue) {
  const accessToken = await getValidAccessToken(request.userId);
  const bill = await clioAPICall(`bills/${billId}.json`, accessToken);

  const reminderTemplate = daysOverdue <= 7
    ? 'polite_reminder'
    : daysOverdue <= 30
      ? 'firm_reminder'
      : 'final_notice';

  const reminderEmail = await generateReminderEmail(bill.data, reminderTemplate);

  // Send via Clio communication system
  await clioAPICall(`communications.json`, accessToken, {
    method: 'POST',
    body: JSON.stringify({
      data: {
        type: 'Email',
        subject: reminderEmail.subject,
        body: reminderEmail.body,
        receivers: [{ id: bill.data.matter.client.id }]
      }
    })
  });

  return {
    content: [{
      type: 'text',
      text: `✅ Payment reminder sent\n\n` +
            `Bill: ${bill.data.id}\n` +
            `Client: ${bill.data.matter.client.name}\n` +
            `Amount Due: $${bill.data.total}\n` +
            `Days Overdue: ${daysOverdue}\n` +
            `Template: ${reminderTemplate}`
    }]
  };
}

// Trust accounting compliance check
async function validateTrustAccountingCompliance(matterId) {
  const accessToken = await getValidAccessToken(request.userId);

  // Fetch trust transactions for matter
  const trustTransactions = await clioAPICall(
    `trust_transactions.json?matter_id=${matterId}`,
    accessToken
  );

  // Calculate balances
  const deposits = trustTransactions.data
    .filter(t => t.type === 'deposit')
    .reduce((sum, t) => sum + t.amount, 0);

  const withdrawals = trustTransactions.data
    .filter(t => t.type === 'withdrawal')
    .reduce((sum, t) => sum + t.amount, 0);

  const balance = deposits - withdrawals;

  // Compliance checks
  const issues = [];

  if (balance < 0) {
    issues.push('❌ CRITICAL: Negative trust balance (violates bar rules)');
  }

  if (withdrawals > deposits) {
    issues.push('⚠️ WARNING: Withdrawals exceed deposits');
  }

  const unbilledTime = await getTotalUnbilledTime(matterId);
  if (balance < unbilledTime * 0.5) {
    issues.push('⚠️ NOTICE: Trust balance may be insufficient for unbilled work');
  }

  return {
    balance,
    deposits,
    withdrawals,
    compliant: issues.length === 0,
    issues
  };
}

Trust Accounting Best Practices:

  • Daily reconciliation: Automated trust balance monitoring
  • Alert thresholds: Notify attorneys when balance falls below unbilled work
  • Audit trails: Complete transaction history with timestamps
  • Compliance reports: Monthly IOLTA compliance summaries

Advanced Features

Legal Research Integration

Connect ChatGPT apps to Westlaw or LexisNexis for AI-powered case law research:

// legal-research.js - Westlaw Edge API integration
async function searchCaseLaw(query, jurisdiction, dateRange) {
  const WESTLAW_API = 'https://api.westlaw.com/v2/search';
  const accessToken = await getWestlawAccessToken();

  const searchResponse = await fetch(WESTLAW_API, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      query,
      jurisdiction,
      date_range: dateRange,
      max_results: 10,
      sort: 'relevance'
    })
  });

  const results = await searchResponse.json();

  return {
    content: [{
      type: 'text',
      text: formatCaseLawResults(results.cases)
    }]
  };
}

Conflict Checking Automation

Prevent ethical violations with automated conflict-of-interest detection:

// conflict-checking.js - Automated conflict detection
async function checkConflicts(newClientName, opposingParties) {
  const existingClients = await getAllClients();
  const existingMatters = await getAllMatters();

  const conflicts = [];

  // Check if opposing party is existing client
  for (const party of opposingParties) {
    const match = existingClients.find(c =>
      c.name.toLowerCase() === party.toLowerCase()
    );

    if (match) {
      conflicts.push({
        type: 'direct_conflict',
        severity: 'CRITICAL',
        description: `Opposing party "${party}" is existing client (Matter ${match.matters[0].id})`
      });
    }
  }

  // Check for related entities
  const relatedConflicts = await checkRelatedEntities(newClientName, opposingParties);
  conflicts.push(...relatedConflicts);

  return {
    hasConflicts: conflicts.length > 0,
    conflicts,
    recommendation: conflicts.length > 0
      ? 'Obtain conflict waiver before engagement'
      : 'No conflicts detected - safe to proceed'
  };
}

Court Filing Integration (E-Filing)

Submit documents directly to courts via e-filing integrations:

// e-filing.js - Court e-filing integration (File & Serve Xpress)
async function fileCourtDocument(documentId, courtId, caseNumber, filingType) {
  const EFILING_API = 'https://api.fileandservexpress.com/v1';
  const accessToken = await getEFilingAccessToken();

  const document = await downloadClioDocument(documentId);

  const filingResponse = await fetch(`${EFILING_API}/filings`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      court_id: courtId,
      case_number: caseNumber,
      filing_type: filingType,
      document: {
        name: document.name,
        content: document.base64,
        mime_type: 'application/pdf'
      }
    })
  });

  const filing = await filingResponse.json();

  return {
    content: [{
      type: 'text',
      text: `✅ Document filed electronically\n\n` +
            `Filing ID: ${filing.id}\n` +
            `Court: ${filing.court_name}\n` +
            `Case: ${caseNumber}\n` +
            `Status: ${filing.status}\n\n` +
            `Confirmation receipt sent to email.`
    }]
  };
}

Client Portal Access

Provide clients with secure, self-service access to case information:

// client-portal.js - Generate secure client portal link
async function generateClientPortalLink(clientId, matterId) {
  const accessToken = await getValidAccessToken(request.userId);

  // Create time-limited portal access token
  const portalToken = jwt.sign(
    { clientId, matterId, type: 'client_portal' },
    process.env.PORTAL_SECRET,
    { expiresIn: '7d' }
  );

  const portalUrl = `https://portal.yourfirm.com/client/${portalToken}`;

  // Send invitation email
  await sendPortalInvitation(clientId, portalUrl);

  return {
    content: [{
      type: 'text',
      text: `✅ Client portal access created\n\n` +
            `Portal URL: ${portalUrl}\n` +
            `Expires: ${new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toLocaleDateString()}\n\n` +
            `Invitation email sent to client.`
    }]
  };
}

Widget Design Best Practices

Inline Card: Matter Status with Deadlines

Use Case: Quick case status check with upcoming deadlines

Design Requirements:

  • Max 2 primary actions (OpenAI requirement)
  • System fonts only (SF Pro on iOS, Roboto on Android)
  • No nested scrolling in inline widgets
  • WCAG AA contrast ratios for accessibility
<!-- Matter status inline card -->
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 400px; padding: 16px; background: #ffffff; border: 1px solid #e0e0e0; border-radius: 8px;">
  <h3 style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600; color: #1a1a1a;">
    Case #2024-CV-1234: Smith Divorce
  </h3>

  <div style="margin-bottom: 16px; font-size: 14px; color: #333;">
    <div style="margin-bottom: 8px;">
      <strong>Status:</strong> <span style="color: #28a745; font-weight: 600;">Active</span>
    </div>
    <div style="margin-bottom: 8px;">
      <strong>Next Event:</strong> Mediation Session
    </div>
    <div style="color: #666;">
      📅 January 15, 2026 at 10:00 AM
    </div>
  </div>

  <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 12px; margin-bottom: 16px; border-radius: 4px;">
    <strong style="color: #856404; font-size: 14px;">⏰ Deadline Alert</strong>
    <div style="font-size: 13px; color: #856404; margin-top: 4px;">
      Discovery responses due in 3 days
    </div>
  </div>

  <div style="display: flex; gap: 8px;">
    <button onclick="window.openai.triggerTool('viewCalendar', {matterId: '12345'})"
            style="flex: 1; padding: 10px; background: #007bff; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer;">
      View Calendar
    </button>
    <button onclick="window.openai.triggerTool('requestUpdate', {matterId: '12345'})"
            style="flex: 1; padding: 10px; background: #6c757d; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer;">
      Request Update
    </button>
  </div>
</div>

Fullscreen: Calendar with Court Dates

Use Case: Monthly calendar view with all court dates, deadlines, and appointments

// fullscreen-calendar.js - Court calendar in fullscreen mode
window.openai.displayMode('fullscreen');

// Render interactive calendar
const calendarHTML = `
  <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 24px; background: #f9f9f9; min-height: 100vh;">
    <h2 style="margin: 0 0 24px 0; font-size: 24px; font-weight: 700; color: #1a1a1a;">
      Court Calendar - January 2026
    </h2>

    <div style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px;">
      ${generateCalendarDays(courtEvents)}
    </div>

    <div style="margin-top: 24px;">
      <h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">Upcoming Events</h3>
      ${courtEvents.map(event => renderEventCard(event)).join('')}
    </div>
  </div>
`;

document.body.innerHTML = calendarHTML;

Carousel: Recent Documents

Use Case: Browse latest case documents with metadata

<!-- Document carousel (3-8 items) -->
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 16px;">
  <h3 style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600;">Recent Documents</h3>

  <div style="display: flex; gap: 12px; overflow-x: auto; padding-bottom: 8px;">
    <div style="min-width: 200px; padding: 16px; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px;">
      <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px; color: #1a1a1a;">
        Motion to Compel Discovery
      </div>
      <div style="font-size: 12px; color: #666; margin-bottom: 12px;">
        Filed: Jan 10, 2026<br/>
        Type: Pleading
      </div>
      <button onclick="window.openai.triggerTool('viewDocument', {id: 'doc123'})"
              style="width: 100%; padding: 8px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 13px; cursor: pointer;">
        View
      </button>
    </div>

    <!-- Repeat for other documents (3-8 total) -->
  </div>
</div>

For comprehensive widget design guidance, see our ChatGPT Widget Development: Complete Guide.

Testing and Validation

Test with Clio Sandbox Account

Setup:

  1. Create free Clio Sandbox at developers.clio.com/sandbox
  2. Populate with test matters, clients, time entries
  3. Configure OAuth credentials for sandbox API endpoint
  4. Test all workflows without affecting production data

Attorney Review Workflows

Approval Testing:

  • Generate test invoices and require attorney approval
  • Create sample time entries exceeding 5 hours (trigger review)
  • Upload test documents with privilege classifications
  • Simulate conflict-of-interest scenarios

Client Acceptance Testing

User Testing Protocol:

  1. Solo practitioner feedback: Test with 1-2 attorney firm (30 minutes)
  2. Mid-size firm testing: 10-attorney firm pilot (2 weeks)
  3. Paralegal usability: Non-attorney staff workflows (1 week)
  4. Client perspective: End-user self-service testing (3-5 clients)

Success Metrics:

  • Time entry accuracy: >95%
  • Invoice generation time: <5 minutes per matter
  • Client status inquiry resolution: <30 seconds
  • Attorney satisfaction score: >4.5/5.0

Troubleshooting Common Issues

OAuth Token Expiration

Symptom: API calls fail with 401 Unauthorized error

Cause: Access tokens expire after 8 hours

Solution:

// Automatic token refresh middleware
async function ensureValidToken(userId) {
  const tokens = await getClioTokens(userId);

  if (tokens.expiresAt - Date.now() < 300000) { // Refresh if <5 min left
    return await refreshClioToken(userId);
  }

  return tokens.accessToken;
}

Time Rounding Errors

Symptom: Billable hours don't match actual work time

Cause: Manual rounding during voice entry

Solution:

  • Store exact decimal hours (e.g., 2.35 hours)
  • Apply firm-specific rounding rules during invoice generation
  • Provide time entry summary before final submission

Billing Calculation Mistakes

Symptom: Invoice totals don't match expected amounts

Cause: Mixing hourly rates, flat fees, or missing expense items

Solution:

// Comprehensive billing validation
function validateInvoiceCalculation(bill) {
  const calculatedTotal = bill.timeEntries.reduce((sum, entry) =>
    sum + (entry.hours * entry.rate), 0
  ) + bill.expenses.reduce((sum, exp) => sum + exp.amount, 0);

  if (Math.abs(calculatedTotal - bill.total) > 0.01) {
    throw new Error(
      `Invoice calculation mismatch: Expected $${calculatedTotal.toFixed(2)}, got $${bill.total.toFixed(2)}`
    );
  }
}

Trust Accounting Compliance

Symptom: Trust balance warnings or negative balances

Cause: Insufficient client retainer for unbilled work

Solution:

  • Implement proactive trust balance monitoring
  • Alert attorneys when balance falls below 50% of unbilled time
  • Automated retainer replenishment requests to clients

Conclusion: Transform Your Legal Practice

Legal case management ChatGPT apps integrated with Clio eliminate the operational friction that costs law firms thousands of billable hours annually. By automating status inquiries, time tracking, document generation, and billing workflows, firms recover 200-300 hours monthly—equivalent to 1-2 full-time employees.

Implementation ROI:

  • Solo practitioners: $144,000 annual value (10 hours weekly × $300/hour)
  • Mid-size firms: $600,000 annual value (250 hours monthly × $200/hour)
  • Payback period: 11-60 days depending on firm size

Next Steps:

  1. Set up Clio developer account and obtain API credentials
  2. Clone the legal MCP server from this guide's code examples
  3. Test with sandbox data before production deployment
  4. Pilot with 1-2 attorneys for 2 weeks
  5. Roll out firm-wide after validation

Ready to build your law firm ChatGPT app? Start building with MakeAIHQ's no-code platform →

Related Resources

External Resources


Built with MakeAIHQ - The no-code platform for ChatGPT App Store development trusted by 150,000+ law firms worldwide.