...
...
#veterans #government apis #mobile apps #oauth #civic tech

Building Veteran Benefits Tracking Apps: Real-Time

Build veteran benefits tracking apps with Flutter. Firebase integration, secure document management, and VA benefits tracking features.

V
VooStack Team
October 2, 2025
16 min read

Building Apps for Veterans: Real-Time VA Benefits Claim Tracking

Veterans deserve better digital tools. The VA has made significant progress with VA.gov, but navigating benefits claims still requires logging into a website, clicking through multiple pages, and deciphering complex status updates. For veterans dealing with disabilities, PTSD, or just the daily grind of civilian life, this friction matters.

We built VetStack to solve this: a mobile-first app that gives veterans real-time access to their VA benefits claims in a format that actually makes sense. Built by veterans, for veterans.

Along the way, we learned a lot about working with government APIs, handling sensitive veteran data, and building for a community that deserves our absolute best. Let me share those lessons.

The Problem: Fragmented Benefits Information

The VA processes millions of disability claims annually. Veterans need to check claim status, understand what stage they’re in, review decision letters, and track appeals. Currently, this means:

  1. Logging into VA.gov
  2. Navigating to the claims section
  3. Clicking through to individual claims
  4. Deciphering status codes and timelines
  5. Checking back regularly for updates

On mobile? Forget it. The responsive design works, but the experience is clearly desktop-first.

What Veterans Actually Need

After talking with hundreds of veterans, the requirements became clear:

Instant Access:

  • See all claims at a glance
  • No multi-step navigation
  • Mobile-first design (most vets use their phones)

Clear Status Updates:

  • Plain English explanations
  • Expected timelines
  • Next steps clearly outlined

Notifications:

  • Push alerts when claim status changes
  • Reminders for required actions
  • Updates on decision letters

Security:

  • VA-level security standards
  • No storing of sensitive data
  • OAuth authentication (no passwords)

Offline Access:

  • View cached claim data without internet
  • Access decision letters offline

Architecture: Government API Integration

Building apps that integrate with government systems requires a different approach than typical SaaS applications.

VA API Authentication (OAuth 2.0)

The VA uses OAuth 2.0 with the authorization code flow. No passwords are stored—users authenticate directly with VA.gov:

import { AuthSession } from 'expo-auth-session';

const VA_OAUTH_CONFIG = {
  clientId: process.env.VA_CLIENT_ID,
  redirectUri: AuthSession.makeRedirectUri({ useProxy: true }),
  scopes: ['profile', 'claims.read', 'appeals.read'],
  authorizationEndpoint: 'https://api.va.gov/oauth2/authorization',
  tokenEndpoint: 'https://api.va.gov/oauth2/token',
  revocationEndpoint: 'https://api.va.gov/oauth2/revoke',
};

async function initiateVALogin() {
  try {
    const authRequest = new AuthSession.AuthRequest({
      clientId: VA_OAUTH_CONFIG.clientId,
      redirectUri: VA_OAUTH_CONFIG.redirectUri,
      scopes: VA_OAUTH_CONFIG.scopes,
      usePKCE: true, // Required for security
    });

    await authRequest.makeAuthUrlAsync(VA_OAUTH_CONFIG);

    const result = await authRequest.promptAsync({
      authorizationEndpoint: VA_OAUTH_CONFIG.authorizationEndpoint,
    });

    if (result.type === 'success') {
      const tokens = await exchangeCodeForToken(result.params.code);
      await storeTokensSecurely(tokens);
      return tokens;
    }
  } catch (error) {
    logger.error('VA OAuth failed', { error });
    throw new Error('Failed to authenticate with VA.gov');
  }
}

async function exchangeCodeForToken(code: string) {
  const response = await fetch(VA_OAUTH_CONFIG.tokenEndpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      client_id: VA_OAUTH_CONFIG.clientId,
      redirect_uri: VA_OAUTH_CONFIG.redirectUri,
      code_verifier: authRequest.codeVerifier, // PKCE
    }),
  });

  if (!response.ok) {
    throw new Error('Token exchange failed');
  }

  return await response.json();
}

Why OAuth Matters:

  • Veterans never give us their VA password
  • We only get access to what they explicitly authorize
  • Tokens expire (security by default)
  • VA can revoke access if needed

Secure Token Storage

Government data requires extra security measures:

import * as SecureStore from 'expo-secure-store';

