Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a presigned S3 URL when the code that uploads the PDF should not hold long-lived AWS credentials. Trusted server-side C# code creates a URL for one bucket, one object key and the PUT verb. The uploader then streams the PDF with HttpClient.PutAsync. When your application already has an authenticated AWS SDK client, calling PutObjectAsync directly is simpler.

Presigned URL upload: the complete flow

A presigned URL delegates one narrowly defined S3 operation for a limited period. Your trusted backend creates the URL with the AWS SDK for .NET, then gives it to the component that has the PDF. That component performs an ordinary HTTPS PUT; it does not need an AWS access key or secret key.

  1. Choose the destination bucket and S3 key, such as invoices/2026/09/invoice-1042.pdf.
  2. Create an S3 client in trusted code, configured for the bucket’s AWS Region.
  3. Build a presigned request with the bucket, key, HttpVerb.PUT and an expiry.
  4. Open the PDF for reading and wrap the stream in StreamContent.
  5. Send the content to the URL with HttpClient.PutAsync, await the response and record useful diagnostics when it fails.

URL generation in .NET

using Amazon.S3;
using Amazon.S3.Model;

public sealed class S3PresignService
{
    private readonly IAmazonS3 _s3;

    public S3PresignService(IAmazonS3 s3) => _s3 = s3;

    public string CreatePdfUploadUrl(string bucketName, string objectKey, TimeSpan lifetime)
    {
        var request = new GetPreSignedUrlRequest
        {
            BucketName = bucketName,
            Key = objectKey,
            Verb = HttpVerb.PUT,
            Expires = DateTime.UtcNow.Add(lifetime)
        };

        return _s3.GetPreSignedURL(request);
    }
}

The credentials used to create this URL must be allowed to perform the intended object operation. Keep that code on a trusted server. Do not put long-lived AWS credentials in a browser, desktop client distributed to users or other untrusted uploader.

Streaming the PDF with HttpClient

using System.Net.Http;

public static async Task UploadPdfAsync(
    HttpClient httpClient,
    string presignedUrl,
    string filePath,
    CancellationToken cancellationToken = default)
{
    await using var file = new FileStream(
        filePath,
        FileMode.Open,
        FileAccess.Read,
        FileShare.Read,
        bufferSize: 64 * 1024,
        useAsync: true);

    using var content = new StreamContent(file);
    // Set this only when it was included in the presigned request's
    // signed headers and your application wants PDF metadata.
    content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");

    using var response = await httpClient.PutAsync(
        presignedUrl,
        content,
        cancellationToken);

    if (!response.IsSuccessStatusCode)
    {
        var error = await response.Content.ReadAsStringAsync(cancellationToken);
        throw new HttpRequestException(
            $"S3 upload failed ({(int)response.StatusCode} {response.ReasonPhrase}): {error}");
    }
}

The AWS .NET sample uses the same essential pattern: open the file, create StreamContent, call PutAsync and base success on IsSuccessStatusCode. Keep the file stream alive until the awaited request completes, then dispose it. A successful S3 response means S3 accepted the complete object; S3 does not add a partial object.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Content type and signed headers

application/pdf is useful when consumers should receive PDF metadata from S3, but it is not a universal requirement for the upload itself. If your presigning code signs a Content-Type or any other header, the PUT must send the same value. A mismatch commonly produces a signature error. If you do not sign that header, omit it from the upload or verify the behavior required by your presigning configuration.

The S3 REST API also supports optional checksums, server-side encryption, tags and conditional-write headers. Add them only when the presigned request and the actual HTTP request agree exactly. Do not treat a returned ETag as an unconditional PDF MD5 checksum; with server-side encryption, AWS explicitly says the ETag is not the object’s MD5.

Choosing the presigned pattern or direct PutObjectAsync

Decision point Presigned URL + HttpClient Direct AWS SDK upload
Who sends the upload request? Any client that receives the generated URL. The application containing an initialized, authenticated S3 client.
Authorization Trusted code signs one bucket/key/verb combination with an expiry. The application configures AWS credentials and IAM permissions directly.
HTTP call HTTPS PUT with the PDF as the request body. PutObjectAsync with a request containing the bucket, key and file path or stream.
Use it when A separate uploader should not receive normal AWS credentials. Your service already owns the authenticated S3 interaction.

This is an architectural choice. A presigned URL is not a different kind of S3 object: both approaches ultimately perform a PutObject operation. The distinction is who is trusted to make the request and where AWS credentials live.

Direct SDK alternative

If the same .NET process already has AWS credentials and should perform the upload, avoid an extra presigning round trip:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
using Amazon.S3;
using Amazon.S3.Model;

