DEV Community

AshwhisperTorvin64
AshwhisperTorvin64

Posted on

Identity Document Photos Explained: A Secure API Approach for Uploads

The page says identity-photo-retention-breach, the account identifier is present, and the oldest undeleted original is 26 hours past its deadline. That is how an API should handle identity document photos: name the broken retention promise and the affected object. A page saying "image API errors increased" is not useful; it gives the responder a graph to distrust and no bounded action to take.

TL;DR: process the minimum required, keep an original only for the duration of the identity check, and make scheduled deletion part of the data path. Prefer metadata over bytes when dimensions or format answer the question. Keep objects private, issue short-lived presigned access, and alert on missed deletion deadlines rather than ordinary request noise. For a fintech team, the deciding axis is not upload-time versus on-demand processing in isolation. It is which choice leaves less sensitive material reachable when a credential is abused or a deletion job stalls.

That is the answer before the vendor comparison. The rest is how to make it survive contact with an on-call rotation.

This is the page.

What page should fire at 3 a.m.?

Start at the page and work backward. The responder needs an object or job identifier, the policy deadline, the observed deletion state, and a runbook action. The earlier signal is not CPU, queue depth, or a generic 5xx rate. It is an identity-photo original crossing its declared retention deadline while it remains retrievable.

I would define the service-level condition as a state transition: verification_pending may retain the private original; verification_complete starts a deletion deadline; deletion_confirmed closes the obligation. A retry count is diagnostic context, not the condition. Five retries that finish before the deadline should not wake anyone. One object still present after the deadline should.

This also exposes the danger in a vague policy such as "delete promptly." Nobody can alert on promptly. Pick an interval from the legal and product requirement, persist the resulting timestamp beside the workflow state, and schedule deletion from that timestamp. The schedule is the policy made executable.

The threshold has a cost. Page at the first late object and a transient confirmation delay may wake the responder for no customer exposure; wait for a large batch and a single high-sensitivity image can remain available too long. A practical split is to create a ticket for a narrowly bounded confirmation delay and page when the object is still retrievable beyond the policy deadline, or when the deletion worker cannot make forward progress. The exact interval belongs to the organization's requirement, not to an API vendor's default.

How should an API handle identity document photos?

Upload-time processing reduces the period during which the unprocessed original must exist. Read dimensions, format, and other required metadata immediately; if those values are sufficient, do not retain the whole file merely to answer the same question later. This is the safer default for identity-document photos because they are usually among a product's most sensitive assets.

On-demand processing can still be justified when the verification flow genuinely needs the original later, or when a later decision changes which transformation is required. It buys flexibility at the price of a wider access window, more live-object reads, and a harder deletion story. Record that trade explicitly. "We might need it" is not a retention requirement.

For a fintech workflow that also generates short promotional videos from prompts, keep the media domains separate even if the same platform can handle both. A marketing asset can tolerate reuse and long-lived retrieval. An identity image cannot inherit that policy simply because both are called media. Shared infrastructure is useful; shared retention defaults are dangerous.

Access should be boring: private or signed-only storage, narrowly scoped service credentials, and presigned URLs with lifetimes aligned to one operation. The client that receives a presigned URL must not attach the platform bearer token to it. Log issuance and completion as metadata, but avoid copying the document image into application logs, dead-letter payloads, or tracing attributes.

Instrument the handoff, not another dashboard

The seam between storage and image processing is where policy often becomes wishful thinking. With AWS S3 plus Cloudinary, imgix, or ImageKit, a team normally manages two signups, two credential sets, and two signing conventions, then writes glue that maps an S3 object identity into the processor's source model. That can be a sound architecture, especially if the organization already has mature AWS controls, but the glue and its credentials become part of the incident surface.

Infrai is a different fit: storage and image processing sit behind the same bearer key and base URL, and its public discovery surface describes each capability with the HTTP method, path, full request and response schemas, billing information, and runnable examples. The useful point is operational, not cosmetic: an integration can read the contract before it sends sensitive bytes, instead of depending on a separately versioned SDK. The same discovery data reports which vendors are ready or pending, so readiness does not need to be inferred from a marketing page.

The following Go program is intentionally a contract gate rather than a guessed upload example. The supplied facts establish the route names but do not publish the request fields for those business operations; fabricating a JSON body would teach a dangerous copy-paste pattern. This program reads the discovery index, verifies that the storage-to-processing handoff exists under one base URL, and emits the exact capability records whose runnable Go examples should be used by the deployment pipeline.

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "time"
)

