Skip to main content
GraphQL is designed for structured data queries and mutations, but it doesn’t natively support file uploads or downloads. To handle documents (PDFs, images, and other files), Pylon provides REST endpoints that work alongside the GraphQL API.

Why REST for documents?

GraphQL operates with JSON payloads and doesn’t have built-in support for binary file data. For document operations, Pylon uses REST endpoints that:
  • Support multipart/form-data for file uploads
  • Provide signed URLs for secure document downloads
  • Handle large file transfers efficiently
  • Integrate seamlessly with GraphQL for metadata

Document upload workflow

The typical workflow for uploading documents involves both GraphQL and REST:
1

Request upload URL via GraphQL

Use the GraphQL API to request a document upload URL. This returns a REST endpoint URL where you’ll upload the file.
2

Upload file to REST endpoint

Use the returned URL to upload your file(s) using a multipart/form-data POST request.
3

Query document metadata

Once uploaded, query document metadata through GraphQL to verify the upload and get download URLs.

Requesting an upload URL

Use the requestDocUpload mutation to get an upload URL:
mutation RequestDocumentUpload($input: DocumentUploadInput!) {
  requestDocUpload(input: $input) {
    url
  }
}
Variables:
{
  "input": {
    "entityType": "BORROWER",
    "entityId": "borrower_123",
    "documentTypeId": "doc_type_456",
    "documentTypeName": "W-2"
  }
}
Response:
{
  "data": {
    "requestDocUpload": {
      "url": "/api/document-upload/upload_request_789"
    }
  }
}

Uploading documents

Once you have the upload URL from GraphQL, upload your file(s) to the REST endpoint:
async function uploadDocument(uploadUrl, file) {
  const formData = new FormData();
  formData.append("document-files", file);

  const response = await fetch(`https://pylon.mortgage${uploadUrl}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
    body: formData,
  });

  if (!response.ok) {
    throw new Error(`Upload failed: ${response.statusText}`);
  }

  return response.json();
}

Uploading multiple files

You can upload multiple files in a single request:
async function uploadMultipleDocuments(uploadUrl, files) {
  const formData = new FormData();
  files.forEach((file) => {
    formData.append("document-files", file);
  });

  const response = await fetch(`https://pylon.mortgage${uploadUrl}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
    body: formData,
  });

  return response.json();
}

Complete example

Here’s a complete example that combines both steps:
async function uploadDocumentForBorrower(borrowerId, documentTypeName, file) {
  // Step 1: Get upload URL from GraphQL
  const mutation = `
    mutation RequestDocumentUpload($input: DocumentUploadInput!) {
      requestDocUpload(input: $input) {
        url
      }
    }
  `;

  const variables = {
    input: {
      entityType: "BORROWER",
      entityId: borrowerId,
      documentTypeName: documentTypeName,
    },
  };

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

  const { data } = await gqlResponse.json();
  const uploadUrl = data.requestDocUpload.url;

  // Step 2: Upload file to REST endpoint
  const formData = new FormData();
  formData.append("document-files", file);

  const uploadResponse = await fetch(`https://pylon.mortgage${uploadUrl}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
    body: formData,
  });

  if (!uploadResponse.ok) {
    throw new Error(`Upload failed: ${uploadResponse.statusText}`);
  }

  return uploadResponse.json();
}

Downloading documents

The workflow for downloading documents involves querying GraphQL to get signed download URLs:
1

Query document metadata via GraphQL

Query the loan application or borrower to get document metadata, including signed download URLs.
2

Use the download URL

Use the signed URL from the documentLink.url field to download the document file.
3

Handle the file

Process the downloaded file (save, display, etc.) and handle URL expiration if needed.

Querying document metadata

Query documents through GraphQL to get download URLs:
query GetLoanDocuments($loanApplicationId: ID!) {
  loanApplication(id: $loanApplicationId) {
    id
    documents {
      id
      name
      category
      uploadedAt
      documentLink {
        url
        title
        uploadStatus
      }
    }
  }
}
The documentLink.url field contains a signed URL for downloading the document. The uploadStatus indicates whether the upload is COMPLETE, UPLOADING, or FAILED.

Using download URLs

Download URLs are signed URLs that expire after a set time (usually 1 hour). Use them directly:
async function downloadDocument(document) {
  // Check if upload is complete
  if (document.documentLink.uploadStatus !== "COMPLETE") {
    throw new Error("Document upload is not complete yet");
  }

  const response = await fetch(document.documentLink.url, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });

  if (!response.ok) {
    throw new Error(`Download failed: ${response.statusText}`);
  }

  const blob = await response.blob();
  
  // Create download link
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = document.name;
  a.click();
  window.URL.revokeObjectURL(url);
  
  return blob;
}

Complete download example

Here’s a complete example that combines querying and downloading:
async function downloadLoanDocuments(loanApplicationId) {
  // Step 1: Query documents via GraphQL
  const query = `
    query GetLoanDocuments($loanApplicationId: ID!) {
      loanApplication(id: $loanApplicationId) {
        id
        documents {
          id
          name
          category
          uploadedAt
          documentLink {
            url
            title
            uploadStatus
          }
        }
      }
    }
  `;

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

  const { data } = await gqlResponse.json();
  const documents = data.loanApplication.documents;

  // Step 2: Download each document
  for (const doc of documents) {
    if (doc.documentLink.uploadStatus === "COMPLETE") {
      const response = await fetch(doc.documentLink.url, {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });

      const blob = await response.blob();
      const url = window.URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = doc.name;
      a.click();
      window.URL.revokeObjectURL(url);
    }
  }

  return documents;
}

Authentication

All REST endpoints require the same OAuth Bearer token authentication as GraphQL requests:
Authorization: Bearer YOUR_ACCESS_TOKEN
See the GraphQL authentication guide for details on obtaining and managing access tokens.

File requirements

  • Supported file types: PDF (.pdf), images (.jpg, .jpeg, .png, .gif), Microsoft Office (.doc, .docx, .xls, .xlsx), text files (.txt)
  • Maximum file size: 50 MB per file
  • Maximum total upload size: 200 MB per request

Error handling

async function uploadDocumentWithErrorHandling(uploadUrl, file) {
  try {
    const formData = new FormData();
    formData.append("document-files", file);

    const response = await fetch(`https://pylon.mortgage${uploadUrl}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
      body: formData,
    });

    if (!response.ok) {
      if (response.status === 401) {
        throw new Error("Authentication failed. Check your access token.");
      } else if (response.status === 413) {
        throw new Error("File too large. Maximum size is 50 MB.");
      } else if (response.status === 400) {
        const error = await response.json();
        throw new Error(`Invalid request: ${error.message}`);
      }
      throw new Error(`Upload failed: ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    console.error("Document upload error:", error);
    throw error;
  }
}

Best practices

  1. Handle URL expiration - Download URLs expire after 1 hour. Re-query document metadata to get fresh URLs if needed.
  2. Check upload status - Verify uploadStatus is COMPLETE before attempting to download.
  3. Validate file types and sizes - Check file types and sizes client-side before uploading to provide better user feedback.
  4. Use progress tracking - For large files, implement upload progress tracking using XMLHttpRequest or libraries that support it.