Control history retention

A recognition result can be read back later, and how long it stays readable is your choice. This guide covers the account default, the per-request override, and what shortening a window does to results you already have.

Pick the account default

Open the dashboard at https://doc.cheap/app/settings and choose one of four windows.

Setting Hours Pick it when
24 hours 24 A result is consumed the same day and nothing needs it after that
7 days 168 A case is worked within a week
1 month 720 A result is evidence for a decision that can be reopened
1 year 8760 You want the longest history the service offers

A new account starts on 1 year. The setting applies to every scan made with a live key that named no window of its own. Every upload made from the dashboard is one of those.

Override it on one request

Send options.retain_hours when one request must differ from the account setting. The value is a whole number of hours from 0 to 8760.

An explicit value always wins, including zero. The account setting decides only what happens when the request is silent.

curl

curl -X POST https://api.doc.cheap/v1/scans \
  -H "Authorization: Bearer sk_sandbox_public" \
  -H "Content-Type: application/json" \
  -d "{\"image\": \"$(base64 < document.jpg | tr -d '\n')\",
       \"options\": {\"retain_hours\": 24}}"

JavaScript

import { readFileSync } from "node:fs";

const image = readFileSync("document.jpg").toString("base64");

const response = await fetch("https://api.doc.cheap/v1/scans", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_sandbox_public",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ image, options: { retain_hours: 24 } }),
});

const scan = await response.json();
console.log(scan.meta.id, scan.meta.created_at);

Python

import base64
import json
import urllib.request

with open("document.jpg", "rb") as file:
    image = base64.b64encode(file.read()).decode()

body = {"image": image, "options": {"retain_hours": 24}}

request = urllib.request.Request(
    "https://api.doc.cheap/v1/scans",
    data=json.dumps(body).encode(),
    headers={
        "Authorization": "Bearer sk_sandbox_public",
        "Content-Type": "application/json",
    },
)

with urllib.request.urlopen(request) as response:
    status = response.status
    scan = json.load(response)

print(scan["meta"]["id"], scan["meta"]["created_at"])

The window runs from meta.created_at, not from the last read. Reading a result does not extend it.

Note. The examples run on the public sandbox key so they work as printed, but that key does not apply retain_hours. Every scan made on it is kept for one year with its thumbnail, whatever the request names. It cannot be read back, listed or deleted through the API. The window you name takes effect on your own live key. See the public sandbox key.

Keep nothing at all

Send retain_hours: 0. No row is written, so nothing expires and nothing has to be deleted later.

curl

curl -X POST https://api.doc.cheap/v1/scans \
  -H "Authorization: Bearer sk_sandbox_public" \
  -H "Content-Type: application/json" \
  -d "{\"image\": \"$(base64 < document.jpg | tr -d '\n')\",
       \"options\": {\"retain_hours\": 0}}"

JavaScript

import { readFileSync } from "node:fs";

const image = readFileSync("document.jpg").toString("base64");

const response = await fetch("https://api.doc.cheap/v1/scans", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_sandbox_public",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ image, options: { retain_hours: 0 } }),
});

const scan = await response.json();
console.log(scan.meta.billed, scan.holder.full_name);

Python

import base64
import json
import urllib.request

with open("document.jpg", "rb") as file:
    image = base64.b64encode(file.read()).decode()

body = {"image": image, "options": {"retain_hours": 0}}

request = urllib.request.Request(
    "https://api.doc.cheap/v1/scans",
    data=json.dumps(body).encode(),
    headers={
        "Authorization": "Bearer sk_sandbox_public",
        "Content-Type": "application/json",
    },
)

with urllib.request.urlopen(request) as response:
    status = response.status
    scan = json.load(response)

print(scan["meta"]["billed"], scan["holder"]["full_name"])

A zero-retention scan on a live key is still charged and still counted in usage. What it does not do is leave a row behind. On the public sandbox key 0 is not applied: the scan is kept for one year, as the note above says.

