How to Get a Direct PDF URL from Amazon S3
Build a working Amazon S3 PDF URL, create private presigned links, fix 403 errors, and control browser download behavior.
Direct answer: A public Amazon S3 PDF uses a virtual-hosted object URL containing the bucket, Region, and exact object key. A private PDF needs a time-limited presigned GET URL. The URL alone never changes S3 permissions: public links require effective anonymous s3:GetObject access, while presigned links authorize a private object until the signature or its credentials expire.
1. Build the direct URL for a public PDF
Use this pattern for a virtual-hosted S3 object URL:
https://BUCKET.s3.REGION.amazonaws.com/OBJECT-KEY
For example, an object stored as docs/guide.pdf in bucket acme-public-files in us-east-1 is:
https://acme-public-files.s3.us-east-1.amazonaws.com/docs/guide.pdf
The object key is case-sensitive and includes every prefix. Encode spaces and other reserved characters:
https://acme-public-files.s3.us-east-1.amazonaws.com/docs/annual%20report.pdf
Virtual-hosted style is the current preferred pattern. AWS documents the URL forms and naming constraints in S3 virtual hosting.
Make sure the object is actually public
New S3 buckets have all four Block Public Access settings enabled by default. A URL does not override those settings. The bucket policy, object ownership settings, and account-level controls must together allow anonymous s3:GetObject. AWS states that S3 objects are private by default in its object sharing documentation.
Check the URL without downloading the file:
curl -I "https://acme-public-files.s3.us-east-1.amazonaws.com/docs/guide.pdf"
A successful response should include 200 OK. A 403 AccessDenied means the effective policy does not allow the request. A 404 commonly means the bucket, Region, key, or capitalization is wrong.
2. Create a direct URL for a private PDF
For private objects, generate a presigned GET URL. It grants access to the exact object for a limited period without changing the bucket policy. Anyone who obtains the URL can use it until it expires, so treat it like a bearer credential.
Using the AWS CLI
Configure credentials with the normal AWS CLI configuration, then run:
aws s3 presign s3://acme-private-files/docs/guide.pdf \\
--region us-east-1 \\
--expires-in 3600
The command prints a complete HTTPS URL. Open that URL in a browser or pass it to curl:
PDF_URL="$(aws s3 presign s3://acme-private-files/docs/guide.pdf --region us-east-1 --expires-in 3600)"
curl -L "$PDF_URL" -o guide.pdf
The AWS CLI and SDKs can create URLs for up to seven days, subject to the lifetime of the credentials used to sign them. Temporary credentials can make the URL expire sooner. Console-created links can be set for up to 12 hours.
Using Python and boto3
import boto3
s3 = boto3.client("s3", region_name="us-east-1")
url = s3.generate_presigned_url(
ClientMethod="get_object",
Params={
"Bucket": "acme-private-files",
"Key": "docs/guide.pdf",
},
ExpiresIn=3600,
)
print(url)
Install the SDK with python -m pip install boto3. The process needs AWS credentials with permission to read the object (normally s3:GetObject).
Using Node.js
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const client = new S3Client({ region: "us-east-1" });
const command = new GetObjectCommand({
Bucket: "acme-private-files",
Key: "docs/guide.pdf",
});
const url = await getSignedUrl(client, command, { expiresIn: 3600 });
console.log(url);
Install dependencies with:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Using the S3 console
- Open the bucket and select the PDF object.
- Choose the action to share the object with a presigned URL.
- Set an expiration within the console limit (up to 12 hours).
- Copy the generated URL exactly, including every query parameter.
3. Make the browser display the PDF
S3 sends the object metadata as HTTP headers. Set the object’s Content-Type to application/pdf so browsers recognize it as a PDF:
aws s3api head-object \\
--bucket acme-private-files \\
--key docs/guide.pdf \\
--region us-east-1
If the metadata is wrong, upload or copy the object with the correct value:
aws s3 cp guide.pdf s3://acme-private-files/docs/guide.pdf \\
--content-type application/pdf \\
--metadata-directive REPLACE
To force inline viewing or downloading, use Content-Disposition. A presigned request can override response headers with response-content-type and response-content-disposition; those overrides must be included when signing the request.
aws s3 presign s3://acme-private-files/docs/guide.pdf \\
--region us-east-1 \\
--expires-in 3600
For SDKs, add ResponseContentType: "application/pdf" and ResponseContentDisposition: "inline; filename=\"guide.pdf\"" to the signed GetObject parameters. Use attachment instead of inline when the browser should download the file.
4. Choose the right S3 delivery model
| Option | Who can retrieve it | Lifetime | Use it when | Trade-off |
|---|---|---|---|---|
| Public REST object URL | Anyone with the URL | Until policy or object changes | The PDF is genuinely public | Anyone can read or share it |
| Presigned GET URL | Anyone holding the link | Expiration and credential lifetime | Access must be private or temporary | You must regenerate expired links |
| S3 website endpoint | Publicly readable content | Until permissions change | Simple static website delivery | HTTP only; no HTTPS |
| CloudFront in front of S3 | Controlled by distribution or signed delivery | Distribution and signing policy | You need HTTPS, caching, or edge delivery | Requires CloudFront configuration |
An S3 website endpoint is different from the REST object endpoint. AWS notes that website endpoints support only publicly readable content and do not support HTTPS; use CloudFront when HTTPS and stronger delivery controls are required. See AWS website endpoints.
5. cURL recipes
Download a public PDF
curl -L "https://acme-public-files.s3.us-east-1.amazonaws.com/docs/guide.pdf" \\
-o guide.pdf
Inspect headers only
curl -sS -D - -o /dev/null "https://acme-public-files.s3.us-east-1.amazonaws.com/docs/guide.pdf"
Download a presigned URL
curl -L "PASTE_THE_PRESIGNED_URL_HERE" -o guide.pdf
6. Troubleshooting direct S3 PDF URLs
| Symptom | Likely cause | Fix |
|---|---|---|
403 AccessDenied on a public URL |
Block Public Access or policy denies anonymous s3:GetObject |
Review account, bucket, and object-level settings. A URL cannot grant public permission. |
403 SignatureDoesNotMatch |
URL was changed, Region is wrong, or a signed header does not match | Use the exact generated URL, sign in the bucket’s Region, and send every signed header unchanged. |
RequestTimeTooSkewed |
The signing machine clock is out of sync | Synchronize the system clock, then generate a new URL. |
404 NoSuchKey |
Key prefix or capitalization is wrong | Copy the exact key from the S3 console or aws s3 ls. |
| URL works briefly, then fails | Presigned URL expired or its temporary credentials expired | Generate a new link and use a longer-lived signing identity when appropriate. |
| Browser downloads instead of displaying | Content-Disposition: attachment or incorrect content type |
Set Content-Type: application/pdf and sign an inline disposition override. |
| Browser shows XML or an S3 error page | The object request is unauthorized or points to the wrong endpoint | Check status and headers with curl -I; verify bucket, Region, and key. |
| Works with CLI but not application code | Different credentials, Region, or endpoint in the application | Log the resolved Region and IAM principal (without secrets), then compare the generated URL. |
7. Performance, reliability, and cost considerations
- Use the nearest correct Region: Keep the bucket and signing configuration aligned. A Region mismatch can produce redirects or signature failures.
- Cache stable public files: CloudFront can reduce repeated origin requests and provide HTTPS delivery. Do not cache user-specific presigned responses as shared public content.
- Keep URLs short-lived for private data: Choose an expiration long enough for the expected download, then regenerate when needed.
- Encode keys once: URL-encode path characters, but do not alter the query string of a presigned URL after signing.
- Check headers before transferring large PDFs: A HEAD request catches permission, key, and content-type errors without downloading the body.
- Plan for expiration: Applications should detect a 403 from an expired link and request a fresh URL rather than retrying the same URL indefinitely.
- Account for S3 and delivery charges: Object storage, requests, data transfer, and any CloudFront usage are billed under your AWS account. A presigned URL itself is an authorization mechanism, not a separate free delivery tier.
8. Or skip the browser setup
If your workflow needs a rendered capture of an accessible PDF or web URL, ScreenshotNeo provides a GET API that returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://acme-public-files.s3.us-east-1.amazonaws.com/docs/guide.pdf -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://acme-public-files.s3.us-east-1.amazonaws.com/docs/guide.pdf"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://acme-public-files.s3.us-east-1.amazonaws.com/docs/guide.pdf' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie or consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server includes take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots each month without a card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account.
9. FAQ
Can I make a private PDF URL permanent?
No. A presigned URL is intentionally time-limited. For a stable public address, use a public object or a CloudFront distribution with an appropriate access policy.
Is the S3 website URL the same as the S3 object URL?
No. Website endpoints serve website content and do not support HTTPS. REST object URLs address individual objects and can be used with presigned requests.
What happens if I rename the PDF?
The object key changes, so a public URL or presigned URL for the old key does not automatically follow it. Generate a new URL.
Can someone share my presigned URL?
Yes. Anyone who has the link can use it until it expires or the object is removed. Keep expiration short for sensitive documents.
Why does changing one character break a signed URL?
The signature covers the request, including its path and query parameters. Editing the URL changes what was signed and causes a signature validation failure.


