> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylon.mortgage/llms.txt
> Use this file to discover all available pages before exploring further.

# Tracking loan updates

> Monitor loan changes by polling the GraphQL API

To monitor loan changes, you'll need to poll the GraphQL API at a cadence appropriate for your use case.

## Quick reference

| Example entities     | Recommended interval | Detection method                |
| -------------------- | -------------------- | ------------------------------- |
| Loan stage           | 15-30 min            | Compare `currentStage`          |
| Tasks                | 15-30 min            | Track task IDs and `status`     |
| Documents            | 30-60 min            | Track `uploadedAt` timestamps   |
| Borrower info        | Daily                | Compare field values            |
| Credit pull (active) | 30 sec - 2 min       | Poll until `status` is complete |

## How it works

<Steps>
  <Step title="Write a focused query">
    Request only the fields you need. Don't fetch the entire loan when you only
    need the stage.
  </Step>

  <Step title="Store the previous state">
    Cache the values you're monitoring (e.g., `currentStage`, task IDs).
  </Step>

  <Step title="Poll at your chosen interval">
    Fetch the data and compare against your cached state.
  </Step>

  <Step title="Emit events on change">
    When a difference is detected, trigger your business logic.
  </Step>
</Steps>

## Implementation example

This TypeScript example polls for loan stage changes every 30 minutes and emits an event when a stage changes.

