pyOdk support for downloading submission attachments

What high-level problem are you trying to solve?

pyODK is a wrapper for communicating with the ODK Central API. It seems that there is no way to download the attachments of a submission using this library.

The submission object object returned from the Client contains an attachment field but it’s None always. The response from ODK to that query only contains metadata information about the submission.

from pyodk import Client

client = Client()
project_id = 3
form_id = "form"
submission_id = "uuid:f686c5f5-7edc-4798-967f-80cc1e6180d1"

print(
    client.submissions.get(
        instance_id=submission_id,
        form_id=form_id,
        project_id=project_id,
    )
)

Whose output is this:

instanceId='uuid:f686c5f5-7edc-4798-967f-80cc1e6180d1' submitterId=61 createdAt=datetime.datetime(2026, 2, 3, 15, 50, 18, 682000, tzinfo=TzInfo(UTC)) deviceId=None reviewState=None userAgent='Enketo/7.5.1 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' instanceName=None updatedAt=None attachments=None

To download the attachments of a submission from ODK Central API, an HTTP endpoint can be used, and it works. The purpose of pyODK is to abstract those details, so it would be nice to have that feature in it.

Any ideas on how ODK could help you solve it?

Adding a method or function to the Client class with the following signature:

def download_attachment(self, project_id : int, form_id : str, submission_id : str, attachment_name : str) -> bytes
    # do the HTTP request to the download attachment endpoint to obtain the bytes.


To solve the problem I've implemented the function like this:


def download_attachment(
    client: Client,
    project_id: int,
    form_id: str,
    submission_id: str,
    attachment_name: str,
) -> bytes:
    user = client.config.central.username
    password = client.config.central.password
    url = client.config.central.base_url
    import requests
    import base64

    credentials = f"{user}:{password}"
    encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8")
    headers = {"Authorization": f"Basic {encoded_credentials}"}

    query = f"{url}v1/projects/{project_id}/forms/{form_id}/submissions/{submission_id}/attachments/{attachment_name}"
    response = requests.get(query, headers=headers)
    return response.content

Upload any helpful links, sketches, and videos.

I’m building a website whose goal is showing statistical information about the submissions of a selected form. My website gets the data from an API I’m running in python using FastAPI, which under the hood uses the *pyODK* client to obtain the data.

1 Like

Thanks for following up here from our conversation on the pyodk repository! It's helpful to know that you're happy with getting the attachment names, passing them into a method, and getting the attachment bytes back. I'm hoping that if others have this need they'll chime in with their requirements and we'll try to add the functionality soon.