Downloading Interaction Files

Request short-lived, single-use download URLs for an interaction's file attachments.

Clinical interactions in Hint can carry file attachments: lab report PDFs, uploaded documents, scanned forms, and files pushed by other partners. This guide shows how to turn those attachments into downloadable files.

The files array names the attachments

Every interaction response includes a files array. Each entry identifies one attachment:

{
  "id": "int-abc123",
  "type": "document",
  "files": [
    { "id": "Fah-3TWDJuQ1MSe-lBTiPh", "filename": "referral.pdf", "type": null },
    { "id": "Znd8CviNr5RERx6L-v5auf", "filename": "scan.tiff", "type": null }
  ]
}

This tells you what is attached, but it is not itself downloadable. The underlying storage keys are private, so you request a short-lived signed URL for a file through one of two endpoints. The file id in the files array is the same id you pass to the single-file endpoint below.

Each entry carries a type

Every entry in the files array carries a type, and its value is null. The key is always present. Read null as untyped.

One interaction type is the exception. A Lab Interaction records which document ordered the lab and which reported it, and its files carry that type. Lab Requisitions and Results covers both values.

Both endpoints below resolve every file id, whatever its type.

Send a file

files takes the documents you attach, and every interaction type accepts the same entry. Each entry is an object:

{
  "files": [
    { "url": "https://files.example.com/referral-000123.pdf", "filename": "referral.pdf" },
    { "url": "data:application/pdf;base64,JVBERi0xLjQKJVBFWEFNUExF", "filename": "scan.pdf" }
  ]
}

url is required, and it carries the document in one of two forms:

  • A data: URI carries the document inline.
  • An https URL points Hint at the document. Hint fetches it while it handles your call, so the URL has to resolve at that moment.

filename is the name the file is stored under. It is optional, and sending it is the reliable choice. When you leave it out Hint takes the name from the response's Content-Disposition, then from the URL's path, and derives a missing extension from the file's own bytes. A file Hint cannot name is rejected with a 422. For a data: URI the name comes from that last step every time, since the URI carries the bytes alone.

Hint accepts a URL string or an object carrying a url. It rejects anything else:

{
  "status": 422,
  "code": "unprocessable_entity",
  "message": "Files must be an array of URL strings or objects holding a url",
  "errors": { "files": ["must be an array of URL strings or objects holding a url"] }
}

A bare URL string also works, and Hint derives the whole name.

A Lab Interaction takes two further file fields that record what each document is. See Lab Requisitions and Results.

Clearing files with files: [] is legacy

files is the canonical file field on an interaction create or update, and it replaces the interaction's file list:

  • Leave files out and the interaction keeps its current files.
  • Send entries in files and they replace the interaction's whole untyped file list.
  • Send an empty files array and Hint removes every untyped file on the interaction, including the files a provider attached in the Hint UI and the files another partner pushed.
{
  "files": []
}

A file typed as a lab requisition or a lab result belongs to a separate list. It stays attached through files: [] and keeps appearing in the interaction's files array. files: [] is the whole of the removal the API offers today.

❗️

Do not build a sync on the empty-array delete

files: [] is the legacy way to remove a file, and a dedicated endpoint for deleting a single interaction file is planned to replace it. Until that endpoint ships, files: [] is the only removal the API offers, and it takes every untyped file with it.

Send it only when you mean to remove every untyped file on the interaction. Send files when you mean to attach one.

Download every file on an interaction

List Interaction File Download URLs returns a signed URL for every file on the interaction, as a bare array:

GET /api/provider/interactions/{interaction_id}/files/download_urls
[
  {
    "id": "Fah-3TWDJuQ1MSe-lBTiPh",
    "filename": "referral.pdf",
    "type": null,
    "url": "https://practice-bucket.s3.amazonaws.com/public/pat-123456/referral.pdf?X-Amz-Signature=EXAMPLE",
    "expires_at": "2026-07-01T12:05:00.000Z"
  },
  {
    "id": "Znd8CviNr5RERx6L-v5auf",
    "filename": "scan.tiff",
    "type": null,
    "url": null,
    "expires_at": null
  }
]

Each element carries the same type the interaction reports, so a sync can group the signed URLs without reading the interaction again.

This is the call to make when you are syncing an interaction and want every attachment in one round trip.

Download a single file

Get Interaction File Download URL returns one element of that same shape. Use it when you already hold a file id (from an earlier sync, a queued job, or a retry) and want a fresh URL for just that file without re-signing the rest:

GET /api/provider/interactions/{interaction_id}/files/{id}/download_url
{
  "id": "Fah-3TWDJuQ1MSe-lBTiPh",
  "filename": "referral.pdf",
  "type": null,
  "url": "https://practice-bucket.s3.amazonaws.com/public/pat-123456/referral.pdf?X-Amz-Signature=EXAMPLE",
  "expires_at": "2026-07-01T12:05:00.000Z"
}

Downloads are PDF-only

Only files that resolve to application/pdf receive a signed url. The two endpoints handle a non-PDF differently:

  • On download_urls, a non-PDF stays in the array with url and expires_at set to null. It is not omitted, so you can tell "this attachment is not downloadable" apart from "this attachment does not exist".
  • On download_url, a non-PDF is a 422 Unprocessable Entity. When you ask for a specific file, a refusal is the honest answer.

An id that does not match any file on the interaction returns 404 Not Found on the single-file endpoint.

URLs are short-lived: sign at download time

The signed url expires roughly 5 minutes after it is issued (expires_at carries the exact time), and it is meant to be used once. Request it at the moment the user acts, then redirect them to (or fetch) the returned url right away.

Do not cache the URL, store it in your database, or precompute it during a scheduled sync. A URL signed during an overnight backfill will have expired long before anyone clicks it. Store the file id instead, and request a fresh URL for it whenever you need one - the same id returns a new URL every time you call.

sequenceDiagram
  participant App as Your app
  participant Hint as Hint Provider API
  App->>Hint: GET /interactions/{id}
  Hint-->>App: files [ { id, filename } ]
  App->>Hint: GET /interactions/{id}/files/{file_id}/download_url
  Hint-->>App: { url, expires_at }
  App->>App: Redirect user to url within 5 minutes

The same pattern holds wherever you put a file in front of a user: keep the file's id from the interaction's files array, and request its download_url at the moment they act.

Access and scope

These endpoints are practice-scoped like the rest of the Provider API, authorized with the practice's access token. A partner can download files on any interaction it can already read; the endpoints do not restrict downloads to files the calling partner uploaded. Files a Hint provider attached in the UI are in scope alongside files uploaded by partners.

Availability

Both endpoints are available from API version 2026-07-01 onward. Clients pinned to an earlier version receive a 404 on both routes. See Initial Data Sync for the broader pattern of reconciling interaction data when you first connect a practice.


Did this page help you?