Skip to main content
Credit pulls are a critical component of the loan origination process. Pylon requires explicit consent before pulling credit, and the consent type must match the type of credit pull (SOFT or HARD). Understanding the difference between these pull types and when to use each is essential for protecting borrowers and avoiding trigger leads.

Understanding SOFT vs. HARD pulls

The choice between SOFT and HARD credit pulls has significant implications for borrower experience and deal retention.

SOFT pulls

A SOFT pull (also called a “soft inquiry”):

No credit impact

Does not affect the borrower’s credit score

No trigger leads

Does not generate trigger leads to other lenders

Pre-qualification

Suitable for pre-qualification and initial pricing

Borrower-friendly

Better borrower experience with no unwanted solicitations
Use SOFT pulls when:
  • Running pre-qualification
  • Getting initial pricing scenarios
  • Pulling credit before the loan is in underwriting stage
  • Borrower is still shopping or comparing options

HARD pulls

A HARD pull (also called a “hard inquiry”):

Credit impact

May slightly lower credit score (typically 5-10 points)

Trigger leads

Generates trigger leads, resulting in solicitations from competing lenders

Underwriting

Required for final underwriting and loan approval

High intent

Best used when borrower is committed to proceeding
Use HARD pulls when:
  • Loan is in underwriting stage
  • Borrower has high intent and is committed to closing
  • Final credit verification is required for approval
Trigger leads can lose you the deal: HARD pulls generate trigger leads that notify competing lenders about the borrower’s mortgage application. These lenders will immediately contact the borrower with competing offers. This can:
  • Annoy borrowers with unwanted calls and emails
  • Lead to price shopping and deal loss
  • Damage borrower trust in your relationship
Always use SOFT pulls early in the process to avoid trigger leads until the borrower is committed to closing with you.

Sandbox environment limitations

Sandbox only supports HARD pulls: When building end-to-end testing scenarios in sandbox, you must use HARD pulls. SOFT pulls will not return any credit data or create liabilities. This limitation only applies to sandbox. Production supports both SOFT and HARD pulls.
You MUST obtain consent from borrowers before pulling credit. The consent type must exactly match the type of credit pull (SOFT or HARD). Pulling credit without proper consent is a compliance violation. Before pulling credit, set the borrower’s credit pull consent:
mutation {
  borrower {
    updateConsent(
      input: {
        borrowerId: "borrower_abc123"
        softCreditConsentType: ELECTRONIC
        softCreditConsentDate: "2024-01-15T10:00:00Z"
        # For HARD pulls, set hardCreditConsentType / hardCreditConsentDate instead
      }
    ) {
      borrowerId
      softCreditConsentType
      softCreditConsentDate
    }
  }
}
The consent type (ELECTRONIC, VERBAL, or WRITTEN) records how the borrower granted consent. Set the soft consent fields before a SOFT pull and the hard consent fields before a HARD pull.

Credit pull workflow

Credit pulls are asynchronous operations. Here’s the complete workflow:
1

Obtain consent

Set the borrower’s credit pull consent with the appropriate type (SOFT or HARD).
2

Initiate credit pull

Trigger the credit pull mutation with the consent type.
3

Receive job ID

The mutation returns a job ID for tracking the credit pull status.
4

Poll for completion

Poll the credit pull status until it completes. See Tracking Loan Updates for polling implementation.
5

Automatic liability creation

Upon successful credit pull, Pylon automatically creates liabilities on your behalf. You do not need to create these manually.

Initiating a credit pull

mutation {
  credit {
    startIndividualCreditPullJob(
      input: {
        borrower: {
          id: "borrower_abc123"
          consentType: ELECTRONIC
        }
        pullType: SOFT # or HARD
      }
    ) {
      job {
        id
        status
      }
      userErrors
    }
  }
}

Polling for credit results

Use the credit pull job id to poll for status. When the job status is SUCCEEDED, the result field is populated with the credit report:
query {
  credit {
    job(id: "credit_pull_job_abc123") {
      id
      status
      result {
        id
        pullTime
        expiryStatus
        bureauStatuses {
          bureau
          fileReturned
          resultStatus
        }
      }
    }
  }
}
Polling frequency: For active credit pulls, poll every 30 seconds to 2 minutes. Most credit pulls complete within 2-5 minutes.

Waterfall approach

Pylon uses a waterfall approach for credit pulls, meaning if one vendor is unavailable, the system automatically falls back to backup vendors.

