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

Quick reference

Example entitiesRecommended intervalDetection method
Loan stage15-30 minCompare currentStage
Tasks15-30 minTrack task IDs and status
Documents30-60 minTrack uploadedAt timestamps
Borrower infoDailyCompare field values
Credit pull (active)30 sec - 2 minPoll until status is complete

How it works

1

Write a focused query

Request only the fields you need. Don’t fetch the entire loan when you only need the stage.
2

Store the previous state

Cache the values you’re monitoring (e.g., currentStage, task IDs).
3

Poll at your chosen interval

Fetch the data and compare against your cached state.
4

Emit events on change

When a difference is detected, trigger your business logic.

Implementation example

This TypeScript example polls for loan stage changes every 30 minutes and emits an event when a stage changes.
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();

Starter queries

Use these comprehensive queries as starting points. Trim them down to only the fields you need for your specific polling use case.
Full loan data including stages, pricing, rate lock, and associated entities.
query GetLoan($id: ID!) {
  loan(id: $id) {
    id
    friendlyId
    loanNumber
    loanPurpose
    loanTermYears
    noteRatePercent
    maxLoanAmount
    maxPurchasePrice
    purchasePrice
    ltv
    outOfPocketMax
    totalAssetsAvailable
    earnestMoneyDeposit
    sellerCredit
    refinanceCashOutProceeds
    closingDate
    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
      }
    }
  }
}
Identity and contact details.
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
    }
  }
}
Complete financial profile for underwriting.
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
      }
    }
  }
}
Debts and obligations for DTI calculation.
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
      }
    }
  }
}
Borrower and loan officer tasks for workflow management.
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
      }
    }
  }
}

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.
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.
// 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.
console.log(
  JSON.stringify({
    timestamp: new Date().toISOString(),
    loanId,
    previousStage,
    currentStage,
    changed: previousStage !== currentStage,
  })
);

Rate limiting

Polling too aggressively can result in rate limiting. Start with conservative intervals and adjust based on your needs.
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