API Philosophy
New Expensify’s API is designed around these core principles:
1:1:1 Ratio : One user action → One API command → One database operation
Optimistic First : UI updates before server confirmation
Offline Support : All requests work offline and sync later
No Client Logic : API responses contain data, not instructions
Pusher Integration : Real-time updates via WebSocket
API Structure
Location
API code is organized in src/libs/API/:
src/libs/API/
├── index.ts # Main API client (read, write, makeRequestWithSideEffects)
├── types.ts # TypeScript types for all commands
└── parameters/ # Parameter types for each command
├── AddCommentParams.ts
├── CreateWorkspaceParams.ts
└── ...
Command Types
All API commands are defined in src/libs/API/types.ts:
// READ commands - fetch data
export const READ_COMMANDS = {
OPEN_REPORT: 'OpenReport' ,
OPEN_APP: 'OpenApp' ,
GET_POLICY_LIST: 'GetPolicyList' ,
} as const ;
// WRITE commands - modify data
export const WRITE_COMMANDS = {
ADD_COMMENT: 'AddComment' ,
CREATE_WORKSPACE: 'CreateWorkspace' ,
UPDATE_POLICY: 'UpdatePolicy' ,
} as const ;
// SIDE_EFFECT commands - return data to caller
export const SIDE_EFFECT_REQUEST_COMMANDS = {
AUTHENTICATE_PUSHER: 'AuthenticatePusher' ,
PLAID_LINK_TOKEN: 'PlaidLinkToken' ,
} as const ;
Making API Calls
API.write() - WRITE Requests
For actions that modify data. Requests are queued and retried if needed.
import API from '@libs/API' ;
import type { OnyxUpdate } from '@src/types/onyx/Request' ;
import ONYXKEYS from '@src/ONYXKEYS' ;
function createWorkspace ( policyName : string ) {
const policyID = generateRandomID ();
// Optimistic data: Applied immediately
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . SET ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
id: policyID ,
name: policyName ,
type: CONST . POLICY . TYPE . TEAM ,
pendingAction: CONST . RED_BRICK_ROAD_PENDING_ACTION . ADD ,
},
},
];
// Success data: Applied when API succeeds
const successData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
pendingAction: null ,
},
},
];
// Failure data: Applied when API fails
const failureData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . SET ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: null , // Remove optimistic policy
},
];
API . write (
'CreateWorkspace' ,
{ policyName },
{ optimisticData , successData , failureData },
);
}
When to use :
Creating new data
Updating existing data
Deleting data
Any action that should work offline
API.read() - READ Requests
For fetching data without modifying server state.
function openReport ( reportID : string ) {
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . REPORT_METADATA }${ reportID } ` ,
value: {
isLoadingInitialReportActions: true ,
},
},
];
const successData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . REPORT_METADATA }${ reportID } ` ,
value: {
isLoadingInitialReportActions: false ,
},
},
];
const failureData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . REPORT_METADATA }${ reportID } ` ,
value: {
isLoadingInitialReportActions: false ,
},
},
];
API . read ( 'OpenReport' , { reportID }, { optimisticData , successData , failureData });
}
When to use :
Loading initial data
Refreshing data
Data that’s not persisted
Commands that don’t need offline support
API.makeRequestWithSideEffects() - Side Effect Requests
For requests that need the response data immediately.
import { makeRequestWithSideEffects } from '@libs/API' ;
async function generatePlaidLinkToken ( bankAccountID : string ) {
const response = await makeRequestWithSideEffects (
'PlaidLinkToken' ,
{ bankAccountID },
);
if ( response ?. linkToken ) {
openPlaidLink ( response . linkToken );
}
}
Use sparingly : makeRequestWithSideEffects is discouraged. Only use when you absolutely need the response data to proceed. Discuss in Slack before using.
When to use (rare cases):
Third-party integration (Plaid, Onfido)
Redirects based on response
Cannot proceed without server data
Response Handling
HTTPS Response
Data returned directly to the requesting client:
// PHP backend
$response [ 'onyxData' ][] = [
'onyxMethod' => Onyx :: METHOD_MERGE ,
'key' => 'session' ,
'value' => [ 'authToken' => $newToken ],
];
Used for:
READ responses
Client-specific data
Error messages
Pusher Events
Data broadcast to all connected clients:
// PHP backend
$onyxUpdate [] = [
'onyxMethod' => Onyx :: METHOD_MERGE ,
'key' => "report_{ $reportID }" ,
'value' => [ 'reportName' => $newName ],
];
// Automatically sent via Pusher to all clients with this report
Used for:
WRITE responses (when successful)
Real-time updates
Multi-user data sync
Command Naming Conventions
API commands follow strict naming rules:
Examples:
RequestMoney
OpenReport
AcceptTask
SignIn
Rules
Unique verbs : Use descriptive verbs (Request, Open, Accept)
Standard verbs only if unique fails : Update, Add, Delete
No parameter names : Bad: SignInWithPassword, Good: SignIn
No implementation details : Bad: AddVBBA, Good: AddBankAccount
No UI terms : Bad: OpenReportPage, Good: OpenReport
One command per action : Not StartAppWhileSignedInAndOpenWorkspace
Examples
// ✅ Good
'CreateWorkspace'
'UpdatePolicy'
'DeleteTransaction'
'RequestMoney'
'OpenReport'
// ❌ Bad
'CreateWorkspaceInNewExpensify' // Implementation detail
'OpenReportPage' // UI term
'UpdatePolicyName' // Parameter in name
'AddVBBA' // Abbreviation
Onyx Data Updates
Update Types
Three types of Onyx updates in API calls:
type OnyxUpdate = {
onyxMethod : 'set' | 'merge' | 'mergeCollection' ;
key : OnyxKey ;
value : any ;
};
Method Usage Example SETReplace entire value Clear a report MERGEUpdate specific fields Change report name MERGE_COLLECTIONUpdate multiple items Bulk update reports
When to Include Each Data Type
// Always include optimisticData for WRITE requests
const optimisticData : OnyxUpdate [] = [ ... ]; // Required
// Include successData if different from optimistic
const successData : OnyxUpdate [] = [ ... ]; // Optional
// Always include failureData for WRITE requests
const failureData : OnyxUpdate [] = [ ... ]; // Required
// OR use finallyData if success and failure are the same
const finallyData : OnyxUpdate [] = [ ... ]; // Alternative to success + failure
Example: Complete Onyx Updates
function updateWorkspaceName ( policyID : string , newName : string ) {
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
name: newName ,
pendingFields: {
name: CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ,
},
},
},
];
const successData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
pendingFields: {
name: null ,
},
},
},
];
const failureData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
name: originalName , // Revert to original
pendingFields: {
name: null ,
},
errorFields: {
name: {
[Date. now ()]: 'Failed to update name' ,
},
},
},
},
];
API . write (
'UpdateWorkspaceName' ,
{ policyID , newName },
{ optimisticData , successData , failureData },
);
}
Error Handling
Error Structure
Errors are stored with timestamps:
type Errors = Record < string , string >; // {timestamp: message}
const failureData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ONYXKEYS . SOME_DATA ,
value: {
errors: {
[Date. now ()]: 'Failed to save data' ,
},
},
},
];
Displaying Errors
Use OfflineWithFeedback component:
import OfflineWithFeedback from '@components/OfflineWithFeedback' ;
function MyComponent ({ data }) {
const handleDismissError = () => {
Onyx . merge ( ONYXKEYS . SOME_DATA , { errors: null });
};
return (
< OfflineWithFeedback
errors = {data. errors }
onClose = { handleDismissError }
>
< View >{ /* Content */ } </ View >
</ OfflineWithFeedback >
);
}
Middleware
API requests pass through middleware in src/libs/Middleware/:
Logging : Log request details
RecheckConnection : Verify connectivity
Reauthentication : Handle expired auth (jsonCode 407)
HandleDeletedAccount : Handle account deletion (jsonCode 408)
SupportalPermission : Handle permission denials
HandleUnusedOptimisticID : Update IDs in queued requests
Pagination : Handle paginated responses
SentryServerTiming : Track server performance
SaveResponseInOnyx : Apply success/failure data
FraudMonitoring : Tag requests for fraud detection
Pusher Integration
Real-time updates via Pusher WebSocket:
import Pusher from '@libs/Pusher' ;
// Pusher automatically:
// 1. Connects when user logs in
// 2. Subscribes to user's channels
// 3. Receives onyxApiUpdate events
// 4. Applies updates to Onyx
// 5. Components re-render automatically
Pusher Socket ID
WRITE requests include pusherSocketID to prevent echo:
// Automatic - handled by API.write()
const data = {
... params ,
pusherSocketID: Pusher . getPusherSocketID (),
};
// Server includes this in Pusher event
// Event is not sent back to requesting client
Best Practices
1. One API Call Per User Action
// ✅ Good: Single API call
function saveWorkspaceSettings ( policyID : string , settings : Settings ) {
API . write ( 'UpdateWorkspace' , { policyID , ... settings });
}
// ❌ Bad: Multiple API calls
function saveWorkspaceSettings ( policyID : string , settings : Settings ) {
API . write ( 'UpdateWorkspaceName' , { policyID , name: settings . name });
API . write ( 'UpdateWorkspaceDescription' , { policyID , desc: settings . desc });
API . write ( 'UpdateWorkspaceAvatar' , { policyID , avatar: settings . avatar });
}
2. Always Provide Failure Data
// ✅ Good: Handles failure
API . write ( 'CreatePolicy' , params , {
optimisticData ,
successData ,
failureData , // Cleans up on failure
});
// ❌ Bad: No failure handling
API . write ( 'CreatePolicy' , params , {
optimisticData ,
});
3. Use Correct Request Type
// ✅ Good: READ for fetching
API . read ( 'GetPolicyList' , {});
// ✅ Good: WRITE for modifying
API . write ( 'UpdatePolicy' , { policyID , changes });
// ❌ Bad: WRITE for fetching
API . write ( 'GetPolicyList' , {}); // Don't do this
Next Steps
Authentication Learn about API authentication
Endpoints Explore available API endpoints
State Management Understand Onyx integration
Offline-First Learn offline patterns