Warning. retain_hours: 0 also removes the replay an Idempotency-Key would have served. For 24 hours a retry under the same key answers 409 idempotency_replay_unavailable, because nothing was kept to replay. After that the key is forgotten, and a retry under it is a new scan, charged again.

Read a result back

GET /v1/scans/{id} returns a scan while its window lasts, and 404 after it. Only a live key reads stored scans. A sandbox key answers 404 for every id, including its own.

curl

curl https://api.doc.cheap/v1/scans/01a0af18-cd8d-7a61-9f2d-4c7b8e105da3 \
  -H "Authorization: Bearer sk_sandbox_public"

JavaScript

const response = await fetch(
  "https://api.doc.cheap/v1/scans/01a0af18-cd8d-7a61-9f2d-4c7b8e105da3",
  { headers: { Authorization: "Bearer sk_sandbox_public" } },
);

const body = await response.json();
console.log(body.error.code, body.error.docs_url);

Python

import json
import urllib.error
import urllib.request

request = urllib.request.Request(
    "https://api.doc.cheap/v1/scans/01a0af18-cd8d-7a61-9f2d-4c7b8e105da3",
    headers={"Authorization": "Bearer sk_sandbox_public"},
)

try:
    with urllib.request.urlopen(request) as response:
        status = response.status
        body = json.load(response)
except urllib.error.HTTPError as error:
    status = error.code
    body = json.load(error)

print(status, body["error"]["code"])

List what is still readable

GET /v1/scans returns the account's history rows, most recent first. A row is the summary a list view needs, not the whole result.

Key Carries
id The scan id, which is what GET /v1/scans/{id} takes
status The recognition status
billed Whether the scan drew a credit
duration_ms Server-side processing time
reference Your own string, echoed back
created_at When the scan was made, and what the window runs from

Only scans made with a live key under a non-zero window are listed, and only while that window lasts. A sandbox key sees an empty list rather than the account's.

The list comes a page at a time. limit sets the page size, from 1 to 100, and defaults to 100. Each page carries next_cursor: send it back as cursor to read the next page. It is null on the last one.

curl "https://api.doc.cheap/v1/scans?limit=20" \
  -H "Authorization: Bearer sk_live_your_key"

A limit outside the range, or a cursor this API did not issue, is refused with 422 validation_failed rather than quietly answered with a different page.

The extracted data and the crops are not in a row. Read one scan back by id when you need them.

Delete a result before its window ends

DELETE /v1/scans/{id} removes one stored scan now: the result, its history row and its thumbnail. Only a live key deletes, and only its own account's scans. It answers with the id and "deleted": true.

curl -X DELETE https://api.doc.cheap/v1/scans/01a0af18-cd8d-7a61-9f2d-4c7b8e105da3 \
  -H "Authorization: Bearer sk_live_your_key"

The deletion is final. The scan can no longer be read, listed or replayed. For 24 hours, a retry under the Idempotency-Key that created it answers 409 idempotency_replay_unavailable. It does not recognize the image again. The credit the scan drew is not returned.

An id that was never stored, belongs to another account, has passed its window or was already deleted answers 404 not_found. A sandbox key answers 404 for every id.

Choose a window for the job

Match the window to what the result is for, rather than keeping everything for a year because that is the default.

Setting a long window on the account and sending retain_hours: 0 for the traffic that does not need it is a working pattern. The override is per request, so the two do not fight.

Weigh the window against what a stored row is. It is identity-document data about a real person, and the shortest window that does your job is the one to pick.

What is kept, and what is not

A stored scan is the reading, not the picture.

Shortening the window back-dates what you already have

Choosing a shorter setting applies to the results already stored, in the same step as the choice itself.

Each row is measured from its own meta.created_at. A scan made yesterday under the 1 year setting expires one day after it was made once the setting becomes 24 hours. It does not get a fresh day.

Rows the change has already pushed past their window go immediately, and their thumbnails are queued for deletion in the same step. A person who asks for less history gets less history now.

Lengthening the window never brings anything back. A row already deleted stays deleted, and a row written under a shorter window keeps the shorter one. The new setting governs the scans made after it.

Next