ScreenshotNeo

BlogHTML to image & PDF

Save a Generated PDF Online and Get Its URL in Python

Generate a PDF in Python, upload it to S3 and return a shareable URL. Learn when to use a temporary presigned link, how to handle uploads, and what can go wrong.

By the ScreenshotNeo team29 September 20269 min read

Save a Generated PDF Online and Get Its URL in Python

To save a PDF online and return its URL from Python, first generate the PDF, then upload it to a host, then create a URL with the access behavior you need. For a private Amazon S3 object, upload the file with Boto3 and generate a presigned get_object URL. That URL expires; it is not a permanent public address. AWS’s Boto3 presigned URL guide documents this flow.

This guide uses S3 for the complete example, with an explicit Signature Version 4 client configuration. The PDF generation function uses ReportLab, but the hosting steps work with a PDF produced by another library too. ReportLab’s guide describes its Python library for creating PDF documents.

1. Choose what kind of URL you need

Decide the sharing model before choosing the URL method. A signed link is convenient for short-term private sharing. An address intended to remain publicly accessible requires host-specific public delivery configuration. Do not label a time-limited presigned URL permanent.

PDF creation and online delivery are separate steps: generate the bytes, upload them, then issue a URL with the access behavior you need.
PDF creation and online delivery are separate steps: generate the bytes, upload them, then issue a URL with the access behavior you need.
Need Pattern Key detail
Share one private PDF temporarily S3 presigned GET Set an expiry. Anyone holding the URL can use the granted access while it remains valid.
Let a browser or client upload without cloud credentials Presigned upload URL or POST This authorizes an upload operation; it is not a download link. A presigned POST also requires its returned form fields.
PDF upload and media delivery workflow Cloudinary Cloudinary documents PDF assets and signed downloads for private or authenticated assets. Check the access settings for your account.
Long-lived public address Host-specific public delivery Configure public access deliberately and review the implications. A presigned URL is temporary.

S3 presigned URLs grant access to a specific operation and object for a limited time. Boto3’s API reference gives ExpiresIn a default of 3600 seconds; set it explicitly so the intended lifetime is visible in your code. See the S3 user guide and Boto3 API reference.

2. Set up the Python environment and AWS access

Install the PDF and AWS libraries:

python -m pip install reportlab boto3

Configure AWS credentials and a default region using the normal AWS credential chain for your environment, such as an assigned application role or local AWS profile. Do not put access keys in source code or commit them. Create or select an S3 bucket in the region where you intend to store the document. This example expects the bucket name, region and optional key prefix in environment variables.

Configure the S3 client with Signature Version 4 (s3v4), as AWS recommends. This avoids depending on legacy signing defaults that may vary by region. The deployment identity needs permission to upload to the chosen bucket and key, and permission to sign requests for the object. Restrict permissions to the resources the application needs.

3. Generate, upload and return the PDF URL

The following is a runnable command-line example. It creates a small PDF locally, uploads it under a unique object key, and prints a temporary download URL. Supply S3_BUCKET and AWS_REGION in the environment and make AWS credentials available through the SDK’s credential chain.

import os
import uuid
from datetime import datetime, timezone

import boto3
from botocore.config import Config
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas


def create_pdf(path: str) -> None:
    pdf = canvas.Canvas(path, pagesize=letter)
    pdf.setTitle("Generated report")
    pdf.drawString(72, 720, "Generated report")
    pdf.drawString(72, 696, "This file was created with ReportLab.")
    pdf.save()


def save_online_and_get_url(path: str, expires_seconds: int = 3600) -> str:
    bucket = os.environ["S3_BUCKET"]
    region = os.environ["AWS_REGION"]
    prefix = os.environ.get("S3_PREFIX", "generated-pdfs").strip("/")
    key = f"{prefix}/{datetime.now(timezone.utc):%Y/%m/%d}/{uuid.uuid4()}.pdf"

    s3 = boto3.client(
        "s3",
        region_name=region,
        config=Config(signature_version="s3v4"),
    )
    s3.upload_file(
        path,
        bucket,
        key,
        ExtraArgs={"ContentType": "application/pdf"},
    )
    return s3.generate_presigned_url(
        "get_object",
        Params={"Bucket": bucket, "Key": key},
        ExpiresIn=expires_seconds,
    )


if __name__ == "__main__":
    filename = "report.pdf"
    create_pdf(filename)
    print(save_online_and_get_url(filename))

The content type helps clients identify the uploaded object as a PDF. The generated UUID makes accidental key collisions unlikely for this example. An S3 upload to an existing key replaces the object at that key, so use unique keys or intentionally manage overwrites. See Amazon S3’s presigned URL documentation for the URL behavior and Boto3’s guide for presigned operations.

Use it from an application

Call save_online_and_get_url after generating the document and return its result from your application endpoint. Treat the URL like a bearer secret: someone who receives it can use the access it grants until it expires. Avoid logging the full URL or placing it in public analytics events if the document is private.

Choose an expiry long enough for the recipient to open or download the file, but no longer than the sharing need. If the application must support later access, store the bucket and key in your database and mint a fresh presigned GET URL when an authorized user requests the document. Do not persist an expired URL as if it were the file’s stable identity.

4. Upload from a client without exposing AWS credentials

For larger files or browser-based flows, an application server can create a presigned upload request and let the client send the bytes directly to S3. The client does not receive AWS credentials. It must use the URL and request details that were generated for the upload.

Boto3’s presigned POST pattern returns both a URL and a fields dictionary. Send every returned field along with the file; omitting fields can make the upload fail. This abbreviated server-side function illustrates the returned request structure:

def create_upload_form(s3, bucket: str, key: str) -> dict:
    return s3.generate_presigned_post(
        Bucket=bucket,
        Key=key,
        Fields={"Content-Type": "application/pdf"},
        Conditions=[{"Content-Type": "application/pdf"}],
        ExpiresIn=600,
    )

Return that form data to the client over your authenticated application endpoint. The client submits a multipart form containing the returned fields and the file to the returned URL. The result is an uploaded object, not a download URL. After upload, your server can create a presigned GET URL for the exact bucket and key. Keep the upload expiry short and avoid accepting arbitrary keys or unbounded file sizes in a production endpoint. See the Boto3 guide for presigned URL and POST examples.

5. Other ways to create and deliver the PDF

Use an existing PDF file

If a different library or process already creates the PDF, keep the S3 portion unchanged: upload the resulting file with its object key and content type, then sign a get_object request. PDF generation and online hosting are separate responsibilities.

Use Cloudinary for PDF delivery

Cloudinary documents uploading and delivering PDF assets, and signed downloads for private or authenticated assets. This may fit an application that already uses a media platform for delivery. The exact delivery URL and access behavior depend on the asset and account configuration; consult the Cloudinary upload documentation and upload API reference before implementing its flow.

Return a stable public URL

A stable public address and a temporary signed link solve different problems. For a public document, configure the host’s delivery access for public reads and use its documented object or delivery URL. This article does not provide a public-bucket recipe: the exact policy and delivery setup should be selected and reviewed for your account. Never make a private PDF public merely to avoid link expiry.

6. Common problems and fixes

Symptom Likely cause What to check
NoCredentialsError or credential lookup failure The SDK cannot find credentials in its configured chain. Configure the runtime’s role, profile or credential provider. Do not hard-code secrets as a workaround.
AccessDenied during upload The application identity lacks permission for the bucket or object key, or a bucket policy prevents the operation. Check the identity policy, bucket policy, target bucket and key prefix.
Presigned URL returns an authorization error The URL expired, its signing credentials are no longer valid, or it was changed after signing. Generate a fresh URL and share it intact. Check the configured expiry and the credentials used to sign it.
Upload succeeds but the URL does not download The URL was made for the wrong operation or object, or the object key differs. Generate a get_object URL for the exact bucket and key used during upload.
Browser upload returns a form error The client omitted a presigned POST field or sent a different content type than the signed condition. Submit every returned field and match the request to the generated policy.
The old PDF appears after another upload The code reused an object key, replacing the previous object. Use a unique key per generated file, or deliberately implement version and overwrite handling.
File opens with an unexpected name or type Object metadata such as content type or content disposition does not match the intended response. Set appropriate upload metadata and verify the download behavior with your target clients.
URL creation works locally but fails in deployment The deployed role, region or runtime configuration differs from the local environment. Check the deployed identity, bucket region and environment variables; configure SigV4 explicitly.

7. Reliability, performance and cost considerations

The workflow makes at least one document-generation step, one object upload, and one URL-signing call. Generation time depends on the document and library; transfer time depends on file size and network conditions. The URL signing call does not upload the document again. Avoid regenerating or re-uploading a file when the stored object is still the desired version.

For reliability, handle generation and upload failures separately so a failed upload is not reported as a shareable document. Retry transient upload errors according to the retry policy appropriate for your application, and make retries safe by using a stable object key for one logical document operation. If you use a random key on every retry, failed attempts can leave orphaned objects. Keep a record of the final bucket and key so the application can issue a new access URL later.

Set a retention and cleanup policy for generated documents, especially when each request creates a unique object. Presigned URLs control access duration; they do not delete the underlying PDF. Storage and transfer charges depend on provider, region, usage and account configuration. The research sources establish the workflow but do not provide a current cost comparison, so check the provider’s current pricing for your workload.

For the private-download workflow, access depends on both the URL’s validity and the object’s availability under the signing identity and bucket configuration. Plan for expired links by generating a replacement after application authorization rather than telling users to reuse an old URL.

8. When the URL itself is a website screenshot or PDF capture

If the file you need is actually a capture of a web page, you do not need to build and host a browser-rendering pipeline just to obtain the capture. ScreenshotNeo is a website screenshot API and MCP server; one GET request can return a PNG, JPEG, WebP or PDF. Its API handles the website capture. The S3 workflow above is for hosting a PDF your Python application generated.

A website capture service can handle browser rendering and clear common overlays before returning an image or PDF.
A website capture service can handle browser rendering and clear common overlays before returning an image or PDF.

9. FAQ

How long does an S3 presigned URL last?

It lasts for the expiry configured when generating it, subject to the validity of the signing credentials and service rules. Boto3’s API default is 3600 seconds, but set ExpiresIn explicitly.

Can I make a presigned URL permanent?

No. A presigned URL is time-limited. For a persistent public address, use the host’s public delivery configuration; for ongoing private access, store the object identity and mint fresh links when authorized.

Not by itself. Presigned requests are tied to an operation. Generate a separate presigned GET URL for downloads.

What if the PDF was created outside Python?

Upload the resulting PDF file with the same object storage steps. The source of the PDF does not change the hosting and access-control decisions.

Or skip the browser setup

If your task is capturing a live web page as a PDF or image, use ScreenshotNeo’s API instead of installing and maintaining a browser. The request below returns a WebP capture of a page; see the ScreenshotNeo API documentation for parameters and formats.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie and consent banners and removes 60+ known consent platforms, newsletter popups and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000, and every feature is on every plan.

Sign up free for 1,000 screenshots a month with no card.