Primary vendor

System attempts primary credit vendor first

Automatic fallback

If primary vendor is down, automatically tries backup vendors

High reliability

Ensures credit pulls succeed even during vendor outages
Why this matters: Credit is a critical piece of the upfront origination process and is required in many cases for loan sale. The waterfall approach ensures high reliability and availability.

Automatic liability creation

No manual work required: Upon successful credit pull, Pylon automatically creates liabilities on your behalf based on the credit report. You do not need to manually create these liabilities.
After a credit pull completes successfully, query the loan to see the automatically created liabilities:
query {
  loan(id: "loan_abc123") {
    borrowers {
      liabilities {
        edges {
          node {
            id
            type
            accountIdentifier
            monthlyPayment
            balance
            bankName
          }
        }
      }
    }
  }
}

Best practices

  1. Always get consent first: Never pull credit without explicit consent from the borrower.
  2. Match consent type: Ensure the consent type exactly matches the credit pull type.
  3. Use SOFT pulls early: For loan applications before underwriting, always use SOFT pulls to avoid trigger leads.
  4. Use HARD pulls late: Only use HARD pulls when the loan is in underwriting and the borrower is committed to closing.
  5. Poll appropriately: Use appropriate polling intervals (30 seconds to 2 minutes) for active credit pulls.
  6. Handle errors gracefully: If a credit pull fails, check the error message and retry if appropriate.

Agent prompts

When building loan applications, you must first obtain and set credit pull consent for the borrower before attempting to pull credit. The consent type (SOFT or HARD) must match the type of credit pull you intend to perform. Never pull credit without proper consent.

If you're building a loan application and pulling credit before the loan is in the underwriting stage, always use a SOFT pull to avoid trigger leads. Trigger leads from HARD pulls can annoy borrowers and may result in losing the deal due to price shopping. Only use HARD pulls when the loan is in underwriting and the borrower is committed to closing.

Example: complete credit pull flow

Here’s a complete TypeScript example:
async function pullCredit(
  loanId: string,
  borrowerId: string,
  pullType: "SOFT" | "HARD",
  accessToken: string
): Promise<CreditReport> {
  // Step 1: Set consent
  const setConsentMutation = `
    mutation SetCreditConsent(
      $borrowerId: ID!
      $consentType: CreditPullConsentType!
      $consentDate: DateTime!
    ) {
      borrower {
        updateConsent(
          input: {
            borrowerId: $borrowerId
            softCreditConsentType: $consentType
            softCreditConsentDate: $consentDate
          }
        ) {
          borrowerId
          softCreditConsentType
          softCreditConsentDate
        }
      }
    }
  `;

  await fetch("https://pylon.mortgage/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({
      query: setConsentMutation,
      variables: {
        borrowerId,
        consentType: "ELECTRONIC",
        consentDate: new Date().toISOString(),
      },
    }),
  });

  // Step 2: Initiate credit pull
  const pullCreditMutation = `
    mutation PullCredit($input: StartIndividualCreditPullJobInput!) {
      credit {
        startIndividualCreditPullJob(input: $input) {
          job {
            id
            status
          }
          userErrors
        }
      }
    }
  `;

  const pullResponse = await fetch("https://pylon.mortgage/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({
      query: pullCreditMutation,
      variables: {
        input: {
          borrower: { id: borrowerId, consentType: "ELECTRONIC" },
          pullType,
        },
      },
    }),
  });

  const { data: pullData } = await pullResponse.json();
  const creditPullJobId = pullData.credit.startIndividualCreditPullJob.job.id;

  // Step 3: Poll for completion
  const query = `
    query GetCreditPull($id: ID!) {
      credit {
        job(id: $id) {
          id
          status
          result {
            id
            pullTime
            expiryStatus
            bureauStatuses {
              bureau
              fileReturned
              resultStatus
            }
          }
        }
      }
    }
  `;

  // Poll with exponential backoff
  for (let attempt = 0; attempt < 20; 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: creditPullJobId },
      }),
    });

    const { data: pollData } = await pollResponse.json();
    const creditJob = pollData.credit.job;

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

    if (creditJob.status === "FAILED") {
      throw new Error("Credit pull failed");
    }

    // Wait before next poll (30 seconds to 2 minutes)
    const delay = Math.min(30000 * (attempt + 1), 120000);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }

  throw new Error("Credit pull polling timeout");
}