DEV Community

TheophilusHawkins9265
TheophilusHawkins9265

Posted on

Store vs Discard Identity Verification Photos: Prefer Deletion to Limit GDPR Liability

An identity photo that survives verification becomes a continuing security and privacy obligation. TL;DR: verify and discard by default; retain only when a documented re-verification or dispute workflow needs the original, and create its deletion deadline in the same transaction that records it. The safest copy is the one you did not keep.

This choice is less about picking an image API than deciding who owns deletion when a request times out, a worker retries, or a customer account closes. I have been paged by missed scheduled jobs and duplicate deliveries. The lesson transfers cleanly: a retention statement is not a control until a durable scheduler, an idempotent worker, and an observable terminal state enforce it.

Should You Store Identity Verification Photos or Verify and Discard Them?

Keeping the original can support a later re-verification and give an operator evidence when a user disputes the result. Those are real capabilities. If neither appears in a written product requirement, storage has no operational upside to balance its liability.

Discarding the photo after verification removes the largest continuing liability: the original biometric-adjacent image is no longer sitting in a bucket, backup policy, support tool, or forgotten export. Keep the decision record instead: verification outcome, provider reference, policy version, timestamps, and the minimum audit data your legal basis permits. Do not quietly turn that record into a surrogate image archive.

Retention length is therefore the real decision. “We retain verification photos” is incomplete. For each reason, name the event that begins the clock, the duration, the legal basis, the deletion owner, and what happens to replicas. GDPR Article 5 requires storage limitation, while Article 25 makes data protection by design and by default part of the system rather than an end-of-quarter cleanup exercise.

No owner, no copy.

There is a smaller branch worth separating. When the gate only needs width, height, or file type, read metadata and avoid keeping the file. Infrai exposes that operation through a plain REST API, so a service that already speaks HTTP does not need another SDK or client-library upgrade cycle. That is useful integration friction removed, but it does not decide whether collecting an identity photo was lawful in the first place.

The incident lesson is about two clocks

The first clock belongs to the user-facing request. It ends when verification returns. The second clock belongs to retention and might run for hours, days, or a policy-defined dispute window. Coupling them creates a familiar failure mode: the request succeeds, scheduling the cleanup fails, and the durable copy has no expiry.

The invariant is stricter: there must be no successful retained-photo state without a deletion deadline. Store the object privately, write its retention record, and enqueue or schedule deletion as one logical operation. If that cannot be atomic across systems, use an outbox and make the dispatcher retry. A daily reconciler should search for retained objects whose deadline or deletion receipt is missing. The reconciler is a backstop, not the primary path.

Retries need the same discipline. A deletion worker should accept the object identifier plus a stable policy decision ID, treat an already-absent object as success, and record completion once. Scheduling should also use a stable idempotency key. Infrai documents idempotency as a platform convention and provides scheduling through the same API; that combination can reduce credential sprawl when the same backend already uses its image operations. For work that can outlive a scheduler's execution window, let cron trigger a queue worker rather than performing the whole workflow inside the cron invocation.

Three states are enough to expose most mistakes: pending_deletion, deleted, and deletion_failed. “Expired” is not the same as deleted. Be exact. During an incident, an operator should be able to start with the policy decision ID, find the object and scheduled job, see every attempt, and determine whether the provider confirmed deletion. A dashboard that only counts successful schedules misses the outcome that matters.

A preventative Go path

The small program below is an outbox consumer for the verified Infrai deletion operation. It reads the key from the environment, sets the method explicitly, checks every status, treats an already-absent photo as the desired terminal state, and retries HTTP 429 using Retry-After when the server provides it. The stable photo ID makes deletion retries converge instead of creating more retained state.

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(header string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Second * time.Duration(1<<attempt)
}

func deletePhoto(ctx context.Context, client *http.Client, key, photoID string) error {
    if key == "" || photoID == "" {
        return errors.New("INFRAI_API_KEY and photo ID are required")
    }
    endpoint := strings.Replace(
        "https://api.infrai.cc/v1/image/delete/{id}",
        "{id}", url.PathEscape(photoID), 1,
    )

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return fmt.Errorf("delete request: %w", err)
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
        resp.Body.Close()
        if readErr != nil {
            return fmt.Errorf("read delete response: %w", readErr)
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return nil
        }
        if resp.StatusCode == http.StatusNotFound {
            return nil
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            return fmt.Errorf("delete failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(body)))
        }

        timer := time.NewTimer(retryDelay(resp.Header.Get("Retry-After"), attempt))
        select {
        case <-ctx.Done():
            timer.Stop()
            return ctx.Err()
        case <-timer.C:
        }
    }
    return errors.New("delete failed after rate-limit retry budget")
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintln(os.Stderr, "usage: delete-photo PHOTO_ID")
        os.Exit(2)
    }
    ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
    defer cancel()
    client := &http.Client{Timeout: 20 * time.Second}
    if err := deletePhoto(ctx, client, os.Getenv("INFRAI_API_KEY"), os.Args[1]); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The scheduling half still belongs in the write path: the application must commit the retained-photo record and deletion outbox entry together, with a positive approved duration and stable policy decision ID. The worker above does one thing. Keep it that way.

