How to Extract Text from a Scanned PDF Using PDF.co in C#
Scanned PDF documents usually contain page images rather than selectable text. This tutorial shows how to upload a scanned PDF to PDF.co, process it with OCR, and save the extracted text using C#.
The workflow runs asynchronously so it can accommodate documents that require additional OCR processing time.
Prerequisites
You will need:
- .NET 6 or later
- A PDF.co account and API key
- A scanned PDF document
- The
Newtonsoft.JsonNuGet package
You can use this PDF.co scanned PDF sample. Download it and save it in the project folder as ScannedPDF.pdf.
Step 1: Create the Project
Create a new console application:
dotnet new console -n ExtractTextFromScannedPdf
cd ExtractTextFromScannedPdfInstall Newtonsoft.Json:
dotnet add package Newtonsoft.JsonPlace ScannedPDF.pdf in the project directory.
Step 2: Configure Your PDF.co API Key
Copy your API key from the PDF.co dashboard and store it in the PDFCO_API_KEY environment variable.
On Windows PowerShell:
$env:PDFCO_API_KEY="YOUR_PDFCO_API_KEY"On macOS or Linux:
export PDFCO_API_KEY="YOUR_PDFCO_API_KEY"The application will read the key from this environment variable and send it in the x-api-key request header.
Step 3: Request a Presigned Upload URL
A local file must first be uploaded to PDF.co storage. The application requests a presigned URL from:
GET /v1/file/upload/get-presigned-urlEncode the filename separately with Uri.EscapeDataString() before adding it to the query string:
var fileName = Path.GetFileName(ScannedPdfLocalPath);
var encodedFileName = Uri.EscapeDataString(fileName);
var requestUri =
$"/v1/file/upload/get-presigned-url" +
$"?contenttype=application/octet-stream" +
$"&name={encodedFileName}";PDF.co returns:
presignedUrl: The URL used to upload the file.url: The PDF.co URL representing the uploaded source file.
Step 4: Upload the Scanned PDF
Upload the local file to the returned presigned URL with an HTTP PUT request:
private static async Task UploadFileAsync(string presignedUrl)
{
using var uploadClient = new HttpClient();
await using var fileStream = File.OpenRead(ScannedPdfLocalPath);
using var content = new StreamContent(fileStream);
content.Headers.ContentType =
new MediaTypeHeaderValue("application/octet-stream");
using var response = await uploadClient.PutAsync(presignedUrl, content);
response.EnsureSuccessStatusCode();
}A separate HTTP client is used for the upload because the presigned URL already contains the authorization information required by the storage service.
Step 5: Start the PDF-to-Text Job
Send the uploaded file URL to the PDF.co PDF-to-Text endpoint:
POST /v1/pdf/convert/to/textConfigure the request to run asynchronously and enable OCR:
var parameters = new Dictionary<string, object>
{
["name"] = Path.GetFileName(ExtractedTextLocalPath),
["url"] = uploadedFileUrl,
["lang"] = "eng",
["async"] = true,
["profiles"] =
"{ 'OCRMode': 'TextFromImagesAndVectorsAndRepairedFonts' }"
};The lang parameter identifies the OCR language. Use another supported language code or a combination such as eng+deu when appropriate.
The selected OCR mode extracts text from scanned images while also handling text represented through fonts and vector elements. Additional OCR modes are listed in the PDF.co Profiles documentation.
The complete endpoint configuration is documented on the PDF-to-Text API page.
Step 6: Check the Background Job
The conversion response includes a jobId. Send it to the Job Check endpoint:
POST /v1/job/checkThe job can return one of these statuses:
working: Processing is still underway.success: Processing has completed.failed: The job encountered an error.aborted: Processing was stopped.
While the status is working, wait briefly and check again. Use Task.Delay() so the application does not block the executing thread.
When the status becomes success, the response contains the URL of the generated text file. See the Background Job Check documentation for the response details.
Step 7: Add the Complete C# Code
Replace the contents of Program.cs with the following:
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Text;
namespace ExtractTextFromScannedPdf;
internal class Program
{
private static readonly string ApiKey =
Environment.GetEnvironmentVariable("PDFCO_API_KEY")
?? throw new InvalidOperationException(
"Set the PDFCO_API_KEY environment variable before running the application.");
private const string ScannedPdfLocalPath = "ScannedPDF.pdf";
private const string ExtractedTextLocalPath = "Extracted.txt";
private static readonly HttpClient PdfCoClient = new()
{
BaseAddress = new Uri("https://api.pdf.co")
};
private static async Task Main()
{
if (!File.Exists(ScannedPdfLocalPath))
{
throw new FileNotFoundException(
$"The source file '{ScannedPdfLocalPath}' was not found.");
}
PdfCoClient.DefaultRequestHeaders.Add("x-api-key", ApiKey);
Console.WriteLine("Requesting an upload URL...");
var uploadInfo = await GetPresignedUrlAsync();
if (string.IsNullOrWhiteSpace(uploadInfo.PresignedUrl) ||
string.IsNullOrWhiteSpace(uploadInfo.Url))
{
throw new InvalidOperationException(
"PDF.co did not return the required upload URLs.");
}
Console.WriteLine("Uploading the scanned PDF...");
await UploadFileAsync(uploadInfo.PresignedUrl);
Console.WriteLine("Starting the OCR conversion...");
var conversionJob = await StartConversionAsync(uploadInfo.Url);
if (string.IsNullOrWhiteSpace(conversionJob.JobId))
{
throw new InvalidOperationException(
"PDF.co did not return a job ID.");
}
Console.WriteLine($"Job ID: {conversionJob.JobId}");
var completedJob = await WaitForJobAsync(conversionJob.JobId);
var resultUrl = completedJob.Url ?? conversionJob.Url;
if (string.IsNullOrWhiteSpace(resultUrl))
{
throw new InvalidOperationException(
"PDF.co did not return the extracted text URL.");
}
Console.WriteLine("Downloading the extracted text...");
using var downloadClient = new HttpClient();
var extractedText = await downloadClient.GetStringAsync(resultUrl);
await File.WriteAllTextAsync(ExtractedTextLocalPath, extractedText);
Console.WriteLine(
$"Extraction completed. Text saved to '{ExtractedTextLocalPath}'.");
}
private static async Task<ApiResponse> GetPresignedUrlAsync()
{
var fileName = Path.GetFileName(ScannedPdfLocalPath);
var encodedFileName = Uri.EscapeDataString(fileName);
var requestUri =
$"/v1/file/upload/get-presigned-url" +
$"?contenttype=application/octet-stream" +
$"&name={encodedFileName}";
using var response = await PdfCoClient.GetAsync(requestUri);
return await ReadApiResponseAsync(response);
}
private static async Task UploadFileAsync(string presignedUrl)
{
using var uploadClient = new HttpClient();
await using var fileStream = File.OpenRead(ScannedPdfLocalPath);
using var content = new StreamContent(fileStream);
content.Headers.ContentType =
new MediaTypeHeaderValue("application/octet-stream");
using var response = await uploadClient.PutAsync(presignedUrl, content);
response.EnsureSuccessStatusCode();
}
private static async Task<ApiResponse> StartConversionAsync(
string uploadedFileUrl)
{
var parameters = new Dictionary<string, object>
{
["name"] = Path.GetFileName(ExtractedTextLocalPath),
["url"] = uploadedFileUrl,
["lang"] = "eng",
["async"] = true,
["profiles"] =
"{ 'OCRMode': 'TextFromImagesAndVectorsAndRepairedFonts' }"
};
using var content = CreateJsonContent(parameters);
using var response = await PdfCoClient.PostAsync(
"/v1/pdf/convert/to/text",
content);
return await ReadApiResponseAsync(response);
}
private static async Task<ApiResponse> CheckJobAsync(string jobId)
{
var parameters = new Dictionary<string, string>
{
["jobid"] = jobId
};
using var content = CreateJsonContent(parameters);
using var response = await PdfCoClient.PostAsync(
"/v1/job/check",
content);
return await ReadApiResponseAsync(response);
}
private static async Task<ApiResponse> WaitForJobAsync(string jobId)
{
while (true)
{
var job = await CheckJobAsync(jobId);
switch (job.Status?.ToLowerInvariant())
{
case "success":
Console.WriteLine("OCR processing completed.");
return job;
case "working":
Console.WriteLine("OCR processing is still underway...");
await Task.Delay(TimeSpan.FromSeconds(5));
break;
case "failed":
case "aborted":
throw new InvalidOperationException(
$"PDF.co job {job.Status}: {job.Message}");
default:
throw new InvalidOperationException(
$"Unexpected PDF.co job status: {job.Status ?? "unknown"}");
}
}
}
private static StringContent CreateJsonContent(object value)
{
var json = JsonConvert.SerializeObject(value);
return new StringContent(json, Encoding.UTF8, "application/json");
}
private static async Task<ApiResponse> ReadApiResponseAsync(
HttpResponseMessage response)
{
var json = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"PDF.co returned HTTP {(int)response.StatusCode}: {json}");
}
var result = JsonConvert.DeserializeObject<ApiResponse>(json)
?? throw new InvalidOperationException(
"PDF.co returned an empty or invalid response.");
if (result.Error)
{
throw new InvalidOperationException(
$"PDF.co error: {result.Message ?? "Unknown error"}");
}
return result;
}
}
internal sealed class ApiResponse
{
[JsonProperty("error")]
public bool Error { get; set; }
[JsonProperty("message")]
public string? Message { get; set; }
[JsonProperty("presignedUrl")]
public string? PresignedUrl { get; set; }
[JsonProperty("url")]
public string? Url { get; set; }
[JsonProperty("jobId")]
public string? JobId { get; set; }
[JsonProperty("status")]
public string? Status { get; set; }
}Step 8: Run the Application
Run the project:
dotnet runThe application will:
- Request a presigned upload URL.
- Upload
ScannedPDF.pdf. - submit the document for OCR processing.
- Monitor the background job.
- Download the extracted text.
- Save it as
Extracted.txt.
Open Extracted.txt to review the OCR result.
Troubleshooting
The source file cannot be found
Make sure ScannedPDF.pdf is located in the project directory from which dotnet run is executed.
The API key is missing
Verify that the PDFCO_API_KEY environment variable is set in the same terminal session used to run the project.
The extracted text is inaccurate
Confirm that lang matches the document language. You can also adjust OCR resolution or apply image-preprocessing filters through the profiles parameter.
The job remains in progress
Larger documents and high-resolution scans can take longer to process. The application will continue checking the job every five seconds until PDF.co reports a final status.
Conclusion
You have created a C# application that uploads a scanned PDF, extracts its text with PDF.co OCR, monitors asynchronous processing, and saves the result locally. The same workflow can be extended to process multiple documents, select specific pages, use multiple OCR languages, or send the extracted text to another application.
Related Tutorials