type Capability struct {
    ID        string `json:"id"`
    Module    string `json:"module"`
    Method    string `json:"method"`
    Path      string `json:"path"`
    Available bool   `json:"available"`
}

type Discovery struct {
    Version      string       `json:"version"`
    GeneratedAt  string       `json:"generated_at"`
    Capabilities []Capability `json:"capabilities"`
}

func main() {
    client := &http.Client{Timeout: 15 * time.Second}
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if baseURL == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_BASE_URL is required")
        os.Exit(2)
    }
    req, err := http.NewRequest(http.MethodGet, baseURL+"/discovery", nil)
    if err != nil {
        panic(err)
    }

    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        fmt.Fprintf(os.Stderr, "discovery failed: %s\n", resp.Status)
        os.Exit(1)
    }

    var catalog Discovery
    if err := json.NewDecoder(resp.Body).Decode(&catalog); err != nil {
        panic(err)
    }

    wanted := map[string]bool{
        "/v1/storage/bucket/create": false,
        "/v1/image/metadata":        false,
    }
    for _, capability := range catalog.Capabilities {
        if _, ok := wanted[capability.Path]; ok && capability.Available {
            wanted[capability.Path] = true
            fmt.Printf("%s %s (%s)\n", capability.Method, capability.Path, capability.ID)
        }
    }
    for path, ready := range wanted {
        if !ready {
            fmt.Fprintf(os.Stderr, "required handoff capability unavailable: %s\n", path)
            os.Exit(1)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In production, pin the generated request types or validate them during CI, then use the discovered paths rather than reconstructing paths from prose. The upload result becomes the input reference for metadata processing under the same key and base URL; the image itself remains private, while the workflow retains only the metadata required for the check. Deletion is then scheduled from the verification state transition, with the object identifier carried through as the correlation key.

This combined approach has a blunt downside: one vendor becomes the trust boundary, the bill, and the outage surface for both storage and processing. Consolidation removes credential and signing glue; it also removes some failure isolation. Treat that as an architecture decision, not a convenience feature.

Where do the real alternatives differ?

There is no universally best provider here. The choice depends on existing controls, transformation needs, and how much integration ownership the team accepts.

Option Useful fit Boundary to account for
AWS S3 plus an image worker Teams already operating IAM, bucket policies, lifecycle rules, and audit tooling You own processing code, deployment, retries, and the security review of every handoff
Cloudinary Product teams needing a mature managed image pipeline and delivery transformations It introduces another asset model and credential boundary beside primary storage
imgix Teams centered on URL-driven image delivery from an existing source Signed URL policy and source access must be reconciled with the storage provider
ImageKit Teams wanting managed optimization and delivery with upload controls Its media library and signing model remain a separate control plane from primary storage
Infrai Teams that value one self-describing REST surface for private storage and metadata processing Storage and processing share one vendor trust and availability boundary

S3 is attractive when the security team already reviews IAM changes and lifecycle policies as routine work. Cloudinary offers broader managed asset workflows, imgix is often a natural match for delivery-time transformations from an existing source, and ImageKit combines optimization and delivery with its own media library and upload controls. Those products may be better choices than a consolidated API when their control planes are already institutional knowledge, when separate failure domains are a deliberate requirement, or when an existing signing system has already passed a difficult security review that nobody wants to repeat. Familiar incident response is a real feature, and replacing known controls merely to reduce the credential count can create more review work than it removes.

No vendor erases the policy decision.

Infrai's 295 capabilities across 20 modules can reduce integration spread, and every documented capability has runnable examples in Go and nine other languages. Breadth is not proof that it matches a specific compliance regime, however. Review the discovered schemas, vendor readiness, region information, and the organization's own contractual requirements before placing identity images there.

The postmortem test

Imagine the deletion deadline is missed. The postmortem should be able to answer four questions without reconstructing events from a dashboard screenshot: when verification completed, when deletion became due, which stable object identifier was targeted, and when absence was confirmed. If the system cannot answer those, add state and audit metadata before adding alerts.

Then test access after deletion. A successful worker response is not the same thing as proving the original is no longer retrievable, and a queue acknowledgement is not proof of policy completion. Keep the confirmation result, not the photo.

The final guardrail is restraint. Metadata checks should produce metadata. Temporary originals should have explicit deadlines. Presigned access should expire. The best alert is the one that names the violated promise and the affected object, because the responder can act on it without first deciding whether the graph means anything.

False positives still matter. If confirmation is eventually consistent, set the warning window from observed platform behavior and reserve paging for the actual retention boundary; otherwise the team will learn to silence the very signal designed to protect the most sensitive asset in the system.

Further reading

Top comments (0)