---
title: "Save signed PDFs directly to a remote server on Android"
canonical_url: "https://www.nutrient.io/guides/android/knowledge-base/save-signed-pdfs-to-remote-server/"
md_url: "https://www.nutrient.io/guides/android/knowledge-base/save-signed-pdfs-to-remote-server.md"
last_updated: "2026-05-15T19:10:04.908Z"
description: "Learn how to capture and flatten PDF signatures in your Android app, store them securely in cache, and upload the signed PDF to a server — without saving files locally."
---

# Save signed PDFs directly to a remote server on Android

Many apps enable users to add annotations to PDFs (for example, signatures on contracts, stamps on delivery confirmations) and save the modified version directly to a remote server without storing files locally.

This guide explains how to:

1. Capture user annotations (such as signatures).

2. Flatten them into the PDF (making them permanent).

3. Securely upload the modified PDF to your server.

## Key challenge

By default, annotations exist as separate layers. Without flattening, uploaded PDFs may appear unsigned or missing annotations when viewed in other PDF readers.

## Solution overview

1. Flatten annotations — Merge signatures/annotations into the PDF content.

2. Process asynchronously — Avoid user interface (UI) freezes during PDF processing.

3. Upload from cache — Temporarily save the modified PDF to the app’s cache (not device storage).

## Implementation

Follow the steps mentioned below to implement the solution.

### Step 1: Flatten annotations

