Submit a capture against the verification
Stage 3 of the upload flow (or stage 1 for inline-base64 payloads ≤ 6 MB). Two body shapes, discriminated by which field is present:
- inline:
{image_base64, surface?, client_captured_at?}— the bytes ride in the request, capped at Lambda’s 6 MB sync-invoke limit. - key_ref:
{image_key, image_sha256, surface?, client_captured_at?}—image_keyis the URL returned by/captures/upload-url; the handler reads bytes from S3 and verifies the heximage_sha256matches before persisting.
Both shapes also accept an optional step_key naming which step of the verification’s steps this capture fills. It may reference an image step (any state, capacity permitting) or an upload step with remaining capacity — upload steps hold pre-existing content and are reachable by explicit key only, there is no keyless fallback into them. Omitting step_key lands the capture in the first image step with remaining capacity (legacy keyless clients: the pre-steps hosted SPA, the SDK worker). A key that doesn’t resolve to an evidence step with capacity — unknown, or naming a text step — is 400 unknown_step; a step already at its max is 409 step_full.
Side effects:
- Persists a
Capturerow atomically with capture-count + status transition guards. - Emits
verification.capture.persistedon the EventBridge bus. The per-capture analysis worker consumes it (today:ANALYSIS=stub; future:crosscheck). - If this is the capture that fills the verification’s
max_capturesquota, the analysis worker may also write a final verdict and transition status tocompleted.
Attestation: if cf_turnstile_token is present and the verifier is configured, the token is validated server-side and persisted as an attestation field on the capture row. A failed verification is recorded as verdict: 'error' but the capture is still persisted (attestation is signal, not gate).
Returns: the updated verification snapshot (post-write state).
Authorizations
Possession of the verification id in the URL path (/v1/verifications/:token/...) authenticates the SPA capture flow. No header — the id is the credential. ULIDs are large enough that existence-by-id is not a meaningful leak. Used for POST /captures/upload-url, POST /captures, GET /v1/verify/:token.
Path Parameters
^vfy_[0-9A-HJKMNP-TV-Z]{26}$"vfy_01HXYZABCDEFGHJKMNPQRSTVWX"
Body
Response
Capture persisted. Returns the updated verification — status will be partially_captured until the worker writes the verdict.
Canonical wire shape for a verification. narrative is present only after POST /v1/verifications/:id/finalize ran. verdict is present only after analysis completed. case_analysis is the structured Gemini multi-image rubric, embedded inline by the sync finalize path when analysis succeeded.
^vfy_[0-9A-HJKMNP-TV-Z]{26}$Lifecycle state. pending → first capture flips to partially_captured → analysis sets the verdict and transitions to completed. Terminal: completed, expired, failed. abandoned is a side-branch off pending/partially_captured, reported by the capture screen via POST /v1/verifications/:token/progress when the end user leaves before finishing — NOT terminal: a later capture resurrects the row to partially_captured like any other.
pending, partially_captured, completed, expired, failed, abandoned x >= 0Tenant-supplied configuration baked into the token at issue time. Immutable after creation — changing config means issuing a new verification. steps is the normalized capture spec: requests that sent max_captures: N read back as a single anonymous step { key: "capture_1", min: N, max: N }.
Integrity seal over the verification record including answers. Present only when answers exist and sealing is on (seal !== false). The hash proves the stored answers have not changed — it does NOT claim they are true or sensor-attested; answers_provenance carries that distinction explicitly.
Aggregate verdict produced by the per-capture analysis pipeline. label is the human-readable bucket; score is the confidence in the verdict.
2000Structured output from the multi-image case-analysis worker (Gemini batch reasoning). Per-dimension scores 0..1 where 0 = nothing suspicious on that dimension and 1 = that dimension alone is grounds to reject. overall_risk_score aggregates with cross-dimensional reasoning, not a simple average.
Object-check result. Present only when the verification was created with expected_object. Independent of the fraud verdict.
Condition assessment. Present only when the verification was created with condition_aspects — the free-form tenant-defined aspect names (any language, any domain). score is the one-decimal average of aspect scores; label buckets it (>=7.5 good, >=5 fair, else poor). Independent of the fraud verdict.
Latest funnel-progress snapshot reported via POST /v1/verifications/:token/progress. A snapshot, not an event log — each call overwrites the previous one. Operator-only; never present on the public projection.