Downloading Files

Downloading files from a Pull

A Pull can carry two kinds of
downloadable files:

  • Consumer-uploaded files — files a consumer submitted through a custom file
    upload
    field in a consumer flow.
  • Documents — insurance documents (declarations pages, ID cards, binders, etc.)
    associated with the Pull.

Both expose a download_url, and both are downloaded the same way. This guide covers
where to find each, then how to download them.

Consumer-uploaded files

Custom form answers are nested a few levels deep inside a Pull:

Pull
└─ answers[]            → AnswerSection
   └─ fields[]          → AnswerField
      ├─ type           → "FILE_UPLOAD"
      └─ value[]        → FileUpload   (when type is "FILE_UPLOAD")
         └─ download_url
  1. Iterate over the Pull's answers array. Each entry is an
    AnswerSection.
  2. Within each section, iterate over fields. Each entry is an
    AnswerField.
  3. For any field whose type is FILE_UPLOAD, the value is an array of
    FileUpload objects
    (one
    per uploaded file). For all other field types, value is a scalar, and is null if
    the field was never answered.
  4. Each FileUpload describes one file and includes a download_url.

A FILE_UPLOAD field looks like this in a Pull response:

{
  "consumer_flow_section_field_id": "8f1c2e6a-2b9d-4f3a-9b1e-7c5d2a4f6e10",
  "label": "Upload your declarations page",
  "type": "FILE_UPLOAD",
  "value": [
    {
      "file_id": "3a7b9c1d-4e2f-4a6b-8c9d-0e1f2a3b4c5d",
      "name": "declarations-page.pdf",
      "type": "application/pdf",
      "size": 482931,
      "download_url": "https://app.usecanopy.com/api/v1.0.0/teams/:teamId/pulls/:pullId/answers/:answerId/files/:fileId"
    }
  ]
}

Documents

Documents are simpler — they live directly on the Pull's documents array, and each
Document carries its own
download_url:

Pull
└─ documents[]          → Document
   └─ download_url

A document looks like this in a Pull response:

{
  "document_id": "5d4c3b2a-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
  "title": "Auto Declarations",
  "document_type": "DECLARATIONS",
  "policy_id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
  "mime_type": "application/pdf",
  "download_url": "https://app.usecanopy.com/api/v1.0.0/teams/:teamId/pulls/:pullId/documents/:documentId"
}

Downloading a file

Use the download_url exactly as returned by the API. The paths shown above are
illustrative — don't construct them yourself.

A download_url is a Canopy Connect API endpoint, not the file itself. This is true
for both consumer-uploaded files and documents. Request it with an authenticated GET,
using the same credentials as every other API call — the x-canopy-client-id and
x-canopy-client-secret headers.

The endpoint responds with a 302 redirect to a short-lived, signed URL where the
file bytes are actually hosted. You can consume this in one of two ways:

Option 1 — follow the redirect (default)

Let your HTTP client follow the 302 and stream the file directly. Most clients do this
automatically.

curl -L \
  -H "x-canopy-client-id: $CANOPY_CLIENT_ID" \
  -H "x-canopy-client-secret: $CANOPY_CLIENT_SECRET" \
  -o declarations-page.pdf \
  "https://app.usecanopy.com/api/v1.0.0/teams/:teamId/pulls/:pullId/documents/:documentId"

The -L flag tells curl to follow the redirect. Note that the signed download URL is
short-lived — fetch the file promptly rather than storing the signed URL for later.

Option 2 — request the signed URL as JSON

If you would rather receive the signed URL instead of following the redirect (for
example, to hand it to a download manager or a client-side component), send an
Accept: application/json header. The endpoint then returns a JSON body containing the
signed URL:

curl \
  -H "x-canopy-client-id: $CANOPY_CLIENT_ID" \
  -H "x-canopy-client-secret: $CANOPY_CLIENT_SECRET" \
  -H "Accept: application/json" \
  "https://app.usecanopy.com/api/v1.0.0/teams/:teamId/pulls/:pullId/documents/:documentId"
{
  "url": "https://signed-storage.example.com/...&signature=...&expires=..."
}

The returned url is the same short-lived signed location the 302 would have pointed
to. It does not require the Canopy Connect authentication headers, but it does expire, so
use it promptly.