Use [`PdfProcessorTask`](https://www.nutrient.io/api/android/nutrient/com.pspdfkit.document.processor/-pdf-processor-task/index.html) to flatten annotations into the PDF:

```kotlin

val task = PdfProcessorTask.fromDocument(document).changeAllAnnotations(PdfProcessorTask.AnnotationProcessingMode.FLATTEN)

```

### Step 2: Process and save to cache

Generate the flattened PDF in the app’s cache directory with progress tracking:

```kotlin

val outputFile = File(context.cacheDir, "signed.pdf")

PdfProcessor.processDocumentAsync(task, outputFile).onBackpressureDrop() // Handle Flowable backpressure..subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(
        { progress ->
            // Track progress during processing.
            val percentComplete = (progress.pagesProcessed * 100) / progress.totalPages
            updateProgressIndicator(percentComplete)
        },
        { error ->
            // Handle processing errors.
            Log.e(TAG, "PDF processing failed", error)
            when (error) {
                is PdfProcessorException -> {
                    showErrorMessage("Failed to process PDF: ${error.message}")
                }
                else -> {
                    showErrorMessage("Unexpected error: ${error.message}")
                }
            }
        },
        {
            // Upload to server after processing completes.
            lifecycleScope.launch(Dispatchers.IO) {
                try {
                    uploadToServer(outputFile)
                    withContext(Dispatchers.Main) {
                        showSuccessMessage()
                    }
                } catch (e: IOException) {
                    withContext(Dispatchers.Main) {
                        showErrorMessage("Upload failed: ${e.message}")
                    }
                } finally {
                    outputFile.delete() // Clean up cache file
                }
            }
        }
    )

```

### Step 3: Upload to server

Implement your server upload logic. Below is an example using Retrofit:

```kotlin

private suspend fun uploadToServer(file: File) {
    val requestBody = file.asRequestBody("application/pdf".toMediaType())
    val part = MultipartBody.Part.createFormData("file", file.name, requestBody)

    val response = yourApiService.uploadSignedPdf(part)
    if (!response.isSuccessful) {
        throw IOException("Upload failed with code: ${response.code()}")
    }
}

```

### Alternative: Synchronous processing with Kotlin coroutines

Alternatively, you can use synchronous processing within a coroutine:

```kotlin

lifecycleScope.launch(Dispatchers.IO) {
    val outputFile = File(context.cacheDir, "signed.pdf")

    try {
        // Process synchronously on IO thread.
        PdfProcessor.processDocument(task, outputFile)

        // Upload to server.
        uploadToServer(outputFile)

        withContext(Dispatchers.Main) {
            showSuccessMessage()
        }
    } catch (e: PdfProcessorException) {
        withContext(Dispatchers.Main) {
            showErrorMessage("Processing failed: ${e.message}")
        }
    } catch (e: IOException) {
        withContext(Dispatchers.Main) {
            showErrorMessage("Upload failed: ${e.message}")
        }
    } finally {
        outputFile.delete()
    }
}

```

## Why the solution works

- Flattening — Converts annotations (for example, signatures, stamps) into permanent PDF content that’s visible in all PDF readers.

- Cache directory — Avoids permanent local storage; files are temporary and auto-cleared by the system.

- Async workflow — Uses RxJava Flowable with backpressure handling to prevent UI blocking during processing/upload.

- Progress tracking — [`ProcessorProgress`](https://www.nutrient.io/api/android/nutrient/com.pspdfkit.document.processor/-pdf-processor/-processor-progress/index.html) provides real-time updates on processing status.

## Best practices

We recommend the following best practices when implementing this solution.

### Error handling

Process `PdfProcessorException` separately:

```kotlin

when (error) {
    is PdfProcessorException -> {
        // PDF processing specific error.
        Log.e(TAG, "Processing error: ${error.detailedMessage}")
    }
    is IOException -> {
        // Network or file I/O error.
        Log.e(TAG, "I/O error: ${error.message}")
    }
}

```

Implement retry logic with exponential backoff:

```kotlin

suspend fun uploadWithRetry(file: File, maxRetries: Int = 3) {
    var delay = 1000L
    repeat(maxRetries) { attempt ->
        try {
            uploadToServer(file)
            return // Success.
        } catch (e: IOException) {
            if (attempt == maxRetries - 1) throw e
            delay(delay)
            delay *= 2 // Exponential backoff.
        }
    }
}

```

Validate server responses:

```kotlin

val response = yourApiService.uploadSignedPdf(part)
when {
    response.isSuccessful -> {
        Log.d(TAG, "Upload successful")
    }
    response.code() == 413 -> {
        throw IOException("File too large for server")
    }
    response.code() >= 500 -> {
        throw IOException("Server error: ${response.code()}")
    }
    else -> {
        throw IOException("Upload failed: ${response.code()}")
    }
}

```

### Security

Always delete cached files after upload:

```kotlin

try {
    uploadToServer(outputFile)
} finally {
    // Ensure file is deleted even if upload fails.
    outputFile.delete()
}

```

Encrypt PDFs during upload for sensitive documents:

```kotlin

// Use HTTPS endpoints (enforced by default in Android 9+).
interface ApiService {
    @Multipart
    @POST("https://secure.example.com/upload")
    suspend fun uploadSignedPdf(@Part file: MultipartBody.Part): Response<ResponseBody>
}

```

Verify SSL certificates:

```kotlin

val client = OkHttpClient.Builder().certificatePinner(CertificatePinner.Builder().add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=").build()).build()

```

### Performance

Use backpressure handling. The `.onBackpressureDrop()` operator prevents overwhelming the UI thread with progress updates for large PDFs.

Show cancellable progress dialog:

```kotlin

val progressDialog = ProgressDialog(context).apply {
    setMessage("Processing PDF...")
    setCancelable(true)
    setOnCancelListener {
        // Dispose of RxJava subscription.
        processingDisposable?.dispose()
    }
    show()
}

```

Monitor processing time for large documents:

```kotlin

val startTime = System.currentTimeMillis()
PdfProcessor.processDocumentAsync(task, outputFile).subscribe(
        { /* progress */ },
        { /* error */ },
        {
            val duration = System.currentTimeMillis() - startTime
            Log.d(TAG, "Processing completed in ${duration}ms")
        }
    )

```

## Troubleshooting

**OutOfMemoryError during processing**

- Process fewer pages at a time using [`PdfProcessorTask.newPage`](https://www.nutrient.io/api/android/nutrient/com.pspdfkit.document.processor/-pdf-processor-task/new-page.html).

- Reduce image quality before flattening.

- Enable `android:largeHeap="true"` in `AndroidManifest.xml`.

**Upload fails with timeout**

- Increase OkHttp timeout settings.

- Compress PDFs before upload using [`DocumentSaveOptions.setRewriteAndOptimizeFileSize(true)`](https://www.nutrient.io/api/android/nutrient/com.pspdfkit.document/-document-save-options/set-rewrite-and-optimize-file-size.html).

- Consider chunked upload for large files.

**Annotations not visible after upload**

- Verify flattening completed successfully.

- Check that `AnnotationProcessingMode.FLATTEN` (not `KEEP`) is used.

- Test the uploaded PDF in multiple PDF readers.

## Related resources

- [Flatten annotations guide](https://www.nutrient.io/guides/android/annotations/flatten.md) — Detailed annotation flattening options

- [Save to remote server](https://www.nutrient.io/guides/android/save-a-document/to-remote-server.md) — Upload PDFs without flattening

- [`PdfProcessor` API reference](https://www.nutrient.io/api/android/nutrient/com.pspdfkit.document.processor/-pdf-processor/index.html) — Complete processor documentation

- [Android file upload best practices](https://developer.android.com/training/data-storage/use-cases#upload-data) — Google’s file upload guidelines
---

## Related pages

- [Allow Clear Text Traffic](/guides/android/knowledge-base/allow-clear-text-traffic.md)
- [Compose Qna](/guides/android/knowledge-base/compose-qna.md)
- [Disabling Annotation Rotation](/guides/android/knowledge-base/disabling-annotation-rotation.md)
- [Custom Print Functionality](/guides/android/knowledge-base/custom-print-functionality.md)
- [Deleting Pages From A Pdf](/guides/android/knowledge-base/deleting-pages-from-a-pdf.md)
- [Easily disable share and print options in Android](/guides/android/knowledge-base/disable-share-documentinfo-print.md)
- [Managing touch scrolling in Compose containers](/guides/android/knowledge-base/document-view-inside-pager-scroll-handling.md)
- [Getting All Digital Signatures](/guides/android/knowledge-base/getting-all-digital-signatures.md)
- [Change page layout dynamically based on orientation](/guides/android/knowledge-base/dynamic-page-layout-when-changing-orientation.md)
- [Getting Signature Location](/guides/android/knowledge-base/getting-signature-location.md)
- [Invoke Search Programmatically](/guides/android/knowledge-base/invoke-search-programmatically.md)
- [Install Failed Insufficient Storage](/guides/android/knowledge-base/install-failed-insufficient-storage.md)
- [Intercepting Touch Events](/guides/android/knowledge-base/intercepting-touch-events.md)
- [Creating invisible digital signatures in Android](/guides/android/knowledge-base/invisible-signature.md)
- [Invoking Share Action Programmatically](/guides/android/knowledge-base/invoking-share-action-programmatically.md)
- [Making Form Elements Read Only](/guides/android/knowledge-base/making-form-elements-read-only.md)
- [Override Hyperlink Behavior](/guides/android/knowledge-base/override-hyperlink-behavior.md)
- [Runtime Permissions Cordova](/guides/android/knowledge-base/runtime-permissions-cordova.md)
- [Remove Tool Variant From Toolbar](/guides/android/knowledge-base/remove-tool-variant-from-toolbar.md)
- [Customize the overflow button color in Android](/guides/android/knowledge-base/styling-overflow-button.md)
- [Using Pspdfkit With Dynamic Feature Modules](/guides/android/knowledge-base/using-pspdfkit-with-dynamic-feature-modules.md)