async function storeTokensSecurely(tokens: OAuthTokens) {
  // iOS: Keychain, Android: EncryptedSharedPreferences
  await SecureStore.setItemAsync('va_access_token', tokens.access_token, {
    keychainAccessible: SecureStore.WHEN_UNLOCKED,
  });

  await SecureStore.setItemAsync('va_refresh_token', tokens.refresh_token, {
    keychainAccessible: SecureStore.WHEN_UNLOCKED,
  });

  await SecureStore.setItemAsync(
    'va_token_expiry',
    (Date.now() + tokens.expires_in * 1000).toString(),
    {
      keychainAccessible: SecureStore.WHEN_UNLOCKED,
    }
  );
}

async function getValidToken() {
  const accessToken = await SecureStore.getItemAsync('va_access_token');
  const expiryTime = await SecureStore.getItemAsync('va_token_expiry');

  // Check if token is still valid
  if (accessToken && expiryTime) {
    const expiresAt = parseInt(expiryTime);
    const now = Date.now();

    // Refresh if expiring in less than 5 minutes
    if (expiresAt - now < 5 * 60 * 1000) {
      return await refreshAccessToken();
    }

    return accessToken;
  }

  // No valid token, need to re-authenticate
  throw new Error('Authentication required');
}

async function refreshAccessToken() {
  const refreshToken = await SecureStore.getItemAsync('va_refresh_token');

  const response = await fetch(VA_OAUTH_CONFIG.tokenEndpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: VA_OAUTH_CONFIG.clientId,
    }),
  });

  const tokens = await response.json();
  await storeTokensSecurely(tokens);
  return tokens.access_token;
}

VA Benefits Claims API

The VA provides a comprehensive API for accessing claims data:

interface VAClaim {
  id: string;
  type: 'compensation' | 'pension' | 'education' | 'burial';
  status: ClaimStatus;
  dateFiled: string;
  claimDate: string;
  phaseChangeDate: string;
  closeDate?: string;

  // Detailed status tracking
  phase: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
  developmentLetterSent: boolean;
  decisionLetterSent: boolean;
  documentsNeeded: boolean;

  // Tracking info
  claimPhaseDates: {
    phaseChangeDate: string;
    phase: number;
  }[];

  // Evidence
  supportingDocuments: SupportingDocument[];
  evidentiarySupportPending: boolean;

  // Content
  contentions: Contention[];
}

class VACLaimsService {
  private baseUrl = 'https://api.va.gov/services/claims/v1';

  async getClaims(userId: string): Promise<VAClaim[]> {
    const token = await getValidToken();

    const response = await fetch(`${this.baseUrl}/claims`, {
      headers: {
        Authorization: `Bearer ${token}`,
        'X-Key-Inflection': 'camel', // Get camelCase response
      },
    });

    if (!response.ok) {
      throw new Error('Failed to fetch claims');
    }

    const data = await response.json();
    return data.data;
  }

  async getClaimDetails(claimId: string): Promise<VAClaim> {
    const token = await getValidToken();

    const response = await fetch(`${this.baseUrl}/claims/${claimId}`, {
      headers: {
        Authorization: `Bearer ${token}`,
        'X-Key-Inflection': 'camel',
      },
    });

    if (!response.ok) {
      throw new Error('Failed to fetch claim details');
    }

    const data = await response.json();
    return data.data;
  }

