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: 0also removes the replay anIdempotency-Keywould have served. For 24 hours a retry under the same key answers 409idempotency_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.
- A result consumed on receipt needs none. Send
retain_hours: 0and store what you need in your own system. - A result a support agent may be asked about needs the window your support promise covers, plus a margin.
- A result that backs a decision somebody can reopen needs the window that process runs for.
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.
- The image crops are never written down. A scan read back answers with every
slot of
imagesset to null, whatever the original call returned. - The engine's own quality measurement is not kept either, so
quality.overallreadsnot_checkedon a read-back rather thanpass. - A 96 px thumbnail of at most 16 KiB is kept beside the row. It shows which document a row is about in the dashboard's operations log, and it is not readable through the API.
- Everything else comes back as it was sent to you:
meta,document,holder,fieldsandmrz.
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¶
- Data retention and privacy – why the image is never stored.
- Scan options – every option and its default.
- Limits – the range
retain_hoursaccepts.