Data retention and privacy¶
The payload of a scan is an identity document, which is the most sensitive thing this service will ever be handed. What is kept of it, and for how long, is therefore a design decision rather than an operational detail. This page is the reasoning behind the one that was taken.
The short version. The picture is never written down. The extracted result is kept for as long as the request asked for, and a request may ask for nothing.
The image is never stored¶
The uploaded image lives in memory for the duration of the request. It is decoded, handed to the recognition engine, and gone when the response is written.
Nothing writes it to a disk, an object store or a log. No bucket of customer documents exists, no retention tier over one, and no procedure for deleting one. None of those would have anything to operate on.
The reasoning is that an image that exists somewhere is an obligation that exists somewhere. A store of identity documents has to be encrypted, access controlled, audited, backed up, and eventually deleted. Every one of those is a thing that can be got wrong. Not having the store is the only version of that work that cannot fail.
A consequence worth stating plainly: a scan cannot be re-run on our side. If you need the same document recognized again, the picture has to be sent again, because we do not have it.
The crops are not stored either¶
A recognition returns crops: the document, the holder's photograph, the signature. They travel in the response of the call that produced them, and nowhere else.
A scan read back later through GET /v1/scans/{id} comes back with every
image slot null, whatever the original call returned. That is not a permission
being withheld; the bytes do not exist to return.
The same is true of the engine's own assessment of the picture. A stored scan
reads quality.overall: not_checked rather than pass, because the picture
it would have been a verdict about was never kept.
Whatever your integration needs from a crop, it needs at the moment of the response.
What a retained scan actually is¶
What is written down, when a retention window is asked for, is the reading. That is the extracted values, the document and holder blocks, the zone's lines and verdict, the timing, and the outcome flags.
Beside the row sits one picture: a thumbnail at most 96 px on its longest side and at most 16 KiB encoded. It exists so that the operations log in the dashboard shows which document a row is about instead of an opaque identifier. It is small enough to identify a row and not a person, and it is readable in the dashboard rather than through the API. The re-encode drops every metadata block the source carried: no camera, no timestamp, no location.
A row is readable by the account that made it, through a live key. A sandbox key of the same account reads nothing. An account's history is a store of identity data, and the key handed to a contractor is not the key that opens it.
Zero retention writes nothing¶
A request carrying retain_hours: 0 produces no row at all.
That is a stronger statement than a row that expires immediately. Nothing is left to expire, nothing for a sweep to find, nothing in a backup taken that minute, and nothing to appear in an export. The result was computed, returned and forgotten.
It costs one thing, and the cost is worth knowing before it surprises anybody.
An Idempotency-Key sent with a zero-retention scan has no stored result to
replay. A retry under that key is refused with
idempotency_replay_unavailable rather than answered twice. The refusal is
the correct one: a replay that invented an answer would be worse.
The window runs from the scan¶
A retention window is counted from the moment the scan was made, not from the last time it was read. Reading a result does not extend it, and there is no way to extend it.
An account chooses a default window, and a request may name its own. An
explicit retain_hours always wins, including zero. The account setting
decides what happens when the request says nothing, which is every upload from
the dashboard.
Shortening the window applies to what is already stored. That is the part people expect least and want most. Choosing a shorter setting re-dates the existing rows in the same step, each measured from its own creation time. A scan made yesterday under a year-long window expires a day after it was made, once the window becomes a day. It does not get a fresh day.
Lengthening never resurrects anything. A row already gone stays gone, and a row written under a shorter window keeps the shorter one. The longer window governs what comes after it.
What deletion removes¶
Expiry is not a filter over reads. A background sweep physically deletes the rows whose window has passed, in bounded batches so the deletion never takes a long lock over a backlog.
What goes with the row is everything that was kept of the scan: the extracted result and the thumbnail beside it. A thumbnail held outside the database follows through a tombstone. It is queued in the same statement that deletes the row, so the object does not outlive it.
A shortened window is the case that makes the tombstone necessary. A row the change has pushed past its window is deleted now, and its picture is enqueued for deletion now. Neither waits out the tier the object was written under.
Why the default is long and the override is short¶
The account default is the longest window the service offers, and a request can name a shorter one. That combination looks backwards until the two audiences are separated.
The default serves the dashboard. Somebody who uploads a document in a browser opened the dashboard to see their history. A default that threw it away after a day would make that page empty.
The override serves the integration. A program knows what it is doing with each result, and it is the only party that can say whether this one needs keeping. An explicit value wins for that reason: the caller who named it knows more than the setting does.
The pattern that follows is worth naming. A long account window with
retain_hours: 0 on the traffic that does not need it keeps the dashboard
useful while storing almost nothing.
What never leaves the process¶
A recognition service has two ways of leaking what it was given: its logs and its failure reports. Both are constrained rather than trusted to discipline.
Bodies, payloads, files, images, secrets, tokens and personal data do not reach either. A failure reported for diagnosis carries the shape of what went wrong. Where, in which release, under which request identifier, and not the thing it went wrong on. The subject of a report is an internal identifier, and the scrubbing is a hook rather than a rule each caller has to remember.
The free sandbox has one more store, and it is the smallest one in the service. To stop one image being replayed endlessly on a free key, a digest of the image bytes is held briefly. A digest is not the image and cannot be turned back into one. It lives in an in-memory store with a short expiry, and no durable row is written for it.
What this is not¶
This page describes what the service does with your data. It is not a legal document, and it does not say which obligations apply to you.
The design above is meant to make those obligations smaller. The fewer copies of an identity document exist, the fewer places have to be described, secured and emptied. Where your own process sits is yours to decide. Control history retention shows how to configure the part that is ours.