  async uploadSupportingDocument(
    claimId: string,
    document: DocumentUpload
  ): Promise<void> {
    const token = await getValidToken();

    const formData = new FormData();
    formData.append('file', document.file);
    formData.append('documentType', document.type);

    const response = await fetch(
      `${this.baseUrl}/claims/${claimId}/documents`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${token}`,
        },
        body: formData,
      }
    );

    if (!response.ok) {
      throw new Error('Failed to upload document');
    }
  }
}

Translating VA Status Codes to Plain English

VA claim phases (1-8) don’t mean much to most veterans:

function getClaimStatusInfo(claim: VAClaim) {
  const statusMap = {
    1: {
      title: 'Claim Received',
      description: 'Your claim has been received and is awaiting review.',
      nextSteps: 'The VA will review your claim and may request additional evidence.',
      estimatedTime: '1-2 weeks',
    },
    2: {
      title: 'Initial Review',
      description: 'Your claim is under initial review to determine what evidence is needed.',
      nextSteps: 'You may receive requests for additional medical evidence or exams.',
      estimatedTime: '2-4 weeks',
    },
    3: {
      title: 'Evidence Gathering',
      description: 'The VA is collecting evidence to support your claim.',
      nextSteps: claim.documentsNeeded
        ? 'Upload any requested documents to speed up your claim.'
        : 'Wait for the VA to complete gathering evidence.',
      estimatedTime: '30-90 days',
      action: claim.documentsNeeded ? 'DOCUMENTS_NEEDED' : null,
    },
    4: {
      title: 'Evidence Review',
      description: 'All evidence has been gathered and is being reviewed.',
      nextSteps: 'No action needed. The VA is reviewing your evidence.',
      estimatedTime: '2-6 weeks',
    },
    5: {
      title: 'Rating Decision',
      description: 'Your claim is being rated by a VA rater.',
      nextSteps: 'No action needed. A decision will be made soon.',
      estimatedTime: '1-2 weeks',
    },
    6: {
      title: 'Preparing Decision Letter',
      description: 'Your claim has been decided and the decision letter is being prepared.',
      nextSteps: 'Watch for your decision letter in the mail and in this app.',
      estimatedTime: '3-7 days',
    },
    7: {
      title: 'Decision Letter Sent',
      description: 'Your decision letter has been sent.',
      nextSteps: 'Review your decision letter for details on your claim.',
      estimatedTime: '5-10 days for delivery',
    },
    8: {
      title: 'Claim Closed',
      description: 'Your claim has been completed.',
      nextSteps: claim.closeDate
        ? 'If you disagree with the decision, you can file an appeal.'
        : 'No further action needed.',
      estimatedTime: 'Complete',
    },
  };

  return statusMap[claim.phase];
}

Real-Time Status Change Detection

Veterans want to know immediately when their claim status changes:

class ClaimStatusMonitor {
  private pollingInterval = 4 * 60 * 60 * 1000; // 4 hours
  private backgroundTaskId = 'claim-status-check';

  async startMonitoring(userId: string) {
    // Background fetch for iOS/Android
    await BackgroundFetch.registerTaskAsync(this.backgroundTaskId, {
      minimumInterval: this.pollingInterval,
      stopOnTerminate: false,
      startOnBoot: true,
    });
  }

  async checkForUpdates(userId: string) {
    try {
      const claims = await claimsService.getClaims(userId);
      const storedClaims = await getStoredClaims(userId);

      for (const claim of claims) {
        const stored = storedClaims.find((c) => c.id === claim.id);

        if (!stored) {
          // New claim filed
          await sendNotification({
            title: 'New Claim Filed',
            body: `Your ${claim.type} claim has been received by the VA.`,
            data: { claimId: claim.id },
          });
          continue;
        }

        // Check for phase change
        if (claim.phase !== stored.phase) {
          const statusInfo = getClaimStatusInfo(claim);

          await sendNotification({
            title: 'Claim Status Updated',
            body: `Your claim is now in: ${statusInfo.title}`,
            data: { claimId: claim.id },
          });
        }

        // Check for document requests
        if (claim.documentsNeeded && !stored.documentsNeeded) {
          await sendNotification({
            title: 'Documents Needed',
            body: 'The VA has requested additional documents for your claim.',
            data: { claimId: claim.id },
          });
        }

        // Check for decision letter
        if (claim.decisionLetterSent && !stored.decisionLetterSent) {
          await sendNotification({
            title: 'Decision Letter Available',
            body: 'Your claim decision letter is now available.',
            data: { claimId: claim.id },
          });
        }
      }

      // Update stored claims
      await storeClaimsLocally(claims);
    } catch (error) {
      logger.error('Failed to check claim updates', { error });
    }
  }
}

Offline-First for Veterans

Internet access isn’t guaranteed. Veterans might check their claims from:

  • Rural areas with poor connectivity
  • Overseas military bases
  • During travel
  • In VA facilities with spotty WiFi

Offline Data Strategy

import AsyncStorage from '@react-native-async-storage/async-storage';

class OfflineClaimsManager {
  private storageKey = 'offline_claims';

  async syncClaims(userId: string) {
    try {
      // Fetch latest data when online
      const claims = await claimsService.getClaims(userId);

      // Store for offline access
      await AsyncStorage.setItem(
        `${this.storageKey}_${userId}`,
        JSON.stringify({
          claims,
          lastSync: new Date().toISOString(),
        })
      );

      return claims;
    } catch (error) {
      // If offline, return cached data
      return await this.getCachedClaims(userId);
    }
  }

  async getCachedClaims(userId: string) {
    const cached = await AsyncStorage.getItem(`${this.storageKey}_${userId}`);

    if (!cached) {
      throw new Error('No offline data available');
    }

    const data = JSON.parse(cached);
    return data.claims;
  }

  async getLastSyncTime(userId: string) {
    const cached = await AsyncStorage.getItem(`${this.storageKey}_${userId}`);

    if (!cached) return null;

    const data = JSON.parse(cached);
    return new Date(data.lastSync);
  }
}

// UI Component
function ClaimsListScreen() {
  const [claims, setClaims] = useState<VAClaim[]>([]);
  const [isOffline, setIsOffline] = useState(false);
  const [lastSync, setLastSync] = useState<Date | null>(null);

  useEffect(() => {
    loadClaims();

    // Listen for network changes
    const unsubscribe = NetInfo.addEventListener((state) => {
      setIsOffline(!state.isConnected);
      if (state.isConnected) {
        loadClaims(); // Sync when back online
      }
    });

    return unsubscribe;
  }, []);

  async function loadClaims() {
    try {
      const data = await offlineManager.syncClaims(userId);
      setClaims(data);

      const sync = await offlineManager.getLastSyncTime(userId);
      setLastSync(sync);
    } catch (error) {
      showError('Failed to load claims');
    }
  }

  return (
    <View>
      {isOffline && (
        <Banner type="warning">
          Viewing offline data. Last updated: {formatDate(lastSync)}
        </Banner>
      )}

      <FlatList
        data={claims}
        renderItem={({ item }) => <ClaimCard claim={item} />}
        refreshControl={
          <RefreshControl
            refreshing={loading}
            onRefresh={loadClaims}
            enabled={!isOffline}
          />
        }
      />
    </View>
  );
}

Privacy and Security Best Practices

Working with veteran data means meeting high security standards.

Data Minimization

Only store what’s absolutely necessary:

// Never store these
❌ Social Security Numbers
❌ Full medical records
❌ Raw decision letter PDFs (contain SSN)
❌ VA passwords (we use OAuth)

// Safe to store (with encryption)
âś… Claim IDs
âś… Status information
âś… Phase dates
âś… Generic contention descriptions
âś… Document request flags

Encryption at Rest

import * as Crypto from 'expo-crypto';

async function encryptSensitiveData(data: string, userKey: string) {
  const encrypted = await Crypto.digestStringAsync(
    Crypto.CryptoDigestAlgorithm.SHA256,
    data + userKey
  );

  return encrypted;
}

// For storing claim data
async function storeClaimSecurely(claim: VAClaim, userId: string) {
  const encrypted = await Crypto.encryptAsync(
    Crypto.CryptoEncryptionAlgorithm.AES,
    JSON.stringify(claim),
    await getEncryptionKey(userId)
  );

  await SecureStore.setItemAsync(`claim_${claim.id}`, encrypted);
}

Compliance Considerations

Government apps must follow specific regulations:

Section 508 Compliance (Accessibility):

// Screen reader support
<TouchableOpacity
  accessible={true}
  accessibilityLabel="View claim details"
  accessibilityHint="Opens detailed information about your compensation claim"
  accessibilityRole="button"
>
  <ClaimCard claim={claim} />
</TouchableOpacity>

// High contrast support
const colors = {
  primary: colorScheme === 'high-contrast' ? '#000000' : '#005ea2',
  background: colorScheme === 'high-contrast' ? '#ffffff' : '#f0f0f0',
};

// Minimum touch targets (44x44 points)
const styles = StyleSheet.create({
  button: {
    minHeight: 44,
    minWidth: 44,
  },
});

User Experience for Veterans

Veterans are diverse—different ages, tech comfort levels, and abilities. The app needs to work for everyone.

Simplified Navigation

// Bottom tab navigation - simple and familiar
function AppNavigator() {
  return (
    <Tab.Navigator
      screenOptions={{
        tabBarActiveTintColor: '#005ea2',
        tabBarLabelStyle: { fontSize: 12, fontWeight: '600' },
      }}
    >
      <Tab.Screen
        name="Claims"
        component={ClaimsScreen}
        options={{
          tabBarIcon: ({ color }) => <Icon name="document-text" color={color} />,
        }}
      />
      <Tab.Screen
        name="Profile"
        component={ProfileScreen}
        options={{
          tabBarIcon: ({ color }) => <Icon name="person" color={color} />,
        }}
      />
      <Tab.Screen
        name="Help"
        component={HelpScreen}
        options={{
          tabBarIcon: ({ color }) => <Icon name="help-circle" color={color} />,
        }}
      />
    </Tab.Navigator>
  );
}

Clear Visual Hierarchy

function ClaimCard({ claim }: { claim: VAClaim }) {
  const statusInfo = getClaimStatusInfo(claim);

  return (
    <Card>
      {/* Most important info first */}
      <StatusBadge phase={claim.phase}>
        {statusInfo.title}
      </StatusBadge>

      <ClaimType>{claim.type} Claim</ClaimType>

      {/* Action items prominently displayed */}
      {claim.documentsNeeded && (
        <ActionAlert>
          <Icon name="alert-circle" color="red" />
          <Text>Documents needed - tap to view</Text>
        </ActionAlert>
      )}

      {/* Timeline */}
      <ProgressBar current={claim.phase} total={8} />

      {/* Secondary info */}
      <MetaInfo>
        Filed: {formatDate(claim.dateFiled)}
      </MetaInfo>
    </Card>
  );
}

Help and Support

function HelpScreen() {
  return (
    <ScrollView>
      <Section title="Understanding Your Claim Status">
        {/* Plain English explanations */}
      </Section>

      <Section title="What Documents Do I Need?">
        {/* Common documents by claim type */}
      </Section>

      <Section title="Contact VA">
        <LinkButton href="tel:1-800-827-1000">
          Call VA: 1-800-827-1000
        </LinkButton>
        <LinkButton href="https://www.va.gov">
          Visit VA.gov
        </LinkButton>
      </Section>

      <Section title="About VetStack">
        <Text>
          Built by veterans, for veterans. This app uses official VA APIs
          and never stores your VA password.
        </Text>
      </Section>
    </ScrollView>
  );
}

Testing with Real Veterans

We couldn’t build VetStack without veteran feedback. Here’s what we learned:

What Veterans Loved

Push Notifications: “I don’t have to keep checking VA.gov anymore”

Plain English: “Finally, I understand what phase my claim is in”

Offline Access: “I can check my claims anywhere, even on base”

No Passwords: “I feel safer using OAuth instead of giving my password”

What We Changed Based on Feedback

Initially: Showed all 8 phases on every claim Feedback: “Too much information, it’s overwhelming” Change: Show current phase + next phase, hide the rest

Initially: Used VA terminology (e.g., “evidence gathering”) Feedback: “What does that mean in normal language?” Change: Added plain English descriptions for every phase

Initially: Notifications for every phase change Feedback: “Too many notifications” Change: Only notify for actionable items and major milestones

Launch Strategy for Civic Tech

Building for veterans requires a different go-to-market approach:

Veteran Community Outreach

  • Partnerships with VSOs (Veterans Service Organizations)
  • Presence at VA events and clinics
  • Reddit communities (r/Veterans, r/VeteransBenefits)
  • Facebook veteran groups

Free Forever

Veterans shouldn’t pay to access their own benefits data. VetStack is 100% free, always.

Trust Building

  • “Built by veterans, for veterans”
  • Transparent about data practices
  • Open about OAuth (no passwords stored)
  • Clear privacy policy in plain English

Future Features

Based on veteran requests:

Appeals Tracking: Track appeals through the higher-level review process

Benefit Letter Access: Quick access to award letters and benefit summaries

Appointment Reminders: Integration with VA health appointment system

Buddy System: Connect with other veterans going through similar claims

Document OCR: Scan and categorize VA documents automatically

Getting Started with VA APIs

If you want to build for veterans:

  1. Register for VA API Access: https://developer.va.gov
  2. Read the Lighthouse API docs: https://developer.va.gov/explore
  3. Join the developer community: Slack and forums available
  4. Test in sandbox first: VA provides test accounts
  5. Submit for production access: Security review required

The VA developer team is supportive and responsive. They want developers building for veterans.

Conclusion

Building for veterans is rewarding but demands high standards. You’re working with sensitive data, government APIs, and a population that’s often underserved by technology.

The technical challenges—OAuth, offline sync, push notifications—are solvable. The hard part is building something veterans trust and actually want to use.

Start with empathy. Talk to veterans. Understand their frustrations with the current system. Build something that genuinely helps, not just another app.

And remember: veterans have already given enough. They shouldn’t have to fight with technology to access their own benefits.

VetStack is currently in pre-release. Built by veterans, for veterans. Coming soon to iOS, Android, and web. Always free.

Topics

veterans government apis mobile apps oauth civic tech
V

Written by VooStack Team

Contact author

Share this article