Skip to main content
The prequalification endpoint is a powerful affordability calculator that goes far beyond traditional mortgage calculators. Unlike other affordability tools that are blind to pricing and guidelines, Pylon’s prequalification endpoint validates against all guidelines and pricing across all takeouts to determine maximum affordability within eligible products.

Why use prequalification?

All products at once

Returns maximum affordability across all products in a single call, rather than requiring separate calculations per product.

Guideline-aware

Validates against all encoded guidelines and pricing rules, ensuring accurate qualification estimates.

Minimal input

Requires only 8 pieces of information to get comprehensive affordability results.

Real-time pricing

Uses live rates and guidelines at the time of calculation, not static assumptions.

How it works

Prequalification is an asynchronous operation. When you create a prequalification request, it returns a job ID that you must poll to get the results.
1

Create prequalification

Submit a mutation with borrower information to create a prequalification job.
2

Receive job ID

The mutation returns a job ID that you’ll use to poll for results.
3

Poll for completion

Poll the prequalification query endpoint until the job status indicates completion.
4

Retrieve results

Once complete, fetch the results showing maximum affordability across all eligible products.

Creating a prequalification

To create a prequalification, you need 8 pieces of information:
mutation {
  preQualification {
    conventional(
      input: {
        fipsCountyCode: "06037"
        qualifyingFicoScore: 750
        qualifyingAssetsTotalAmount: 120000
        qualifyingMonthlyIncome: 10000
        qualifyingMonthlyExpenses: 2000
        propertyUsageType: PRIMARY_RESIDENCE
        neighborhoodHousingType: SINGLE_FAMILY
        hoaDues: 0
        maxDiscountPointsTotal: 0
      }
    ) {
      id
    }
  }
}

Required input fields

FieldTypeDescription
fipsCountyCodeString!Five-digit FIPS county code for the property location
qualifyingFicoScoreInt!FICO score used for qualification
qualifyingAssetsTotalAmountNonNegativeInt!Borrower’s total qualifying assets in dollars
qualifyingMonthlyIncomeNonNegativeInt!Borrower’s total qualifying monthly income
qualifyingMonthlyExpensesNonNegativeInt!Borrower’s total qualifying monthly expenses

Optional input fields

FieldTypeDefaultDescription
propertyUsageTypePropertyUsageTypePRIMARY_RESIDENCEPRIMARY_RESIDENCE, SECOND_HOME, or INVESTMENT
neighborhoodHousingTypeNeighborhoodHousingTypeSINGLE_FAMILYProperty type (e.g., SINGLE_FAMILY, CONDOMINIUM)
hoaDuesNonNegativeInt0Monthly HOA dues in dollars
maxDiscountPointsTotalFloat0Maximum total discount points

Response

The mutation returns a job ID:
{
  "data": {
    "preQualification": {
      "conventional": {
        "id": "prequal_abc123xyz"
      }
    }
  }
}
Save the id from the response. You’ll use it to poll for results.

Polling for results

After creating a prequalification, poll the query endpoint to check the job status and retrieve results:
query {
  preQualification {
    conventional(id: "prequal_abc123xyz") {
      id
      status
      result {
        product {
          id
          description
          type
        }
        loanAmount
        salesContractAmount
        ltv
        rate
        apr
        discountPointsTotal
        discountPointsTotalAmount
      }
      productBreakdown {
        product {
          id
          description
          type
        }
        result {
          ... on EligiblePreQualification {
            loanAmount
            salesContractAmount
            ltv
            rate
            apr
          }
          ... on IneligiblePreQualification {
            details {
              __typename
            }
          }
        }
      }
    }
  }
}

Job status

The status field indicates the current state of the prequalification:
StatusDescriptionAction
PROCESSINGJob is queued and processingContinue polling
SUCCEEDEDCalculation finished successfullyRead result field
FAILEDCalculation encountered an errorSurface failure to user

Polling strategy

Implement exponential backoff when polling to avoid excessive API calls:
async function pollPrequalification(
  id: string,
  maxAttempts: number = 30,
  initialDelayMs: number = 2000
): Promise<PrequalificationResult> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetchPrequalification(id);

    if (response.status === "SUCCEEDED") {
      return response.result;
    }

    if (response.status === "FAILED") {
      throw new Error("Prequalification failed");
    }

    // Exponential backoff: 2s, 4s, 8s, 16s, max 30s
    const delay = Math.min(initialDelayMs * Math.pow(2, attempt), 30000);

    await new Promise((resolve) => setTimeout(resolve, delay));
  }

  throw new Error("Prequalification polling timeout");
}
Polling intervals: Start with 2-5 second intervals and increase gradually. Most prequalifications complete within 10-30 seconds, but complex scenarios may take longer.

Understanding results