<Tabs>
  <Tab title="Quick Start">
    ```typescript theme={null}
    const poller = new LoanStagePoller({
      apiEndpoint: "https://api.pylon.mortgage/graphql",
      accessToken: "your-access-token",
      loanIds: ["loan-id-1", "loan-id-2"],
      pollingIntervalMs: 30 * 60 * 1000, // 30 minutes
    });

    poller.on("stageChange", (event) => {
      console.log(`Loan ${event.friendlyId}: ${event.previousStage} → ${event.currentStage}`);
      // Update your database, notify users, trigger workflows, etc.
    });

    poller.on("error", ({ loanId, error }) => {
      console.error(`Error polling loan ${loanId}:`, error);
    });

    poller.start();
    ```
  </Tab>

  <Tab title="GraphQL Query">
    ```graphql theme={null}
    query GetLoanStage($id: ID!) {
      loan(id: $id) {
        id
        friendlyId
        currentStage
        stages {
          name
          timestamp
        }
      }
    }
    ```
  </Tab>

  <Tab title="Full Implementation">
    ```typescript theme={null}
    import { EventEmitter } from "events";

    // ─────────────────────────────────────────────────────────────
    // Types
    // ─────────────────────────────────────────────────────────────

    interface LoanStage {
      name: string;
      timestamp: string;
    }

    interface LoanStageResponse {
      data: {
        loan: {
          id: string;
          friendlyId: string;
          currentStage: string;
          stages: LoanStage[];
        } | null;
      };
    }

    interface StageChangeEvent {
      loanId: string;
      friendlyId: string;
      previousStage: string;
      currentStage: string;
      timestamp: string;
    }

    interface PollerConfig {
      apiEndpoint: string;
      accessToken: string;
      loanIds: string[];
      pollingIntervalMs?: number;
    }

    // ─────────────────────────────────────────────────────────────
    // Query
    // ─────────────────────────────────────────────────────────────

    const LOAN_STAGE_QUERY = `
      query GetLoanStage($id: ID!) {
        loan(id: $id) {
          id
          friendlyId
          currentStage
          stages {
            name
            timestamp
          }
        }
      }
    `;

    // ─────────────────────────────────────────────────────────────
    // Poller Class
    // ─────────────────────────────────────────────────────────────

    class LoanStagePoller extends EventEmitter {
      private apiEndpoint: string;
      private accessToken: string;
      private loanIds: string[];
      private pollingIntervalMs: number;
      private previousStages = new Map<string, string>();
      private intervalId: NodeJS.Timeout | null = null;

      constructor(config: PollerConfig) {
        super();
        this.apiEndpoint = config.apiEndpoint;
        this.accessToken = config.accessToken;
        this.loanIds = config.loanIds;
        this.pollingIntervalMs = config.pollingIntervalMs ?? 30 * 60 * 1000;
      }

      async start(): Promise<void> {
        await this.poll();
        this.intervalId = setInterval(() => this.poll(), this.pollingIntervalMs);
        this.emit("started", { loanIds: this.loanIds, intervalMs: this.pollingIntervalMs });
      }

      stop(): void {
        if (this.intervalId) {
          clearInterval(this.intervalId);
          this.intervalId = null;
          this.emit("stopped");
        }
      }

      addLoan(loanId: string): void {
        if (!this.loanIds.includes(loanId)) this.loanIds.push(loanId);
      }

      removeLoan(loanId: string): void {
        this.loanIds = this.loanIds.filter((id) => id !== loanId);
        this.previousStages.delete(loanId);
      }

      private async poll(): Promise<void> {
        for (const loanId of this.loanIds) {
          try {
            const loan = await this.fetchLoan(loanId);
            if (!loan) {
              this.emit("error", { loanId, error: new Error("Loan not found") });
              continue;
            }

            const previous = this.previousStages.get(loanId);
            const current = loan.currentStage;

            if (previous !== undefined && previous !== current) {
              const latest = loan.stages[loan.stages.length - 1];
              this.emit("stageChange", {
                loanId: loan.id,
                friendlyId: loan.friendlyId,
                previousStage: previous,
                currentStage: current,
                timestamp: latest?.timestamp ?? new Date().toISOString(),
              } as StageChangeEvent);
            }

            this.previousStages.set(loanId, current);
          } catch (error) {
            this.emit("error", { loanId, error });
          }
        }
      }

      private async fetchLoan(loanId: string): Promise<LoanStageResponse["data"]["loan"]> {
        const res = await fetch(this.apiEndpoint, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${this.accessToken}`,
          },
          body: JSON.stringify({ query: LOAN_STAGE_QUERY, variables: { id: loanId } }),
        });

        if (!res.ok) throw new Error(`API error: ${res.status}`);
        const json: LoanStageResponse = await res.json();
        return json.data.loan;
      }
    }

    export { LoanStagePoller, StageChangeEvent, PollerConfig };
    ```
  </Tab>
</Tabs>

## Starter queries

Use these comprehensive queries as starting points. Trim them down to only the fields you need for your specific polling use case.

<AccordionGroup>
  <Accordion title="Full loan">
    Full loan data including stages, pricing, rate lock, and associated entities.

    ```graphql theme={null}
    query GetLoan($id: ID!) {
      loan(id: $id) {
        id
        friendlyId
        loanNumber
        loanPurpose
        loanTermYears
        noteRatePercent
        maxLoanAmount
        maxPurchasePrice
        purchasePrice
        ltv
        outOfPocketMax
        totalAssetsAvailable
        earnestMoneyDeposit
        sellerCredit
        refinanceCashOutProceeds
        closingDate
        fundedDate
        latestPreApprovalRunTime
        currentStage
        dealId
        isClosed
        isFrozen
        isFirstTimeHomebuyer
        pylonApproved
        useOwnTitleCompany
        allowedStates

        assignedLoanOfficer {
          id
          email
          firstName
          lastName
        }
        borrowerPreferences {
          closingCosts
          downPaymentAmount
          monthlyPayment
          rate
        }
        borrowers(first: 5) {
          edges {
            node {
              id
              dependentAges
              emailVerified
              externalUserId
              isFirstTimeHomeBuyer
            }
          }
          pageInfo {
            hasNextPage
            endCursor
          }
        }
        changeRequests {
          id
          status
        }
        concessions {
          id
          amount
          approvedAt
          reason
        }
        contacts {
          id
          email
          companyName
          firstName
        }
        documents {
          id
          name
          category
          uploadedAt
        }
        fees {
          id
          feeActualTotalAmount
          feeDescription
        }
        preapprovalProduct {
          id
          type
          description
        }
        productPricingRate {
          id
          apr
          closingCosts
        }
        productStructure {
          id
          apr
          closingCosts
        }
        rateLock {
          status
          expirationTime
          lockedTime
          period
        }
        stages {
          name
          timestamp
        }
        subjectProperty {
          id
          avmEstimatedValue
          hoaDues
          homeInsuranceMonthlyAmount
          subjectPropertyIntent {
            id
            neighborhoodHousingType
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Borrower personal information">
    Identity and contact details.

    ```graphql theme={null}
    query GetBorrowerPersonalInfo($id: ID!) {
      borrower(id: $id) {
        id
        dependentAges
        personalInformation {
          firstName
          middleName
          lastName
          email
          dateOfBirth
          citizenshipResidencyType
          taxIdentifierNumber
          taxIdentifierNumberType
          phoneNumbers {
            id
            number
            type
          }
          currentAddress {
            id
            address {
              line
              line2
              city
              state
              zipCode
            }
          }
        }
        mailingAddress {
          line
          line2
          city
          state
          zipCode
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Borrower financials (assets, income, properties)">
    Complete financial profile for underwriting.

    ```graphql theme={null}
    query GetBorrowerFinancials($id: ID!) {
      borrower(id: $id) {
        assets {
          id
          amount
          borrowerIds
          ... on CheckingAccountAsset {
            accountIdentifier
            institutionName
          }
          ... on SavingsAccountAsset {
            accountIdentifier
            institutionName
          }
          ... on RetirementFundAsset {
            accountIdentifier
            institutionName
          }
          ... on GiftAsset {
            source
            donorName
            dateOfTransfer
          }
        }
        incomes(first: 25) {
          edges {
            node {
              id
              statedMonthlyAmount
              ... on StandardEmploymentIncome {
                employment {
                  position
                  startDate
                  isCurrentEmployment
                  ... on StandardEmployment {
                    employer {
                      name
                    }
                  }
                }
              }
              ... on SelfEmploymentIncome {
                employment {
                  position
                  ... on SelfEmployment {
                    business {
                      name
                    }
                  }
                }
              }
            }
          }
          pageInfo {
            hasNextPage
          }
        }
        ownedProperties(first: 25) {
          edges {
            node {
              id
              address {
                line
                city
                state
                zipCode
              }
              intendedDisposition
              currentUsageType
              rentalIncome {
                statedMonthlyAmount
              }
              liabilities {
                id
                balance
              }
            }
          }
          pageInfo {
            hasNextPage
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Borrower liabilities" iconType="regular">
    Debts and obligations for [DTI](/entity-models/key-concepts/dti) calculation.

    ```graphql theme={null}
    query GetBorrowerLiabilities($id: ID!, $after: String) {
      borrower(id: $id) {
        liabilities(first: 25, after: $after) {
          edges {
            node {
              id
              accountIdentifier
              balance
              creditorName
              type
              monthlyPayment
              intent
              exclusionReason
            }
          }
          pageInfo {
            endCursor
            hasNextPage
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Tasks">
    Borrower and loan officer tasks for workflow management.

    ```graphql theme={null}
    query GetTasks($loanId: ID!) {
      loan(id: $loanId) {
        id
        borrowers(first: 10) {
          edges {
            node {
              id
              personalInformation {
                firstName
                lastName
              }
              tasks: borrowerTasks {
                id
                title
                status
                type
                priority
                dueDate
                completedAt
                createdOn
                borrowerFacingDescription
                documentLinks {
                  id
                  title
                  url
                }
                details {
                  borrowerId
                  borrowerName
                  entityKind
                }
              }
            }
          }
        }
        loanOfficerTasks: loanOfficerDocumentTasks {
          id
          title
          status
          type
          priority
          dueDate
          completedAt
          createdOn
          description
          documentLinks {
            id
            title
            url
          }
          details {
            borrowerId
            borrowerName
            entityKind
          }
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Best practices

### Use focused queries

Don't fetch the entire loan object when you only need `currentStage`. Smaller queries are faster, reduce bandwidth, and make change detection simpler.

### Implement exponential backoff

If requests fail, wait progressively longer before retrying. This prevents overwhelming the API during outages and gives transient issues time to resolve.

```typescript theme={null}
const backoff = (attempt: number) => Math.min(1000 * 2 ** attempt, 30000);

// Attempt 1: 2 seconds
// Attempt 2: 4 seconds
// Attempt 3: 8 seconds
// ...capped at 30 seconds
```

### Store state efficiently

Only cache what you need for comparison. For stage monitoring, store a `Map<loanId, stage>` rather than full loan objects. This keeps memory usage low and simplifies the comparison logic.

### Stagger requests for many loans

If monitoring hundreds of loans, don't fire all requests simultaneously. Spread them over the polling interval to avoid rate limits and reduce peak load.

### Use pagination cursors for lists

For lists like tasks or documents, track cursors to efficiently detect new items rather than comparing entire arrays. Store the last cursor and fetch only items after it.

```typescript theme={null}
// Store the cursor from your last successful poll
let lastCursor: string | null = null;

// On next poll, fetch only new items
const query = `
  query GetTasks($loanId: ID!, $after: String) {
    loan(id: $loanId) {
      borrowers(first: 10) {
        edges {
          node {
            tasks: borrowerTasks(after: $after) {
              id
              status
            }
          }
        }
        pageInfo { endCursor }
      }
    }
  }
`;
```

### Log polling activity

Keep logs of your polling activity for debugging. Include timestamps, loan IDs, and any detected changes. This helps diagnose issues when changes aren't being detected as expected.

```typescript theme={null}
console.log(
  JSON.stringify({
    timestamp: new Date().toISOString(),
    loanId,
    previousStage,
    currentStage,
    changed: previousStage !== currentStage,
  })
);
```

## Rate limiting

<Warning>
  Polling too aggressively can result in rate limiting. Start with conservative
  intervals and adjust based on your needs.
</Warning>

If you're monitoring many loans:

* Stagger requests rather than firing them simultaneously
* Use longer intervals for less time-sensitive data
* Implement circuit breakers to pause polling if you hit rate limits
* Log all poll activity for debugging
