Sending Documents

Before starting this part, make sure you've decided which evidence requests to send documents for.

📘

Note that sending Documents uses GraphQL, unlike sending Resources, which uses REST.

Gather info and get set up

First, ensure you have completed Implement OAuth. We then recommend completing the GraphQL quick start to ensure everything is working.

Then, get the ID(s) for each evidence request. The easiest way is to view the evidence request on the Documents page and grab the part after documents/ in the URL. For example, the URL for the "Proof of completed access review" evidence request is https://app.vanta.com/documents/access-reviews, so the ID is access-reviews.

Query and upload documents

Sending documents is now relatively simple. The only two endpoints you need are the following: (You can play around with these endpoints in Explorer, or reference them in the Schema).

  • evidenceRequests: View the list of evidence requests, and for each evidence request view the previously uploaded documents and next due date. Note that this endpoint is paginated. This requires the self:read-document OAuth scope
  • uploadEvidenceFile: Upload a document to a given evidence request. Note that this endpoint is a mutation. This requires the self:write-document OAuth scope

Your code needs to do the following (see Example):

  1. Get the list of previously uploaded documents by calling evidenceRequests and filtering by evidenceRequestId.
  2. Parse the results to determine if a new document should be uploaded. A new document should be uploaded if: 1) there are no documents previously uploaded or 2) a new document is due based on the recurrence settings (see Recurrence).
  3. Upload a new document by calling uploadEvidenceFile.

📘

When uploading some file types, some libraries and versions of curl will change the Content-Type to application/octet-stream, causing a blank document to be uploaded, or the request to fail. In these cases, ensure you specify the filetype explicitly for relevant form fields - for example, if you're uploading a CSV, ensure the Content-Type header is set to text/csv, or application/pdf for PDFs.

If you are successful, you should see a document uploaded:

Recurrence

Some evidence requests have a concept of recurrence. For example, Proof of completed access review may need to be uploaded on a quarterly basis (users can configure recurrence settings by clicking "Edit recurrence" on the top right).

You can view recurrence info by parsing the renewalMetadata object. Vanta provides a field, called nextDate, that specifies the exact time the document is due by - you can simply check this date against the current date to determine if a new document should be uploaded.

🚧

You are responsible for keeping documents up-to-date based on document recurrence. To that end, we recommend setting up your script to run on a cron.

📘

A note on document limits

You're welcome to choose how many and what type of documents to upload. However, note that:

  • Document filetypes are restricted to PDF, PS, PNG, JPG, WEBP, TXT, CSV, XLSX, DOCX, and ZIP.
  • You can only upload up to five documents per (evidence request, recurrence cadence). For example, if Proof of completed access review is set to renew quarterly, you can only upload five documents to that evidence request per quarter. (This limit resets on the next quarter).

Example

Here's an example Python script that queries the "Proof of completed access review" evidence request, determines if a new document should be uploaded it, and uploads it to Vanta if so.

We recommend using this script as a starting point.

# This uses the GQL library (pip install gql[all])
from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport
from datetime import date, datetime, timezone
import base64
import io

# An oauth access token with self:read-document and self:write-document scopes.
# See https://developer.vanta.com/docs/oauth-flow
OAUTH_ACCESS_TOKEN = 'YOUR_OAUTH_ACCESS_TOKEN'

# The ID of the document to upload
DOCUMENT_ID = 'access-reviews'