public static async Task PutPdfWithSdkAsync(
    IAmazonS3 s3,
    string bucketName,
    string objectKey,
    string filePath,
    CancellationToken cancellationToken = default)
{
    var request = new PutObjectRequest
    {
        BucketName = bucketName,
        Key = objectKey,
        FilePath = filePath,
        ContentType = "application/pdf"
    };

    var response = await s3.PutObjectAsync(request, cancellationToken);
    if ((int)response.HttpStatusCode < 200 || (int)response.HttpStatusCode >= 300)
        throw new HttpRequestException($"S3 returned {response.HttpStatusCode}");
}

The SDK also supports stream-based input when the PDF is produced in memory or by another stream. Supply a seekable or non-seekable stream according to the SDK API version you target, and dispose streams according to their ownership rules.

Keys, expiry and request scope

Choose a stable key

An S3 key is the complete object name, including prefixes. Decide whether names are immutable (for example, a generated identifier) or intentionally overwrite an existing key. Never derive a key solely from untrusted user input without validating path conventions and authorization.

Set an appropriate lifetime

The expiry is part of the authorization boundary. Make it long enough for the expected transfer and clock skew, but no longer than necessary. The AWS sample’s duration is example context, not a universal recommendation. Confirm current presigning and Signature Version 4 requirements for your bucket Region and encryption settings.

Match the verb

A URL signed for PUT is not a URL for POST or GET. Use the same HTTP verb that was included in the presigned request. Changing it, the host, signed headers or query parameters can invalidate the signature.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Production reliability and diagnostics

  • Use one long-lived HttpClient. Inject HttpClient through IHttpClientFactory or another managed lifetime rather than creating a new instance for every PDF.
  • Honor cancellation. Pass a request cancellation token so a disconnected caller does not leave an upload running indefinitely.
  • Capture status and body. On failure, retain the HTTP status, reason phrase and S3 error body. Avoid logging the full presigned URL because its query string grants temporary access.
  • Retry carefully. A retry can overwrite an object when the key is reused. Generate an idempotent key or verify your overwrite policy before retrying. Do not retry an expired URL; request a new one.
  • Stream rather than buffer. StreamContent avoids loading the complete PDF into memory. The AWS REST documentation’s example sizes are illustrative request values, not throughput or capacity recommendations.
  • Check the destination Region. Configure the S3 client for the bucket’s Region when generating the URL. Region or endpoint mismatches can appear as redirects or signature failures.

Troubleshooting common failures

HTTP 403 or “SignatureDoesNotMatch”

Check that the URL has not expired, the machine clock is accurate, and the upload uses the exact signed verb, host and headers. A Content-Type added after signing is a frequent cause. Regenerate the URL after changing any signed value.

HTTP 400 or a redirect

Verify the bucket Region and endpoint used by the AWS client that generated the URL. Do not follow a redirect by changing the presigned URL manually; create it for the correct Region.

Access denied while generating the URL

The trusted service’s IAM principal may lack permission for the target bucket/key, or a bucket policy, encryption key policy or organization control may deny it. Inspect the server-side AWS error; the uploader cannot fix a URL that was never authorized.

The object is empty or truncated

Ensure the source stream is readable and positioned correctly, and keep it open until PutAsync finishes. A successful response should represent the complete object, so investigate local stream handling and any intermediary that might terminate the request.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The PDF downloads with the wrong metadata

Set Content-Type to application/pdf when creating and using the request, or update object metadata afterward. Confirm whether that header was part of the signature.

Retries fail after the first attempt

The URL may have expired, or a retry policy may be replaying a request with a consumed or altered stream. Create a fresh stream for each attempt and obtain a new presigned URL when its lifetime has elapsed.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your next task is producing a clean screenshot of a web page that documents or links to the uploaded PDF, ScreenshotNeo provides a separate website screenshot API. It is not an S3 uploader, but it can remove cookie banners, newsletter popups and chat widgets before capture. Only clean shots are billed; bot checks, blank pages and failed loads are not billed. Its MCP server lets AI agents use take_screenshot, get_page_info and capture_pdf.

One GET request returns an image or PDF:

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

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo documentation for all options. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Security checklist

  • Generate URLs only in trusted code with least-privilege permissions.
  • Scope each URL to the intended bucket, key and PUT operation.
  • Use short, practical expiries and protect the URL like a temporary credential.
  • Validate file ownership and destination keys before issuing a URL.
  • Redact presigned query strings from logs and telemetry.
  • Sign and send encryption, checksum or metadata headers consistently.

Frequently Asked Questions

Can I upload a PDF directly from a browser with a presigned URL?

Yes. A browser or other client can issue the signed PUT without AWS credentials, but browser CORS and the exact signed headers must be configured for your bucket and request.

Does a successful PUT prove the file is a valid PDF?

No. S3 confirms that it accepted the bytes. Validate file type, size and application-level requirements before issuing the upload URL or after the object is stored.

Should I use multipart upload for every PDF?

No. The minimal presigned PUT pattern is appropriate for an ordinary single-object upload. Multipart handling is a separate design for larger objects and is not required by this workflow.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.