For developers

How to send files from Python, with receipts

By Hein de Wilde·Updated ·3 min read

Key takeaways

  • Let the CLI do the resumable upload and parse its --json output — no uploader to write.
  • Use requests against the API to check per-recipient download receipts; poll gently, as there are no webhooks yet.
  • Budget for API upload metering and give scheduled links an expiry.
Contents
  1. Why not just requests.post the file
  2. Set up once
  3. Send a file and get the link back
  4. Check who downloaded it
  5. Doing it without the CLI
  6. Things to decide before you schedule it
  7. What the API cannot do

The simplest reliable way to send files from Python is to let a purpose-built uploader move the bytes and use Python for everything around it: call the Yungle CLI with --json to upload and send, parse the link it returns, then query the API with requests to see who downloaded it. You get resumable uploads without writing a resumable uploader, and your script stays about twenty lines long.

I built Yungle, so this is a guide to our API specifically. The shape — let a proper uploader do the bytes, keep the orchestration in Python — applies to any service.

Why not just requests.post the file

It is tempting to post the file in one request. For a small file, it works. For a large one, it fails in the most annoying way: a dropped connection at 90% throws everything away and starts again, and at a few gigabytes something always drops.

Large uploads need to be resumable — sent in parts, with the ability to pick up from the last part the server confirmed. Yungle uses tus, an open protocol for exactly this. You can speak tus from Python, but you do not have to: the CLI already does it, correctly, and handles the tokens. Why resumable uploads are hard explains what it saves you from.

Set up once

Install the CLI and give it an API key from your account settings:

npm install -g yungle-cli
export YUNGLE_API_KEY=yk_live_...
yungle auth status

The CLI reads YUNGLE_API_KEY from the environment first, which is what you want on a server or in a scheduled job. Give the key only the scopes the script needs — for sending and checking receipts, transfers:write (which includes read of transfers) is enough.

import json
import subprocess

def send(paths, to, message=None):
    cmd = ["yungle", "send", *paths, "--json"]
    for address in to:
        cmd += ["--to", address]
    if message:
        cmd += ["--message", message]
    # Progress goes to stderr; stdout is a single JSON object.
    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
    return json.loads(result.stdout)

sent = send(["exports/report-2026-09.pdf"], to=["client@example.com"],
            message="September report attached.")
print(sent["url"], sent["expiresAt"], sent["notified"])

--json makes the CLI print one JSON object with the transfer's id, url, expiresAt and the addresses that were emailed. Progress and diagnostics go to stderr, so they never land in what you parse.

A few flags worth knowing: --password puts a password on the link, --expires <days> sets its lifetime (clamped to what your plan allows), and --title labels it in your dashboard without showing it to the recipient.

Check who downloaded it

This is where plain requests is the right tool. The API reports downloads per recipient:

import os
import requests

API = "https://yungle.co/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['YUNGLE_API_KEY']}"}

def receipts(transfer_id):
    r = requests.get(f"{API}/transfers/{transfer_id}/downloads", headers=HEADERS, timeout=30)
    r.raise_for_status()
    data = r.json()
    return {rec["email"]: rec["downloaded"] for rec in data["recipients"]}

print(receipts(sent["id"]))  # {'client@example.com': False}

There are no webhooks yet, so if you need to act on a download, poll — gently. Once an hour is plenty for a human recipient.

Doing it without the CLI

If you cannot install Node on the machine, you can call the API directly: POST /transfers with each file's name and exact size in bytes returns a tus endpoint and an upload token per file; you upload each file with a tus client, sending the token in the x-yungle-upload-token header on every request; then POST /transfers/{id}/finalize with the recipients makes the link live and sends the email. The uploads guide lists the rules that bite — exact sizes, two-hour token windows, and never re-posting an upload that is already in progress.

A file on Yungle: encrypted as it uploads, stored in Germany, scanned, and delivered only through Yungle — so a link can really expire.

Things to decide before you schedule it

Metering. API uploads include 10 GB a month on every plan, including Free, and are charged per GB from prepaid credit beyond that. Downloads are never metered. A nightly job sending a 1 GB export uses a third of that in ten days — do the sum first.

Expiry. A script that runs daily produces a lot of links. Give them a sensible lifetime with --expires, rather than accumulating every report forever.

Failure. check=True makes a failed send raise, which is what you want: a scheduled job that swallows an upload error produces a green run and no delivery.

What the API cannot do

Worth knowing before you design around it: the vault and end-to-end encrypted transfers are not reachable from the API or the CLI, because their keys are created in the browser and never sent to Yungle. What the API cannot do has the full list.

If sending is a step in an automated workflow, the page for automation and CI shows the pattern end to end. If your files come out of a build rather than a script, sending large files from CI covers the pipeline version, and the API guide covers the request sequence in more depth.

Hein de Wilde

I build and run Yungle, and I write everything here. Comparisons name competitors and credit them, every claim about another company comes from that company’s own documentation, and where we fall short it says so. More about who is behind this.

Read next