# The file to upload. For the purposes of this tutorial, it's encoded as Base64,
# but feel free to parse any file from your system.
FILE = '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nMic46ulV/NWD+Qa5G7PnVzjtebrtai1rLW9eep5m/LG1rmuq18PrU9Y37tBc0PBhomN3hsPbyJsSt30Q75Zfln+q81Rm3sKVAo2Foxv8d7SWihRyC+8vdVha9021DbutsHtlturtn8sYhddKTYrrih+X8IqufKV+VeVXy3uSNwxWGpTun8nZidv58gu512Hy6TL1pSN7w7c3VlOLy8qf7Vn5Z7LFVYVdXsJe4V7RysDKrurtKp2Vr2vTq4ernGraa9Vrt1eO7+Pve/Gfpf9bXUqdcV17w5wD9yp967vbNBpqDiIOZh98EljZGP/14yvm5sUm4qbPhziHRo9HHq4r9m2ublFuaW0FW4Vtk4fiTty/RuPb7rbjNvq22ntxUfBUeHRp9/GfztyzP9Y73HG8bbvtL+r7aB0FHVCnbmds13JXaPdMd1DJ/xO9PY49HR8b/L9oZPqJ2tOyZ4qPU04XXB68cyaM3NnM8/OnEs6N967svf++ejzt/pC+gYv+F+4dNHr4vl+1/4zlxwvnbxsf/nEFcaVrqs2VzsHrAc6frD+oWPQZrDzmu217ut213uGlgydvuF849xNj5sXb/neujq8dHhoJGLkzu2426N32Hem7qbdfXEv+97C/Y0P0A+KHko9rHik/KjhR/0f20dtRk+NeYwNPA57fH+cNf7sp6yf3k8UPCE/qZhUm2yespg6Oe01ff3psqcTzzKfLcwU/iz9c+1zveff/eLyy8Bs9OzEC/6LxV9LXiq8PPTK6lXvXPDco9fprxfmi94ovDn8lvG2/13Uu8mFnPfY95Uf9D/0fPT/+GAxfXHxX/eE8/vLYglBDQplbmRzdHJlYW0NCmVuZG9iag0KOCAwIG9iag0KPDwvQmFzZUZvbnQvSGVsdmV0aWNhL0VuY29kaW5nL1dpbkFuc2lFbmNvZGluZy9OYW1lL0hlbHYvU3VidHlwZS9UeXBlMS9UeXBlL0ZvbnQ+Pg0KZW5kb2JqDQp4cmVmDQowIDkNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxNyAwMDAwMCBuDQowMDAwMDAwMDY2IDAwMDAwIG4NCjAwMDAwMDAxMjIgMDAwMDAgbg0KMDAwMDAwMDIwOSAwMDAwMCBuDQowMDAwMDAwNDM5IDAwMDAwIG4NCjAwMDAwMDA2MzIgMDAwMDAgbg0KMDAwMDAwMDY2OSAwMDAwMCBuDQowMDAwMDAzMzQ4IDAwMDAwIG4NCnRyYWlsZXINCjw8DQovUm9vdCAxIDAgUg0KL0luZm8gMyAwIFINCi9TaXplIDkvSURbPEEwNkJDODc0RkMzOEE2NzZENzBEMzAwNEY1RTQzMTY2PjxBMDZCQzg3NEZDMzhBNjc2RDcwRDMwMDRGNUU0MzE2Nj5dPj4NCnN0YXJ0eHJlZg0KMzQ0OQ0KJSVFT0YNCg=='

# Create a GraphQL client
client = Client(
    transport=AIOHTTPTransport(
        url="https://api.vanta.com/graphql",
        headers={
            'Authorization': f'bearer {OAUTH_ACCESS_TOKEN}',
        }
    )
)

# Query used later to get the list of uploaded documents
documentStatusQuery = gql(
    """
  query getDocumentStatuses {
    organization {
        evidenceRequests(first: 100) {
          edges {
            node {
              evidenceRequestId
              renewalMetadata {
                nextDate
              }
              evidence(first: 10) {
                edges {
                  node {
                    title
                  }
                }
              }
            }
          }
      }
    }
  }
"""
)

# Mutation used later to upload a document
uploadDocumentMutation = gql(
    """
  mutation uploadEvidenceFile($input: UploadEvidenceFileInput!) {
    uploadEvidenceFile(input: $input) {
      ... on UploadEvidenceFileSuccess {
        evidenceRequest {
          evidenceRequestId
        }
      }
      ... on BaseUserError {
        message
      }
    }
  }
"""
)

# Parse the result to determine whether a new document should be uploaded
documentStatusQueryResult = client.execute(documentStatusQuery)
evidenceRequests = list(map(
    lambda x: x["node"], documentStatusQueryResult["organization"]["evidenceRequests"]["edges"]))
evidenceRequest = list(
    filter(lambda x: x["evidenceRequestId"] == DOCUMENT_ID, evidenceRequests))[0]

# A new document is needed if there are either no documents uploaded yet, or the next due date is past
numPreviouslyUploadedDocs = len(evidenceRequest["evidence"]["edges"])
nextDocumentDueDate = datetime.fromisoformat(
    evidenceRequest["renewalMetadata"]["nextDate"])
newDocumentNeeded = numPreviouslyUploadedDocs == 0 or nextDocumentDueDate < datetime.now(
).replace(tzinfo=timezone.utc)

# Upload the document
if newDocumentNeeded:
    print("New document needed. Uploading the document...")
    fileToUpload = {
        "input": {
            "evidenceRequestId": DOCUMENT_ID,
            "filename": "connectors_example_file.pdf",
            "description": "Simple file upload to demonstrate use of Connectors API for documents",
            "file": io.BytesIO(base64.b64decode(FILE))
        }
    }

    client.execute(uploadDocumentMutation,
                   variable_values=fileToUpload, upload_files=True)
else:
    print("No new document upload needed.")

What’s Next