When the prequalification completes, the result field contains maximum affordability information:
{
  "data": {
    "preQualification": {
      "conventional": {
        "id": "prequal_abc123xyz",
        "status": "SUCCEEDED",
        "result": {
          "product": {
            "id": "prod_conforming_30yr_fixed",
            "description": "Conforming 30-Year Fixed",
            "type": "Conforming"
          },
          "loanAmount": 450000,
          "salesContractAmount": 562500,
          "ltv": 0.8,
          "rate": 6.5,
          "apr": 6.713,
          "discountPointsTotal": 0,
          "discountPointsTotalAmount": 0
        },
        "productBreakdown": [
          {
            "product": {
              "id": "prod_conforming_30yr_fixed",
              "description": "Conforming 30-Year Fixed",
              "type": "Conforming"
            },
            "result": {
              "loanAmount": 450000,
              "salesContractAmount": 562500,
              "ltv": 0.8,
              "rate": 6.5,
              "apr": 6.713
            }
          }
        ]
      }
    }
  }
}

Result fields

The result field is the best eligible result across all products:
FieldDescription
productThe loan product with the optimal prequalification result
loanAmountMaximum qualifying loan amount
salesContractAmountMaximum qualifying purchase price
ltvLoan-to-value ratio for the result
rateInterest rate for the result
aprAnnual percentage rate for the result
discountPointsTotalTotal discount points
discountPointsTotalAmountTotal discount points in dollars
The top-level result represents the best eligible product. Use the productBreakdown array to show borrowers per-product results, including ineligibility details when a product does not qualify.

Complete example

Here’s a complete TypeScript example that creates a prequalification and polls for results:
interface PrequalificationInput {
  fipsCountyCode: string;
  qualifyingFicoScore: number;
  qualifyingAssetsTotalAmount: number;
  qualifyingMonthlyIncome: number;
  qualifyingMonthlyExpenses: number;
  propertyUsageType?: "PRIMARY_RESIDENCE" | "SECOND_HOME" | "INVESTMENT";
  neighborhoodHousingType?: string;
  hoaDues?: number;
  maxDiscountPointsTotal?: number;
}

async function runPrequalification(
  input: PrequalificationInput,
  accessToken: string
): Promise<PrequalificationResult> {
  // Step 1: Create prequalification
  const createMutation = `
    mutation CreatePrequalification($input: ConventionalPreQualificationInput!) {
      preQualification {
        conventional(input: $input) {
          id
        }
      }
    }
  `;

  const createResponse = await fetch("https://pylon.mortgage/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({
      query: createMutation,
      variables: { input },
    }),
  });

  const { data: createData } = await createResponse.json();
  const prequalId = createData.preQualification.conventional.id;

  // Step 2: Poll for results
  const query = `
    query GetPrequalification($id: ID!) {
      preQualification {
        conventional(id: $id) {
          id
          status
          result {
            product {
              id
              description
              type
            }
            loanAmount
            salesContractAmount
            ltv
            rate
            apr
            discountPointsTotal
            discountPointsTotalAmount
          }
          productBreakdown {
            product {
              id
              description
              type
            }
            result {
              ... on EligiblePreQualification {
                loanAmount
                salesContractAmount
                ltv
                rate
                apr
              }
              ... on IneligiblePreQualification {
                details {
                  __typename
                }
              }
            }
          }
        }
      }
    }
  `;

  // Poll with exponential backoff
  for (let attempt = 0; attempt < 30; attempt++) {
    const pollResponse = await fetch("https://pylon.mortgage/graphql", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${accessToken}`,
      },
      body: JSON.stringify({
        query,
        variables: { id: prequalId },
      }),
    });

    const { data: pollData } = await pollResponse.json();
    const prequal = pollData.preQualification.conventional;

    if (prequal.status === "SUCCEEDED") {
      return prequal.result;
    }

    if (prequal.status === "FAILED") {
      throw new Error("Prequalification failed");
    }

    // Wait before next poll (exponential backoff)
    const delay = Math.min(2000 * Math.pow(2, attempt), 30000);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }

  throw new Error("Prequalification polling timeout");
}

Best practices

  1. Handle errors gracefully: When status is FAILED, surface a meaningful message to borrowers and offer to retry.
  2. Set reasonable timeouts: Most prequalifications complete within 30 seconds, but set a maximum timeout (e.g., 2-3 minutes) to avoid indefinite polling.
  3. Cache results: Prequalification results are valid for a period of time. Consider caching results with the input parameters as a key to avoid redundant calculations.
  4. Show product breakdown: Display results per product so borrowers can see which products offer the highest affordability.
  5. Explain limitations: Help borrowers understand that prequalification is an estimate and actual qualification may vary based on complete application details.
  6. Use for lead qualification: Prequalification is perfect for initial borrower conversations to quickly assess affordability before creating a full loan application.

Error handling

Common error scenarios and how to handle them:
Error CodeDescriptionResolution
INVALID_INPUTOne or more input fields are invalidValidate inputs before submission
GUIDELINE_VIOLATIONBorrower doesn’t meet minimum guidelinesExplain qualification requirements to borrower
RATE_UNAVAILABLENo rates available for the given parametersTry adjusting inputs or explain market conditions
TIMEOUTCalculation took too longRetry the prequalification request