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:
- Logging into VA.gov
- Navigating to the claims section
- Clicking through to individual claims
- Deciphering status codes and timelines
- 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:
- Register for VA API Access: https://developer.va.gov
- Read the Lighthouse API docs: https://developer.va.gov/explore
- Join the developer community: Slack and forums available
- Test in sandbox first: VA provides test accounts
- 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.