Skip to main content

API Philosophy

New Expensify’s API is designed around these core principles:
  1. 1:1:1 Ratio: One user action → One API command → One database operation
  2. Optimistic First: UI updates before server confirmation
  3. Offline Support: All requests work offline and sync later
  4. No Client Logic: API responses contain data, not instructions
  5. Pusher Integration: Real-time updates via WebSocket
For complete API details, see contributingGuides/API.md in the repository.

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:

Format

<Verb>[<Noun>]
Examples:
  • RequestMoney
  • OpenReport
  • AcceptTask
  • SignIn

Rules

  1. Unique verbs: Use descriptive verbs (Request, Open, Accept)
  2. Standard verbs only if unique fails: Update, Add, Delete
  3. No parameter names: Bad: SignInWithPassword, Good: SignIn
  4. No implementation details: Bad: AddVBBA, Good: AddBankAccount
  5. No UI terms: Bad: OpenReportPage, Good: OpenReport
  6. 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;
};
MethodUsageExample
SETReplace entire valueClear a report
MERGEUpdate specific fieldsChange report name
MERGE_COLLECTIONUpdate multiple itemsBulk 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/:
  1. Logging: Log request details
  2. RecheckConnection: Verify connectivity
  3. Reauthentication: Handle expired auth (jsonCode 407)
  4. HandleDeletedAccount: Handle account deletion (jsonCode 408)
  5. SupportalPermission: Handle permission denials
  6. HandleUnusedOptimisticID: Update IDs in queued requests
  7. Pagination: Handle paginated responses
  8. SentryServerTiming: Track server performance
  9. SaveResponseInOnyx: Apply success/failure data
  10. 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

Build docs developers (and LLMs) love