> ## 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.

# Importing a MISMO file (Beta)

> Beta: create a new loan in one request by uploading a MISMO 3.4 or 3.6 XML file, including re-applications from an existing loan

<Warning>
  **This endpoint is in beta.** You can use it today, but its request and response contract may change while it is in beta, including in ways that are not backward compatible. That covers the response fields, the error codes and the import report. Read [What beta means for your integration](#what-beta-means-for-your-integration) before you build on it.
</Warning>

<Note>
  **MISMO import is turned on per organization.** To use it, contact your Pylon representative and ask for MISMO import to be enabled for your organization. Mention whether you need it in sandbox, production or both.
</Note>

The MISMO import endpoint turns a MISMO XML file into a new Pylon loan in a single request. It reads borrowers, income, assets, owned real estate, interested parties and the subject property out of the file. You don't have to recreate each of them with separate GraphQL mutations.

Common uses:

* **Bringing in a loan from another system**, such as a 1003 export from another loan origination system or an AUS submission file.
* **Starting a re-application from an existing Pylon loan.** Export the existing loan as a MISMO file, then import that file as a new loan. See [Re-applying from an existing loan](#re-applying-from-an-existing-loan).

A file rarely carries everything a loan needs. The import succeeds as long as the file describes a deal and a loan. Data the import read but couldn't use is listed in an import report, so you can fill it in afterwards. Some kinds of data are never imported. See [What gets imported](#what-gets-imported).

## At a glance

| | |
| - | - |
| **Endpoint** | `POST {BASE_URL}/api/mismo-import` |
| **Status** | Beta. Enabled per organization on request |
| **Authentication** | OAuth Bearer token with the `create:loan` scope |
| **Request body** | `multipart/form-data` with exactly one file in the `mismo-file` field |
| **Accepted files** | MISMO 3.4 or 3.6 XML, UTF-8 encoded, up to 50 MB |
| **Result** | Creates a **new** deal and loan, and returns the loan ID and an import report ID |
| **Idempotent** | No. Every successful call creates another loan |

## Environments

The endpoint exists in both environments, once it is enabled for your organization in that environment. Only the host differs:

| Environment | Base URL |
| - | - |
| Sandbox | `https://sandbox.pylon.mortgage` |
| Production | `https://pylon.mortgage` |

```javascript theme={null}
// Point this at sandbox while developing, production when you go live.
const BASE_URL = "https://sandbox.pylon.mortgage";
```

<Note>
  Access tokens are environment-specific. A sandbox token will not authenticate against production, and vice versa. See the [authentication](/guides/getting-started/authentication/overview) guide.
</Note>

## Before you import: borrower consent

<Warning>
  **Importing a file records credit consent for every borrower in it.** Each imported borrower is saved with electronic consent to both a hard and a soft credit pull, dated at the time of the import. This lets you run credit on the new loan right away.

  A MISMO file does not carry consent, so the import cannot check for it. Only import a file once you have each borrower's authorization to pull their credit for this application. This applies to re-applications too: consent given for the original loan does not carry over.
</Warning>

## Making the request

Send the file as `multipart/form-data` in a field named `mismo-file`. Send exactly one file per request. Do not set the `Content-Type` header yourself. Let your HTTP client set it, so that the multipart boundary is included.

| Part | Location | Required | Description |
| - | - | - | - |
| `Authorization` | Header | Yes | `Bearer YOUR_ACCESS_TOKEN`. The token needs the `create:loan` scope. |
| `mismo-file` | Body (multipart) | Yes | The MISMO XML file. Exactly one. |

The loan is created in the organization your access token belongs to. Nothing in the file can place it anywhere else.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://sandbox.pylon.mortgage/api/mismo-import" \
    -H "Authorization: Bearer $PYLON_ACCESS_TOKEN" \
    -F "mismo-file=@./loan-export.xml;type=application/xml"
  ```

  ```javascript Node.js 20+ theme={null}
  import { openAsBlob } from "node:fs";

  async function importMismoFile(path) {
    const formData = new FormData();
    formData.append(
      "mismo-file",
      await openAsBlob(path, { type: "application/xml" }),
      "loan-export.xml"
    );

    const response = await fetch(`${BASE_URL}/api/mismo-import`, {
      method: "POST",
      headers: { Authorization: `Bearer ${accessToken}` },
      body: formData,
      signal: AbortSignal.timeout(120_000),
    });

    // Error responses from a proxy or gateway may not be JSON
    const body = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new MismoImportError(response, body);
    }
    return body; // { loan_id, import_id, missing_fields }
  }

  class MismoImportError extends Error {
    constructor(response, body) {
      super(body.error ?? `MISMO import failed with HTTP ${response.status}`);
      this.status = response.status;
      this.code = body.code; // not present on every error, see "Errors"
      this.errorId = body.error_id;
      this.retryAfter = response.headers.get("Retry-After");
    }
  }
  ```

  ```python Python theme={null}
  import requests

  def import_mismo_file(path: str) -> dict:
      with open(path, "rb") as f:
          response = requests.post(
              f"{BASE_URL}/api/mismo-import",
              headers={"Authorization": f"Bearer {access_token}"},
              files={"mismo-file": ("loan-export.xml", f, "application/xml")},
              timeout=120,
          )
      try:
          body = response.json()
      except ValueError:
          body = {}
      if not response.ok:
          raise RuntimeError(
              f"{response.status_code} {body.get('code')}: {body.get('error')}"
          )
      return body  # {"loan_id": ..., "import_id": ..., "missing_fields": [...]}
  ```
</CodeGroup>

<Tip>
  Call this endpoint from your server, not from a browser. Your access token must never be exposed to client-side code. If your users upload files in a browser, send the file to your backend and forward it from there.
</Tip>

## What the file must contain

The import needs very little to succeed, but the file has to be a MISMO document of a version it reads.

* **The root element is `MESSAGE`.** A namespace prefix on it is fine. Any other root element is rejected as `MISMO_UPLOAD_PARSE_FAILURE`.
* **A supported version.** MISMO 3.4 and MISMO 3.6 are supported. The version is read from the `MISMOReferenceModelIdentifier` attribute on `MESSAGE`, or from `MISMOLogicalDataDictionaryIdentifier` when the first is absent. Identifiers such as `3.4.032420160128` or `3.6.0` are accepted. A file that declares another version, such as `3.5.021220160120`, is rejected as `MISMO_UPLOAD_UNSUPPORTED_VERSION`. A file that declares no version, or a version identifier that isn't in the usual `major.minor` form, is read as MISMO 3.4.
* **A deal.** The file needs a `DEAL` under `DEAL_SETS/DEAL_SET/DEALS`. Only the first deal in the file is imported. Any further deals are ignored without being reported, so send one deal per file.
* **One subject loan.** Inside the deal's `LOANS`, the loan with `LoanRoleType` set to `SubjectLoan` is imported. If no loan carries that role, the first `LOAN` is used. A deal with more than one `SubjectLoan` is rejected, because the import can't tell which loan you meant.
* **UTF-8 encoding.** The file is always read as UTF-8. A file saved in another encoding, such as Windows-1252, usually still imports, but accented or special characters in names and addresses turn into replacement characters (`�`). Convert the file to UTF-8 before you upload it.
* **No complex `DOCTYPE`.** A plain `<!DOCTYPE MESSAGE>` declaration is fine. A `DOCTYPE` with an external identifier or an internal subset is rejected, and so is extremely deep element nesting (`413`, `MISMO_UPLOAD_EXCESSIVE_NESTING`). Real MISMO exports don't use either.
* **50 MB or smaller** (52,428,800 bytes). Larger files are rejected with HTTP `413`.

Everything else is optional. Missing details, such as a borrower's email address or a property's county, don't fail the import. They are listed in the import report instead.

## What gets imported

| Imported from the file | Not imported |
| - | - |
| Loan terms, such as purpose and amounts | **Liabilities.** These come from the credit report when you pull credit. Add any debts the bureau won't report yourself (see below). |
| The subject property | **Credit scores and credit report data.** Pull credit on the new loan. |
| Borrowers, with contact details and address history | **Fees, closing costs, mortgage insurance, property tax and homeowner's insurance estimates.** These are produced when the loan is priced. |
| Employment and income | **Rate, points and product.** These are pricing results. |
| Assets | **The loan officer.** The new loan is unassigned. Assign an officer the same way you would on any other loan. |
| Owned real estate | **Documents.** Upload them separately. See [Working with documents](/guides/getting-started/documents). |
| Non-borrowing owners | |
| Interested parties, such as real estate agents, the seller or an attorney | |

**The primary borrower** is the borrower the file classifies as primary (`BorrowerClassificationType` of `Primary`). If no borrower is classified that way, the first borrower in the file is the primary borrower.

Some data is imported differently from how it appears in the file:

* **Owned real estate arrives with no mortgage attached.** Liabilities aren't imported, so every owned property looks owned free and clear until you link its mortgage. When the file shows a lien on an owned property, the import report flags it as `CRITICAL`. After you pull credit, link each mortgage to its property with the `attachOwnedPropertyLiability` mutation. See [Property-associated liabilities](/entity-models/liabilities#property-associated-liabilities).
* **Self-reported debts are not carried over.** Debts a credit bureau may not report, such as an HOA lien, garnishments, delinquent taxes or a private personal loan, must be added by hand. Otherwise DTI is understated. See [Managing self-reported liabilities](/entity-models/liabilities#managing-self-reported-liabilities).
* **Addresses without a residency basis are left out.** An address in a borrower's history that doesn't state whether the borrower owned, rented or lived rent-free is not imported, and it is listed in the report. Add it back on the borrower.
* **Records that can't be attached are skipped.** For example, an asset or a rental income that isn't linked to any borrower, a non-borrowing owner with no name, or a party in a role Pylon doesn't track. Each one is listed in the report.
* **A few MISMO 3.6 values can't be imported yet.** Each one is listed in the report with the element's name from the file.
* **Missing names and email addresses read as empty strings.** When the file has no first name, last name or email address for a borrower, GraphQL returns `""` for that field, not `null`.

## Successful response

A successful import returns HTTP `201 Created` with a JSON body:

```json theme={null}
{
  "loan_id": "app_3vXq9TzK7mRbN2wLpYc8Hd",
  "import_id": "mimp_5Rk2WcQ8vNtLx3HbYp9JzA",
  "missing_fields": [
    {
      "entity": "SubjectProperty",
      "field": "address.county_fips",
      "ref": null,
      "severity": "RECOMMENDED",
      "value": null,
      "reason": "ABSENT",
      "message": null
    }
  ]
}
```

<Tip>
  Treat any `2xx` status as success rather than checking for `201` exactly.
</Tip>

| Field | Type | Description |
| - | - | - |
| `loan_id` | `string` | The ID of the new loan. Use it anywhere the GraphQL API takes a loan ID, for example `loan(id:)`. Treat it as an opaque string. |
| `import_id` | `string` | The ID of this import's report. Read the report with the `mismoImport` GraphQL query. See [The import report](#the-import-report). Treat it as an opaque string. |
| `missing_fields` | `array` | **Deprecated.** The same findings as the import report, in an older format. It will be removed. New integrations should read the import report instead. See [Legacy `missing_fields`](#legacy-missing-fields). |

### Reading the new loan

Use `loan_id` with the GraphQL API to read what was imported:

```graphql theme={null}
query ImportedLoan($loanId: ID!) {
  loan(id: $loanId) {
    id
    dealId
    currentStage
    borrowers(first: 20) {
      edges {
        node {
          id
          personalInformation {
            firstName
            lastName
            email
          }
        }
      }
    }
  }
}
```

## The import report

The import report lists the data the import read from the file but couldn't use. Data listed under "Not imported" in [What gets imported](#what-gets-imported) is dropped without being reported. Each finding points to the Pylon record it concerns, so you can send your user straight to the right borrower, asset or property to fix it. The import never fails because of these findings.

Read it with the `mismoImport` query, using the `import_id` from the response. It needs a token with the `read:loan` scope. You can also reach it from the loan, as `loan { mismoImport { ... } }`.

<Warning>
  **The report is kept for 24 hours.** After that, `mismoImport` returns `null` and `loan.mismoImport` is `null`. If you need the findings for longer, read the report soon after the import and store what you need. A `null` result can also mean the ID is unknown or belongs to a loan your token can't read. These cases are deliberately indistinguishable.
</Warning>

```graphql theme={null}
query MismoImportReport($importId: ID!) {
  mismoImport(id: $importId) {
    id
    importedAt
    loan {
      id
    }
    nonAdoptions {
      __typename
      id
      reason
      severity
      source {
        entity
        label
        position
        rawValue
      }
      ... on MismoFieldNonAdoption {
        entity
        record {
          id
        }
        fields {
          column
          valueKind
          values {
            ... on IntakeInlineValues {
              values
            }
            ... on IntakeValuesRef {
              keyedOn
            }
          }
        }
      }
      ... on MismoUnrepresentableElement {
        element
        record {
          id
        }
      }
    }
  }
}
```

Each item in `nonAdoptions` is one finding, in the order the import met it in the file. Switch on `__typename`:

* **`MismoFieldNonAdoption`**: data that wasn't imported for a record. When `record` is set, the record exists and one of its fields (`fields`) is empty or needs correcting. When `record` is `null`, the record was never created, and `entity` tells you what kind of record to create, for example a borrower's income.
* **`MismoUnrepresentableElement`**: a MISMO 3.6 element that Pylon can't store yet. `element` is the element's name as it appears in the file. There's nothing on the loan to fix, but the data from the file was lost.

| Field | Description |
| - | - |
| `id` | Stable for the life of the report. Two reads return the same `id` for the same finding. |
| `severity` | How much the finding matters. See [Severity](#severity). |
| `reason` | Why the data wasn't imported. See [Reason](#reason). |
| `entity` | The kind of record: `LOAN_APPLICATION` (the loan), `SUBJECT_PROPERTY`, `BORROWER`, `INCOME`, `ASSET`, `OWNED_PROPERTY`, `NON_BORROWING_OWNER` or `PARTY`. |
| `record` | The record the finding concerns, as a `Node`. Use its `id` with the matching GraphQL query or mutation. `null` when no record was created, or when the record has since been deleted. |
| `fields` | The fields left empty, each with a `column` identifier, the kind of value it takes (`valueKind`, for example `MONEY`, `DATE`, `ENUM` or `EMAIL`) and, for choice fields, the permitted `values`. Short lists of choices come inline (`IntakeInlineValues`). Long lists, such as counties, come as a reference to the query that supplies them (`IntakeValuesRef`), with `keyedOn` naming the field they depend on, such as the state. It is empty when the finding is about a whole record. |
| `source` | Where the finding came from in the file. `entity` and `position` (counting from zero) locate the element, `label` is the file's own label for it when it has one, and `rawValue` shows what the file gave when it gave a value that couldn't be used. For choice fields, such as marital status or asset type, it is the file's own word. For personal values it is a placeholder such as `Unreadable date of birth`, never the value itself. |

<Note>
  Treat the report as loan data. Show it only to people who are allowed to see the loan, and keep it out of your logs.
</Note>

### Severity

| Value | Meaning |
| - | - |
| `CRITICAL` | The record was created, but this value silently changes how the loan's DTI is calculated until someone fixes it. Review these before you price. Examples: a borrower whose address history doesn't resolve to exactly one current address, and an owned property the file shows a lien on. |
| `RECOMMENDED` | The record was created, but this value is empty. Fill it in. |
| `OPTIONAL` | The record was never created. Check `reason` to see whether the file had no such section or the import skipped it. |

### Reason

| Value | Meaning |
| - | - |
| `ABSENT` | The file didn't include this value or section. |
| `UNREADABLE` | The file gave a value Pylon doesn't recognize, so nothing was saved for the field. `fields[].values` lists the values you can choose from. |
| `DEFAULTED` | The file gave a value Pylon doesn't recognize. The record was still created, with a substitute value, such as an asset type of `OTHER`. Check the substitute. |
| `ASSUMED` | The file gave nothing usable, and pricing will assume a value until you set one. These findings are usually `CRITICAL`. |
| `INCOMPLETE` | The section was in the file but had too little information to create the record, such as a property owner with no name. |
| `UNLINKED` | The section was in the file but isn't connected to any borrower, such as an asset or a rental income no borrower owns. Add it on the right borrower to bring it onto the loan. |
| `OUT_OF_SCOPE` | The section was in the file, but Pylon doesn't import that kind of data, such as a party in a role Pylon doesn't track. Add it by hand if it matters. |
| `NOT_REPRESENTABLE` | A MISMO 3.6 value Pylon can't store yet. |

<Note>
  The `reason` enum is shared with other Pylon features and contains more values than the ones above. Treat any value you don't recognize as "review this finding".
</Note>

### Legacy missing fields

`missing_fields` in the upload response carries the same findings in an older, flatter format. It is deprecated. Use the import report for new integrations.

| Field | Type | Description |
| - | - | - |
| `entity` | `string` | `LoanApplication`, `SubjectProperty`, `Borrower`, `Income`, `Asset`, `OwnedProperty`, `NonBorrowingOwner` or `Party`. |
| `field` | `string \| null` | Identifies the value within the record. `null` when `severity` is `OPTIONAL`. |
| `ref` | `string \| null` | The record's position in the file, such as `borrower[1]`, the second borrower, counting from zero. Not a Pylon ID. Income findings use the borrower's position, and party positions carry a role, such as `party[2]:RealEstateAgent`. `null` when only one record of that kind can exist, and for MISMO 3.6 values that can't be stored. |
| `severity` | `string` | `CRITICAL`, `RECOMMENDED` or `OPTIONAL`, as in the report. |
| `reason` | `string` | `ABSENT`, `UNREADABLE`, `DEFAULTED`, `ASSUMED`, `NOT_REPRESENTABLE` or `SKIPPED`. `SKIPPED` covers what the report splits into `INCOMPLETE`, `UNLINKED` and `OUT_OF_SCOPE`. |
| `value` | `string \| null` | What the file gave, when it gave a value that couldn't be used. As with `rawValue`, personal values are replaced by a placeholder. |
| `message` | `string \| null` | A human-readable explanation, when one is available. |

<Warning>
  **Don't parse `field` or `ref`.** Their format may change while the endpoint is in beta. Use them for display only.
</Warning>

### Common follow-ups after an import

Some gaps block later steps, so check for these first:

* **Every `CRITICAL` finding.** Each one changes how DTI is calculated until it's resolved.
* **The subject property's address and county.** A loan can't be priced without a complete subject property address, including its county.
* **Each borrower's email address.** Borrowers without one can't receive disclosures.
* **Credit.** No credit data is imported, so pull credit before you price the loan. Then link mortgages to owned properties.

## Re-applying from an existing loan

When a borrower needs a new application for a loan they've already started with you, for example to move to a different property or program, or to restart a file that was withdrawn, you can start the new loan from the old one. Export the existing loan as a MISMO file, then import that file. The borrowers, income, assets, owned real estate and property details come across without being retyped.

<Steps>
  <Step title="Export the existing loan">
    Call the `export` mutation on the existing loan. It needs a token with the `update:loan` scope, and it returns a temporary download URL for a MISMO 3.4 file of the loan.

    ```graphql theme={null}
    mutation ExportLoan($loanId: ID!) {
      loan {
        export(input: { loanId: $loanId }) {
          downloadUrl
        }
      }
    }
    ```

    Exporting doesn't change the existing loan. It works even on a loan that can no longer be edited.

    If the export fails, the GraphQL error carries `extensions.errorDetails.code` of `LOAN_OPERATION_ERROR`. A loan created a moment ago isn't available for export yet, so retry a few times with backoff. If it keeps failing, contact Pylon support with the error's `errorId`.
  </Step>

  <Step title="Download the file">
    Fetch `downloadUrl` with a plain `GET`. Don't send your `Authorization` header with this request, because the URL already carries its own authorization.

    <Warning>
      **Treat `downloadUrl` as a secret.** Anyone who has the URL can download the file, which contains the borrowers' full personal information. Download it right away, and don't log, store or share the URL. It stops working after at most three days.
    </Warning>
  </Step>

  <Step title="Get the borrowers' authorization">
    Importing records new credit consent for every borrower on the new loan. Make sure each borrower has authorized a credit pull for the new application. See [Before you import: borrower consent](#before-you-import-borrower-consent).
  </Step>

  <Step title="Import the file">
    Upload the file to `POST /api/mismo-import` as described in [Making the request](#making-the-request). The import creates a new deal and a new loan. Save the new `loan_id` and `import_id`.
  </Step>

  <Step title="Review the import report and finish the new loan">
    Read the [import report](#the-import-report) and work through the [common follow-ups](#common-follow-ups-after-an-import). Then make the changes that are the reason for the re-application, such as a new subject property, on the new loan with the usual GraphQL mutations.
  </Step>
</Steps>

```javascript theme={null}
async function reapply(existingLoanId) {
  // 1. Export the existing loan
  const exportResult = await graphql(
    `mutation ExportLoan($loanId: ID!) {
      loan { export(input: { loanId: $loanId }) { downloadUrl } }
    }`,
    { loanId: existingLoanId }
  );
  // `graphql` is your GraphQL client call; it must throw when the response has `errors`
  const downloadUrl = exportResult.loan?.export?.downloadUrl;
  if (!downloadUrl) {
    throw new Error("Export returned no download URL");
  }

  // 2. Download the file (no Authorization header)
  const fileResponse = await fetch(downloadUrl);
  if (!fileResponse.ok) {
    throw new Error(`Download failed: ${fileResponse.status}`);
  }
  const file = new Blob([await fileResponse.arrayBuffer()], {
    type: "application/xml",
  });

  // 3. Confirm borrower authorization for the new application before this point

  // 4. Import it as a new loan
  const formData = new FormData();
  formData.append("mismo-file", file, "reapplication.xml");
  const importResponse = await fetch(`${BASE_URL}/api/mismo-import`, {
    method: "POST",
    headers: { Authorization: `Bearer ${accessToken}` },
    body: formData,
    signal: AbortSignal.timeout(120_000),
  });
  const body = await importResponse.json().catch(() => ({}));
  if (!importResponse.ok) {
    throw new Error(`${importResponse.status} ${body.code ?? ""}: ${body.error}`);
  }

  // 5. Review the report, then finish the new loan
  return { newLoanId: body.loan_id, importId: body.import_id };
}
```

**What the new loan does not inherit from the existing one:**

* **Credit.** The credit report, scores and liabilities stay on the existing loan. Pull credit again on the new loan, and link mortgages to owned properties afterwards.
* **Pricing, rate lock and disclosures.** Price the new loan from scratch. A rate lock on the existing loan doesn't transfer.
* **Documents, tasks and conditions.** Upload the documents the new loan needs. See [Working with documents](/guides/getting-started/documents).
* **The loan officer.** Assign one on the new loan.
* **Anything the import doesn't read.** The export contains data the import drops, such as the existing loan's liabilities. Expect a `CRITICAL` finding for each owned property the export shows a mortgage on, until you link the mortgage on the new loan.

**The existing loan stays as it is.** Importing doesn't withdraw, archive or otherwise change it. Close it out according to your own process, so that the borrower doesn't have two active applications.

<Tip>
  Make your changes on the new loan after the import, rather than by editing the exported XML. Editing MISMO XML by hand is easy to get wrong, and a malformed file is rejected.
</Tip>

## Errors

Error responses have a JSON body with this shape:

```json theme={null}
{
  "code": "MISMO_UPLOAD_UNSUPPORTED_VERSION",
  "error": "MISMO file declares reference model 3.5.021220160120; this endpoint imports MISMO 3.4 and 3.6",
  "error_id": "errx_7JpQ2nVb4KxRt9WmZc3LsA",
  "error_time": "2026-10-02T17:04:11.512Z"
}
```

| Field | Description |
| - | - |
| `code` | A stable, machine-readable code. It is present on the errors listed in the tables below that show one, and absent on others, such as `401` and `403`. Branch on `code` when it is present, and on the HTTP status otherwise. |
| `error` | A human-readable description. Don't parse it, because its wording may change. On a parse failure it can quote text from the file, so show it only to the person who uploaded the file and keep it out of your logs. |
| `error_id` | A unique ID for this failure. Include it when you contact Pylon support. |
| `error_time` | When the error occurred, as an ISO 8601 timestamp. |
| `errorDetails` | Optional extra detail. On `MISMO_UPLOAD_PARSE_FAILURE`, when the problem can be located, it holds `xmlErrorKind` and `xmlErrorRawMessage`, plus `xmlErrorLine` and `xmlErrorColumn` when the line is known. All values are strings. Note the camelCase name. |

**A failed import creates nothing.** No deal, loan, borrower or report is left behind when the request returns an error. You can fix the file and send it again without cleaning up first.

### Import error codes

| HTTP | `code` | Cause | What to do |
| - | - | - | - |
| `400` | `MISMO_UPLOAD_INVALID_FILE_COUNT` | The request had no file, or more than one file, in the `mismo-file` field. | Send exactly one file per request. |
| `400` | `MISMO_UPLOAD_PARSE_FAILURE` | The file isn't well-formed XML, its root element isn't `MESSAGE`, or its content doesn't follow the MISMO structure. | Check that you're sending the MISMO XML export itself, not a PDF or ZIP. Use `errorDetails` to find the problem, or re-export the file from the source system. |
| `400` | `MISMO_UPLOAD_UNSUPPORTED_VERSION` | The file declares a MISMO version other than 3.4 or 3.6. The `error` text names the declared version. | Re-export the file as MISMO 3.4 or 3.6. The file itself may be valid. |
| `400` | `MISMO_UPLOAD_MISSING_DEAL` | The file contains no `DEAL`. | Export a file that contains a loan application, not only party or property data. |
| `400` | `MISMO_UPLOAD_MISSING_LOAN` | The deal contains no `LOAN`. | Export a file that includes the loan. |
| `400` | `MISMO_UPLOAD_DUPLICATE_SUBJECT_LOAN` | More than one loan in the deal has `LoanRoleType` set to `SubjectLoan`. | Export a file with a single subject loan. |
| `413` | `MISMO_UPLOAD_EXCESSIVE_NESTING` | The file's elements are nested far deeper than any real MISMO export, or it declares a `DOCTYPE` with an external identifier or internal subset. | Re-export the file from the source system. Don't add a `DOCTYPE`. |
| `429` | `RATE_LIMITED` | Too many requests. The limit is counted per organization and is shared with most of your other API calls, not only imports. | Wait for the number of seconds in the `Retry-After` header, then retry. |
| `500` | `MISMO_UPLOAD_INTERNAL` | The import failed for a reason unrelated to the file. | Nothing was created, so retry the same file with backoff. If it keeps failing, contact support with the `error_id`. |
| `503` | `MISMO_UPLOAD_AT_CAPACITY` | Imports are temporarily at capacity. | Nothing was created. Retry after a short wait, with exponential backoff. |

### Other errors

These come from the request itself rather than from the file. They carry no `code`.

| HTTP | Cause | What to do |
| - | - | - |
| `400` | The multipart body is malformed, or the file was sent in a field other than `mismo-file`. The `error` text names the unexpected field, for example `Unexpected field - file`. | Send the file in the `mismo-file` field, and let your HTTP client build the multipart body. |
| `401` | The access token is missing, invalid or expired, or belongs to the other environment. | Request a new token for this environment. |
| `403` | The token lacks the `create:loan` scope, or your organization's API access is paused or inactive. | Check the token's scopes. If the scopes are right, contact your Pylon representative. |
| `413` | The file is larger than 50 MB. The `error` text is `File too large`. | Reduce the file size. If the export embeds documents, re-export it without them. |

Responses to authenticated requests carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (a Unix timestamp in seconds) and `X-RateLimit-Policy` headers, so you can slow down before you hit the limit. `401` and `403` responses don't carry them.

<Note>
  **Send a multipart request.** The endpoint only accepts `multipart/form-data`. A request with any other body, such as raw XML or JSON, currently fails with `500 MISMO_UPLOAD_INTERNAL` rather than a `400`. If every request fails that way, check how the request body is built first.
</Note>

## Retries and duplicates

The endpoint is **not idempotent**. Every successful request creates a new deal and loan, even when the file is identical to one you've already imported. There is no idempotency key.

* **After `429`, `500` or `503`**, retry with backoff. Honor `Retry-After` when it is present. Nothing was created by the failed request.
* **After a `400` or `413`**, don't retry until you've changed the file or the request. They fail the same way every time.
* **After a timeout or dropped connection with no response**, the import may still have completed. Before you retry, check whether the loan was already created, for example by listing your most recent deals. Otherwise you may end up with duplicates.
* **Never retry after a success.** A second call creates a second loan.

Large files take longer to import than most API calls. A request can wait up to 20 seconds for a free import slot (after which it gets `503 MISMO_UPLOAD_AT_CAPACITY`), and reading the file is stopped after 45 seconds (`500 MISMO_UPLOAD_INTERNAL`). Saving the loan comes after that. Set a client timeout comfortably above the total. The examples above use 120 seconds.

## What beta means for your integration

While the endpoint is in beta:

* **The contract may change, including in ways that aren't backward compatible.** This covers the response fields, the error codes and the import report. Check the [changelog](/changelog) for updates.
* **`missing_fields` will be removed.** Build on `import_id` and the import report instead.
* **New error codes and report values may be added.** Handle an unknown `code` by its HTTP status: an unknown `4xx` code means the file or request was rejected, and an unknown `5xx` code means a failure on Pylon's side. Handle an unknown `reason` or `severity` as "review this finding".
* **New fields may be added to responses.** Ignore fields you don't recognize instead of failing on them.
* **Fewer findings over time.** As more of the MISMO format is imported, files that produce findings today will produce fewer. Don't hard-code a list of expected findings.
* **Review imported loans.** Check imported data before you rely on it for pricing or disclosures, especially for files from a source system you haven't imported from before.

If an import produces a loan that doesn't match its file, or rejects a file you believe is valid, contact Pylon support with the `loan_id`, `import_id` or `error_id`. Don't send the file itself unless support asks for it, because MISMO files contain borrowers' personal information.

## Related

<CardGroup cols={2}>
  <Card title="Complete integration guide" icon="route" href="/guides/getting-started/e2e-build">
    Build a loan field by field with GraphQL instead, and see the steps that follow loan creation.
  </Card>

  <Card title="Working with documents" icon="folder-open" href="/guides/getting-started/documents">
    Upload the loan's supporting documents after the import.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.