API Scan

API Scan turns a PDF into a realistic scanned copy through a REST call, which suits automated pipelines and app integrations. Create a job, upload the PDF, then poll or wait for the webhook — three steps, from any environment or language that can send an HTTP request. Colour space, resolution, rotation, blur, noise, brightness, contrast and borders are all yours to set.

How a call works

  1. Create the job

    POST /v1/scan-jobs

    Send your config and an optional webhookUrl, and you get back a jobID and a presigned uploadURL.

  2. Upload the PDF

    PUT {uploadURL}

    PUT the file straight to the presigned S3 address from the previous step — no token needed.

  3. Collect the scanned copy

    GET /v1/scan-jobs/{jobID}

    Poll the status or wait for the webhook; once the job is completed, download it from downloadURL.

Where it fits

Server-side batch output

Contracts, invoices and reports generated on the server go straight through the scan effect, with nobody repeating the run by hand on the web page.

Inside an existing system

Add an “export a scanned copy” action to a CRM, ERP or ticketing system and let it call the API.

Automation pipelines

CI, n8n, Zapier and the like start a job on an event, and the webhook hands off to the next step when it finishes.

Large queues of files

Jobs are asynchronous: create them and each is processed on its own, with progress available by status and createdAfter.

Languages and environments

TypeScriptfetch / axios
Node.jsfetch / undici
Pythonrequests / httpx
Gonet/http
PHPcURL / Guzzle
Rubynet/http
JavaHttpClient
cURLCommand line / CI
MoreAny HTTP client

The API is plain HTTP and JSON, so any language or automation platform that can send a request can call it.

Code examples

API Bearer Token

The token belongs to your account and can be regenerated at any time. API Scan needs a Pro account: without a valid token the API answers 401, and without the Pro role it answers 403.

Try it out

Set the parameters, watch the request body update as you go, then run the three calls against the API.

Scan parameters

A trial run calls the API with your token and needs a Pro account; the parameters and the request body are free to browse.

POST/v1/scan-jobs
{
  "config": {
    "rotate": 1,
    "rotate_var": 0.5,
    "colorspace": "gray",
    "blur": 0,
    "noise": 0,
    "border": false,
    "brightness": 1.3,
    "contrast": 1.3,
    "resolution": 150,
    "output_format": "image/jpeg"
  }
}

Scan job info

example
{
  "jobID": "3f9c1e64-0000-4000-8000-00000000a71b",
  "userID": "8f21c4b0-0000-4000-8000-000000004a17",
  "createdAt": 1724409600,
  "status": "completed",
  "inputUploadedAt": 1724409601,
  "completedAt": 1724409602,
  "numPages": 6,
  "downloadURL": "https://…/output/3f9c.pdf?X-Amz-…"
}

API reference

MethodPathDescription
POST/v1/scan-jobsCreate a scan job. Send config and an optional webhookUrl; you get back the job object with status created and a presigned uploadURL.
PUT{uploadURL}The presigned S3 address from the previous step, not on api.lookscanned.ioUpload the source PDF with Content-Type: application/pdf and Content-Length. The address carries its own signature, so do not add an Authorization header.
GET/v1/scan-jobs/{jobID}Read a single job, for polling. While the status is created it carries uploadURL; once it is completed it carries downloadURL.
GET/v1/scan-jobsList your own jobs, filtered by jobID, status or createdAfter.
Statuscreatedprocessingcompletedfailed
  • 401 no valid token
  • 403 the account is not Pro
  • 404 no such job

Request body

FieldTypeDefaultDescription
webhookUrlstring · —Called once when the job finishes, so you do not have to poll for it.
config.colorspace'gray' | 'sRGB' · graygrayColour space of the output image; gray is a black-and-white scan.
config.resolutionnumber · 7272Resolution of the output image, in DPI.
config.rotatenumber · —Rotation of the whole document, in degrees.
config.rotate_varnumber · —Range of the random per-page rotation, in degrees — the look of paper laid down crooked.
config.blurnumber · 00Amount of blur.
config.noisenumber · 00Amount of noise.
config.brightnessnumber · 11Brightness; 1 leaves it unchanged.
config.contrastnumber · 11Contrast; 1 leaves it unchanged.
config.borderboolean · falsefalseWhether to add a scan border to the page.
config.output_format'image/png' | 'image/jpeg' · image/jpegimage/jpegImage format the pages are rendered to.

Every field can be left out. The values “Try it out” starts from — resolution 150, rotation 1, brightness and contrast 1.3 — are the combination the web app recommends, not the API's defaults.

Fields to watch in the job object

status
created / processing / completed / failed — decides whether the two addresses below are present.
uploadURL
Only while created. A presigned upload address that expires.
downloadURL
Only once completed. A presigned download address that expires.
inputUploadedAt / completedAt
When the source finished uploading and when the job finished; the difference is the processing time.

Frequently asked questions

Does API Scan need Pro?

Yes. Without a valid token the API answers 401, and an account without the Pro role gets 403. Upgrade and sign in, and the token is on this page.

How do I know when a job is done?

Two ways: poll GET /v1/scan-jobs/{jobID}, or pass a webhookUrl when you create the job and let the service call you back once.

Is the result the same as scanning on the web page?

Yes. Both use the same scan-effect implementation, and colour space, resolution, rotation, blur, noise, brightness, contrast and border in config are the web page's options under other names — the same parameters give the same output. Only the place the work happens differs: locally on the page, remotely through the API.

Can I store the upload and download addresses and reuse them?

Better not to. uploadURL and downloadURL are presigned addresses with a lifetime; once they expire you have to read the job again to get fresh ones.

How long does a job take?

It depends on the page count and the resolution. A few pages usually finish within seconds, and higher resolutions or longer documents take longer. The difference between inputUploadedAt and completedAt is the real elapsed time.

What if a job fails?

The status becomes failed. The usual causes are a file that is not a valid PDF, encryption restrictions, or an interrupted upload. Check that the file opens, then create a new job.

Can I look up past jobs?

Yes. GET /v1/scan-jobs lists your own jobs and can be filtered by jobID, status and createdAfter, which is enough for reconciliation or a repeat download.