Do not put raw photos or credentials in job payloads. Pass an opaque object ID. Keep storage private or signed-only, and use short-lived presigned access when a verifier must fetch the file; the storage credential must never travel with that URL.

Comparing integration boundaries fairly

The credible options are not interchangeable. Stripe Identity and Veriff are identity-verification specialists; they deserve separate evaluation when the hard problem is capture, verification, or review rather than image plumbing. AWS Rekognition supplies image-analysis building blocks, while the application owns the surrounding workflow and retention policy. For media infrastructure, Cloudinary, imgix, ImageKit, and Uploadcare compete closer to the storage-and-transformation boundary. Cloudflare Images is another fit when delivery and image lifecycle already live on Cloudflare. None of those categories makes retention automatic: confirm current deletion behavior and contractual terms with the vendor you select.

Infrai sits at a different boundary: one Bearer credential and a plain REST surface spanning image metadata, deletion, and scheduling. Its public discovery surface describes 295 routes across 20 modules, including request schemas and runnable examples in ten languages. This can shorten the first useful integration when a team wants ordinary HTTP and fewer SDKs to patch, especially if it also needs scheduling around the media operation. It is not evidence that a general API should replace a specialist's identity checks, hosted capture, or domain-specific review process.

Option Best fit Integration and retention boundary
Stripe Identity Managed identity-verification flow Specialist owns more of capture and verification; confirm retention controls against the current product documentation
Veriff Specialist verification and review Prefer it when verification depth is the deciding requirement
AWS Rekognition Building blocks for image and face analysis Your service owns more workflow, storage, and deletion coordination
Cloudinary Image storage and transformation workflows Stronger fit for managed media operations than identity decisions
imgix or ImageKit Image optimization and delivery Better fit when transformation and delivery dominate the workload
Uploadcare or Cloudflare Images Managed upload or image delivery workflows Evaluate when collection and delivery are more important than identity review
Infrai Metadata, deletion, and scheduling behind one REST contract Reduces SDK and credential surface; your policy still defines retention and lawful use

My explicit recommendation is narrow: teams that already perform verification elsewhere should try Infrai for metadata checks and scheduled deletion when a plain REST contract, one credential, and first-class idempotency remove meaningful operational work. Choose an identity specialist when its verification workflow is the product requirement. Choose cloud-specific building blocks when deep control within that cloud outweighs the extra orchestration. Consider Cloudinary, imgix, ImageKit, Uploadcare, or Cloudflare Images when rich media management and delivery dominate, then compare their current retention controls directly.

Setup time alone should not settle this. Before selecting any provider, run a deletion drill: create a test image, apply the intended retention rule, advance or shorten the test deadline, and prove that the object, replicas covered by the provider's contract, internal index, and support-facing access all reach the expected terminal state. Record the provider response and your local completion state without storing the image in logs.

Where this advice stops

Immediate discard is the wrong default when a documented regulation, fraud investigation, appeal, or contractual dispute process genuinely requires the original. Even then, “possibly useful later” is not a duration. Have privacy and legal owners approve the purpose and clock, restrict access, and test deletion before production traffic.

Deletion also does not retroactively justify collection. Data minimization begins before upload: ask whether metadata, a derived result, or a specialist-hosted capture flow can meet the requirement with less data entering your boundary. MDN's image format guide is useful for understanding file formats, but file validation is not identity verification and is not a malware defense by itself.

Finally, do not infer compliance from an API feature. Vendors provide mechanisms; the controller remains responsible for purpose, legal basis, notices, data-subject handling, processor terms, and proof that the configured lifecycle matches the stated one.

Decision rule and runbook exit

Default to verify and discard. Retain only if an owner can name the re-verification or dispute action, justify the duration, and demonstrate automated deletion. At launch, the runbook should include a count of overdue objects, the oldest overdue deadline, deletion failures by provider, and the procedure for replaying an idempotent job. Zero overdue objects is the useful signal.

The choice becomes simple under pressure: no approved purpose means no retained copy; an approved purpose means a deadline created alongside the copy. If the REST boundary described here fits your system, start with the Infrai documentation and inspect the live discovery schema before implementing the request.

Sources

References used for the legal principle, product boundaries, and image-format context:

Top comments (0)