Silent liveness
A single image or a recorded sequence. The cloud runs RAW RGB inference and, for sequences, the final temporal window.
rgb_livenessCapture on your device. Verify in the cloud. Connect silent RGB and color liveness through one compact REST interface, with module-scoped tokens and predictable usage.
https://api.inspirehub.ccUse your reachable server address or HTTPS domain. 0.0.0.0 is the server’s listening address, not a destination for remote clients.
Get a business token from the service operator, then check your permissions and available quota. One token can grant access to one or more algorithm modules.
InspireFace eye points + validated 320 × 320 crop.
JSON metadata + lossless binary image parts.
Check status, then alive. Recollect if retry.
export API_BASE='https://api.inspirehub.cc'
export API_TOKEN='YOUR_BUSINESS_TOKEN'
curl --fail-with-body "$API_BASE/v1/me" \
-H "Authorization: Bearer $API_TOKEN"
curl --fail-with-body "$API_BASE/v1/modules" \
-H "Authorization: Bearer $API_TOKEN"import os
import httpx
base = os.environ["API_BASE"].rstrip("/")
headers = {"Authorization": f"Bearer {os.environ['API_TOKEN']}"}
with httpx.Client(base_url=base, headers=headers, timeout=30) as client:
for path in ("/v1/me", "/v1/modules"):
response = client.get(base + path)
response.raise_for_status()
print(response.json())Python examples require Python 3.10+ and httpx (pip install httpx). Set API_BASE and API_TOKEN in your environment before running them. This documentation never asks for or stores your token.
A single image or a recorded sequence. The cloud runs RAW RGB inference and, for sequences, the final temporal window.
rgb_livenessOne capture with white, red, green and blue illumination. The cloud computes frame differences and fuses two RAW CNN outputs.
color_liveness/v1/modules/{module_id}/verify| Module | mode | capture_profile | Input |
|---|---|---|---|
rgb_liveness | rgb_single | rgb_single_v1 | One crop |
rgb_liveness | rgb_sequence | rgb_contiguous_v1 | Ordered frames + failure events |
color_liveness | color | color_wrgb_v1 | white → red → green → blue |
Availability has two controls: the module must be enabled on the server and granted to your token. GET /v1/modules returns your authorized modules and their deployment status.
Use multipart/form-data. The metadata part is a text field containing JSON; each image is a binary file part whose form field name exactly matches image.part. Let your HTTP library generate Content-Type and its boundary.
Before integration, implement and validate the agreed eye_crop_320_v1 preprocessing with your mobile SDK. Core accepts prepared crops; it does not detect, align or crop faces. A generic bounding-box resize is not equivalent. Keep rotation and mirroring consistent across the capture.
| Encoding | Requirements |
|---|---|
png | 320 × 320, 8-bit truecolor RGB (PNG color type 2), no alpha or palette. Lossless; recommended for smaller uploads. |
rgb8 | Exactly 307200 bytes: contiguous HWC RGB uint8 pixels. No header, channel swapping, normalization or float conversion. |
Do not send JPEG, video, Base64 or float arrays. For color liveness, upload the four original crops; the cloud computes differences. Hash the exact uploaded bytes with SHA-256 and use lowercase hexadecimal. The hash checks integrity, not authenticity.
| Field | Contract |
|---|---|
protocol_version | Integer 1. |
capture_id | String, 1–128 characters; unique within a batch. Use a new ID for each new capture. |
mode | rgb_single / rgb_sequence / color |
capture_profile | Exact profile matching the chosen mode; see the module table. |
preprocess_profile | eye_crop_320_v1 |
complete | Boolean. Set true only when the agreed capture conditions have finished. false produces a normal retry result. |
frames | Ordered event array. No unknown fields, labels, thresholds or model choices in the packet. |
| Field | When required | Contract |
|---|---|---|
source_index | Always | Nonnegative source frame integer, strictly increasing. Preserve gaps; do not renumber selected frames. |
timestamp_us | Always | Nonnegative integer exposure timestamp in microseconds, strictly increasing on one capture clock. Unix time is not required. |
status | Always | ok / face_unavailable / dropped |
source_size | status = ok | [width, height] of the upright source image, integer dimensions ≥ 2. The source coordinate space stays fixed throughout the capture. |
eyes_xy | status = ok | [[x0,y0],[x1,y1]] in the upright source image, not the 320 × 320 crop. Finite, in bounds, sorted by x; x1 − x0 ≥ 2 pixels. Required for COLOR too. |
image | status = ok | Exactly three keys: part, encoding, sha256. The referenced file part must exist and may be used only once. |
phase | COLOR only; required | white → red → green → blue |
display_time_us | COLOR only; optional | Nonnegative phase display timestamp, no later than current exposure and strictly after the preceding phase exposure. Use the same clock as timestamp_us. |
For rgb_sequence, upload ordered crops and preserve face_unavailable / dropped events without image data. A missing source index, unavailable face, geometry discontinuity or CNN score jump can reset the final window. Twenty frames is the window maximum, not a guarantee of a complete final window. A failed terminal frame returns retry.
{"source_index": 7, "timestamp_us": 233331, "status": "face_unavailable"}For color, send the white/red/green/blue original crops in that order. If interrupted, send the collected prefix with complete=false; do not relabel phases or fabricate missing images. Timestamp checks catch contradictions but do not prove display/exposure synchronization or challenge authenticity. Validate capture timing on the actual device.
The examples below assume you already have validated crop PNGs and matching acquisition metadata. Coordinates and times shown are illustrative: replace them with measurements from those exact source frames. The helper computes hashes from your files; it does not preprocess images.
Use trusted local input files. The helper reads image.part relative to the input JSON, fills sha256, writes metadata.json, and sends the same binary bytes. --prepare-only writes metadata.json without making any API call.
import argparse
import hashlib
import json
import os
from pathlib import Path
import httpx
parser = argparse.ArgumentParser()
parser.add_argument("input", type=Path)
parser.add_argument("--prepare-only", action="store_true")
args = parser.parse_args()
metadata = json.loads(args.input.read_text(encoding="utf-8"))
captures = metadata["captures"] if "captures" in metadata else [metadata]
module_for = {"rgb_single": "rgb_liveness",
"rgb_sequence": "rgb_liveness", "color": "color_liveness"}
modules = {module_for[capture["mode"]] for capture in captures}
if len(modules) != 1:
raise ValueError("Use exactly one module per HTTP request")
module = modules.pop()
parts = {}
for capture in captures:
for frame in capture["frames"]:
if "image" not in frame:
continue
descriptor = frame["image"]
name = descriptor["part"]
if name in parts:
raise ValueError("Each image.part must be unique")
data = (args.input.parent / name).read_bytes()
descriptor["sha256"] = hashlib.sha256(data).hexdigest()
parts[name] = data
text = json.dumps(metadata, ensure_ascii=False, allow_nan=False)
Path("metadata.json").write_text(text, encoding="utf-8")
if args.prepare_only:
raise SystemExit(0)
base = os.environ["API_BASE"].rstrip("/")
headers = {
"Authorization": f"Bearer {os.environ['API_TOKEN']}",
"Idempotency-Key": os.environ["IDEMPOTENCY_KEY"],
}
files = [("metadata", (None, text))]
files.extend((name, (name, data, "application/octet-stream"))
for name, data in parts.items())
response = httpx.post(
f"{base}/v1/modules/{module}/verify",
headers=headers, files=files, timeout=60,
)
print(response.status_code)
print(json.dumps(response.json(), indent=2, ensure_ascii=False))
response.raise_for_status()
for result in response.json()["results"]:
if result["status"] == "ok":
print(result["capture_id"], "alive:", result["alive"])
else:
print(result["capture_id"], "retry:", result["reason"])After preparing a cURL upload, do not change the PNG files: their bytes must still match metadata.json. Never set Content-Type manually. Even a capture containing only failure events still needs multipart, as generated by this helper.
Put rgb.png next to input-rgb.json. The sha256 placeholder is replaced by send_capture.py before upload.
{
"protocol_version": 1,
"capture_id": "rgb-capture-001",
"mode": "rgb_single",
"capture_profile": "rgb_single_v1",
"preprocess_profile": "eye_crop_320_v1",
"complete": true,
"frames": [{
"source_index": 0, "timestamp_us": 100000, "status": "ok",
"source_size": [640, 480],
"eyes_xy": [[220.0, 180.0], [340.0, 180.0]],
"image": {"part": "rgb.png", "encoding": "png",
"sha256": "REPLACED_BY_SEND_CAPTURE_PY"}
}]
}export IDEMPOTENCY_KEY="$(python -c 'import uuid; print(uuid.uuid4())')"
python send_capture.py input-rgb.json --prepare-only
curl --fail-with-body "$API_BASE/v1/modules/rgb_liveness/verify" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-F 'metadata=<metadata.json' \
-F 'rgb.png=@rgb.png;type=image/png'export IDEMPOTENCY_KEY="$(python -c 'import uuid; print(uuid.uuid4())')"
python send_capture.py input-rgb.jsonThese commands start a new verification. For a network retry, reuse the existing IDEMPOTENCY_KEY; do not rerun the export line. To use a sequence, set mode=rgb_sequence and capture_profile=rgb_contiguous_v1, then append ordered frame records and failure events.
Prepare white.png, red.png, green.png and blue.png from the corresponding illumination phases. Upload the four crops together; the whole capture costs one unit.
Illustrative source coordinates and timing only. Use actual per-frame measurements, including display_time_us if available.
{
"protocol_version": 1,
"capture_id": "color-capture-001",
"mode": "color",
"capture_profile": "color_wrgb_v1",
"preprocess_profile": "eye_crop_320_v1",
"complete": true,
"frames": [
{
"source_index": 0, "timestamp_us": 200000, "status": "ok",
"phase": "white", "display_time_us": 0,
"source_size": [640, 480],
"eyes_xy": [[220.0, 180.0], [340.0, 180.0]],
"image": {"part": "white.png", "encoding": "png",
"sha256": "REPLACED_BY_SEND_CAPTURE_PY"}
},
{
"source_index": 1, "timestamp_us": 450000, "status": "ok",
"phase": "red", "display_time_us": 250000,
"source_size": [640, 480],
"eyes_xy": [[221.0, 180.0], [341.0, 180.0]],
"image": {"part": "red.png", "encoding": "png",
"sha256": "REPLACED_BY_SEND_CAPTURE_PY"}
},
{
"source_index": 2, "timestamp_us": 700000, "status": "ok",
"phase": "green", "display_time_us": 500000,
"source_size": [640, 480],
"eyes_xy": [[220.0, 181.0], [340.0, 181.0]],
"image": {"part": "green.png", "encoding": "png",
"sha256": "REPLACED_BY_SEND_CAPTURE_PY"}
},
{
"source_index": 3, "timestamp_us": 950000, "status": "ok",
"phase": "blue", "display_time_us": 750000,
"source_size": [640, 480],
"eyes_xy": [[220.0, 180.0], [340.0, 180.0]],
"image": {"part": "blue.png", "encoding": "png",
"sha256": "REPLACED_BY_SEND_CAPTURE_PY"}
}
]
}export IDEMPOTENCY_KEY="$(python -c 'import uuid; print(uuid.uuid4())')"
python send_capture.py input-color.json --prepare-only
curl --fail-with-body "$API_BASE/v1/modules/color_liveness/verify" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-F 'metadata=<metadata.json' \
-F 'white.png=@white.png;type=image/png' \
-F 'red.png=@red.png;type=image/png' \
-F 'green.png=@green.png;type=image/png' \
-F 'blue.png=@blue.png;type=image/png'export IDEMPOTENCY_KEY="$(python -c 'import uuid; print(uuid.uuid4())')"
python send_capture.py input-color.jsonWrap complete CapturePacket objects in a captures array and send all referenced image parts together. The batch must target one module. RGB single frames and RGB sequences can share a request; RGB and COLOR cannot.
import json
from pathlib import Path
captures = []
for folder in ("capture-a", "capture-b"):
capture = json.loads(Path(folder, "input.json").read_text())
for frame in capture["frames"]:
if "image" in frame:
name = frame["image"]["part"]
frame["image"]["part"] = f"{folder}/{name}"
captures.append(capture)
Path("input-batch.json").write_text(json.dumps({"captures": captures}))Give the two input captures distinct capture_id values. Run the same send_capture.py helper on input-batch.json with a new IDEMPOTENCY_KEY. Each prefixed part name maps to its matching form field; filenames alone do not identify a part.
| Result | Meaning | Client action |
|---|---|---|
status: "ok"alive: true | Model score is strictly greater than threshold. | Liveness passed under the returned policy. |
status: "ok"alive: false | Model classified the capture as non-live. | Handle as a failed liveness check. |
status: "retry" | No decision: alive and score are null. The capture was incomplete or had insufficient usable terminal data. | Read reason and recollect. Use a new capture_id and idempotency key; this is a new charged verification. |
Scores and quota values below are examples, not a recorded model result. usage is an array of module quotas.
{
"request_id": "res_example",
"module_id": "rgb_liveness",
"charged_units": 1,
"results": [{
"capture_id": "rgb-capture-001", "mode": "rgb_single",
"status": "ok", "alive": true, "score": 0.91, "reason": null,
"final_window_frames": 0, "window_frame_indices": [],
"reset_events": [], "single_score": 0.91,
"temporal_logit": null, "color_logits": null,
"model_set": "raw_v1", "decision_policy": "single_frame_v1",
"threshold": 0.5, "min_final_frames": null
}],
"usage": [{
"module_id": "rgb_liveness", "limit": 1000, "period": "day",
"used": 1, "remaining": 999, "reset_at": "2027-01-02T00:00:00Z"
}]
}| Fields | How to use them |
|---|---|
request_id | Trace identifier, also returned as X-Request-ID. Keep it for support and troubleshooting. |
charged_units / usage | Units attributed to the operation and quota snapshot. An idempotent replay returns the original snapshot; query /v1/me for current usage. |
model_set / decision_policy | Model and decision policy identifiers for traceability. threshold is chosen by the service, not passed by the client. |
final_window_frameswindow_frame_indicesreset_events | RGB sequence diagnostics: final window length, included source indices and reset reasons. min_final_frames applies only to sequences; it is null for other modes. |
single_scoretemporal_logitcolor_logits | Mode-specific diagnostic scores. Use status + alive for the decision; do not replace them with a diagnostic branch output. |
Send Authorization: Bearer YOUR_BUSINESS_TOKEN on every /v1 request. The operator controls your token’s enabled state, expiry and per-module grants. Account login sessions cannot be used as business API tokens.
An RGB recording is one unit; four COLOR phases together are one unit; N captures cost N units. Live, non-live and normal retry results are all charged. Authentication, validation, capacity and quota rejections are not charged.
| Quota setting | Refresh / behavior |
|---|---|
period: "day" | Resets at 00:00 UTC each day, independent of the device’s timezone. |
period: "month" | Resets at 00:00 UTC on the first day of each calendar month. |
period: "never" | Cumulative quota; no automatic refresh. reset_at is null. |
limit: null | Unlimited calls, with usage still recorded. remaining is null. |
limit: 0 | Authorized module, but no available units. |
Use GET /v1/me to read current usage; these read-only calls do not consume algorithm quota. Changing a limit or disabling and re-enabling a grant does not reset usage within the same period. Send tokens over HTTPS in production; keep administrator credentials out of client apps.
Create one Idempotency-Key per verification and retain it with the unchanged package until the operation is resolved. The key is scoped to your token and accepts 1–200 printable ASCII characters without spaces. UUIDs are a convenient choice.
{
"error": {"code": "quota_exceeded", "message": "Quota exceeded for module: rgb_liveness"},
"request_id": "example-request-id"
}| HTTP / code | What to do |
|---|---|
400invalid_idempotency_keyinvalid_content_length | Fix the invalid header. Do not loop on the same malformed request. |
401 invalid_token | Check the Bearer token. It may be missing, invalid, expired, disabled, revoked or the wrong token type. Contact the operator. |
403 module_forbidden404 module_not_found | Request authorization for the module, or correct the module ID. |
409 request_in_progress | The same key is still pending. Wait for Retry-After and retry the unchanged request with that key. |
409 idempotency_conflict | The key was used with different input. Restore the original package, or use a new key only if this is a new verification. |
413 payload_too_large | Reduce request size or batch size. Respect both the service and reverse proxy limits. |
415 unsupported_media_type422 invalid_input | Use multipart, then fix metadata, frame order, encoding, dimensions or hashes as indicated. Invalid image encoding is a 422 input error. |
429 quota_exceeded | Wait until reset or ask the operator for more quota. Day/month quotas include Retry-After; never quotas do not auto-reset. |
503 server_busymodule_unavailable | Retry the same package and key after Retry-After, with bounded exponential backoff and jitter. module_disabled requires the operator to enable the module. |
500 inference_failed | Reserved quota was refunded. Retry with the same key after a delay; report repeated failures with request_id. |
500 settlement_failed | Settlement needs operator recovery; a reservation may remain pending. Keep the original key and contact support rather than starting duplicate operations. |
500 internal_error | Keep the package, key and request_id. A safe retry uses the same key; persistent failures need operator investigation. |
These values describe this deployment. The body limit includes multipart overhead; a reverse proxy may enforce a smaller limit. Split large batches before uploading.
| Deployment setting | Current value |
|---|---|
| Enabled modules | rgb_liveness, color_liveness |
| CNN execution / maximum internal batch | sequential / 4 |
| Minimum final RGB sequence window | 1 |
Single RGB accepts at most one event; COLOR at most four. The RGB window is at most 20 frames. A minimum window of 1 preserves the original numerical baseline; it is not a calibrated production capture policy. Coordinate stricter capture requirements with the operator. Single-frame mode is unaffected by the sequence minimum.
There is no unbounded inference queue. Capacity is reserved through upload, preparation and execution; excess requests return 503. The database stores token hashes, quota records, small result JSON and request audit metadata (including IP and approximate country). For designated demo tokens, complete uploaded images, metadata and verification results are additionally archived by UTC day. Other tokens do not archive images by default. Confirm the archive and retention configuration with your operator.
Operators sign in with an administrator username and password to manage users and their business tokens, module grants, expiry and quotas. User login is currently disabled. Administration requires a session cookie and CSRF protection; business tokens cannot access it. A new business token is shown in full only once.
| Endpoint | Purpose |
|---|---|
GET /health/liveGET /health/ready | Unauthenticated process and model-readiness checks. ready returns 503 when unavailable. |
/admin | Open the administration console ↗ |
/admin/api/users/admin/api/modules/admin/api/tokens | Administrator-only user, module and token management with a login session. Refer to the interactive reference for methods and schemas. |
Android runs local InspireFace and owns camera capture, face checks, crop generation, networking and UI state. Reuse the original demo recording interaction, prepare one complete capture, then send it once for cloud RAW liveness inference.
No camera callbacks or SDK face tokens cross the API boundary. Android submits original 320 × 320 crops and source-frame metadata; the server computes normalization, color differences and model scores.
| Item | Silent RGB | Color liveness |
|---|---|---|
| Module | rgb_liveness | color_liveness |
| mode | rgb_sequence | color |
| capture_profile | rgb_contiguous_v1 | color_wrgb_v1 |
| Ready to submit | 20 consecutive valid frames | 4 valid frames: white → red → green → blue |
| Cost of one recording | 1 unit for all 20 images | 1 unit for all 4 images |
Twenty frames is this Android integration’s submission target. The general core also supports variable-length input. CNN score changes can still shorten the final GRU window; read final_window_frames from the response. Android does not need a local copy of the cloud liveness model to predict this.
Match the current RGB geometry rule: let d_old and d_new be consecutive original-image eye distances and m_old / m_new their eye midpoints. Keep 0.65 <= d_new / d_old <= 1.5 and distance(m_new, m_old) / d_old <= 0.6. Keep source_size fixed for the recording; restart the segment when these conditions fail. Eye coordinates must be finite, inside the source image and ordered by x with at least two pixels of horizontal separation.
For eye_crop_320_v1, reuse ModelMath.crop() from the original arcface/android/flash_lab demo (ModelMath.java), with ModelMathTest.java as the frozen comparison starting point. Feed it the local InspireFace eye coordinates for that same upright frame and validate pixel equality. The wire example below begins after that exact crop is ready.
Use rgb8 for the first integration: 307200 bytes per crop, row-major RGB, three bytes per pixel. Extract channels explicitly from the agreed crop’s ARGB int array. This avoids PNG alpha and byte-order differences; do not convert via RGB565.
public final class AndroidRgb8 {
private AndroidRgb8() {}
// Input: the agreed 320 x 320 crop as ARGB int pixels.
public static byte[] fromArgb(int[] cropArgb) {
if (cropArgb.length != 320 * 320) {
throw new IllegalArgumentException("Expected a 320 x 320 crop");
}
byte[] rgb = new byte[307200];
int offset = 0;
for (int pixel : cropArgb) {
rgb[offset++] = (byte) (pixel >>> 16); // R
rgb[offset++] = (byte) (pixel >>> 8); // G
rgb[offset++] = (byte) pixel; // B
}
return rgb;
}
}Add one metadata record per frame. The example below is one RGB record, not a complete 20-frame request. Replace coordinates, dimensions, index and time with actual local observations. The Java request builder calculates image.sha256; the placeholder is not sent as a digest.
{
"source_index": 340,
"timestamp_us": 1200000,
"status": "ok",
"source_size": [
640,
480
],
"eyes_xy": [
[
180.5,
160.25
],
[
300.5,
161.0
]
],
"image": {
"part": "f0340.rgb",
"encoding": "rgb8",
"sha256": "CALCULATED_BY_THE_JAVA_BUILDER_FROM_UPLOADED_BYTES"
}
}For COLOR, add phase to each record, plus display_time_us when available. Keep source_size and eyes_xy for all four frames. Only protocol fields belong in metadata; save SDK objects and device diagnostics separately on Android.
The following Java example uses Android org.json and OkHttp. Reuse the HTTP client already selected by the Android project. preparedFrames is the ordered array of the records above; rgb8Parts maps each image.part to the exact RGB8 byte array. Perform crop conversion, hashing and request construction on a worker thread.
import java.security.MessageDigest;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.json.JSONArray;
import org.json.JSONObject;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Request;
import okhttp3.RequestBody;
public final class AndroidCaptureRequest {
private AndroidCaptureRequest() {}
// Build ONCE after a complete recording. Keep this Request for retries.
// preparedFrames contain the fields shown below; rgb8Parts is keyed by image.part.
public static Request buildRgb8(
String apiBase, String businessToken, String mode,
JSONArray preparedFrames, Map<String, byte[]> rgb8Parts) throws Exception {
boolean color = "color".equals(mode);
if (!color && !"rgb_sequence".equals(mode)) {
throw new IllegalArgumentException("Use rgb_sequence or color");
}
int count = color ? 4 : 20;
if (preparedFrames.length() != count) {
throw new IllegalArgumentException("Capture is not complete");
}
String module = color ? "color_liveness" : "rgb_liveness";
String profile = color ? "color_wrgb_v1" : "rgb_contiguous_v1";
String[] phases = {"white", "red", "green", "blue"};
String captureId = UUID.randomUUID().toString();
JSONArray frames = new JSONArray(preparedFrames.toString());
MultipartBody.Builder multipart = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
Set<String> used = new HashSet<>();
long lastIndex = -1, lastTimeUs = -1;
for (int i = 0; i < count; i++) {
JSONObject row = frames.getJSONObject(i);
long index = row.getLong("source_index");
long timeUs = row.getLong("timestamp_us");
if (!"ok".equals(row.getString("status")) || index <= lastIndex
|| timeUs <= lastTimeUs || (!color && i > 0 && index != lastIndex + 1)) {
throw new IllegalArgumentException("Invalid frame status, order or continuity");
}
if (color) {
if (!phases[i].equals(row.getString("phase"))) {
throw new IllegalArgumentException("Expected white/red/green/blue");
}
if (row.has("display_time_us")) {
long displayed = row.getLong("display_time_us");
if (displayed < 0 || displayed > timeUs || (i > 0 && displayed <= lastTimeUs)) {
throw new IllegalArgumentException("Invalid phase timestamps");
}
}
} else if (row.has("phase") || row.has("display_time_us")) {
throw new IllegalArgumentException("RGB must omit color-phase fields");
}
lastIndex = index;
lastTimeUs = timeUs;
JSONObject image = row.getJSONObject("image");
String part = image.getString("part");
byte[] source = rgb8Parts.get(part);
if (!used.add(part) || source == null || source.length != 307200) {
throw new IllegalArgumentException("Missing, duplicate or invalid RGB8 part");
}
byte[] snapshot = source.clone(); // Keep retries independent of capture buffers.
image.put("encoding", "rgb8");
image.put("sha256", sha256(snapshot));
multipart.addFormDataPart(part, part,
RequestBody.create(MediaType.parse("application/octet-stream"), snapshot));
}
if (used.size() != rgb8Parts.size()) {
throw new IllegalArgumentException("Unreferenced image parts");
}
JSONObject packet = new JSONObject()
.put("protocol_version", 1)
.put("capture_id", captureId)
.put("mode", mode)
.put("capture_profile", profile)
.put("preprocess_profile", "eye_crop_320_v1")
.put("complete", true)
.put("frames", frames);
multipart.addFormDataPart("metadata", packet.toString());
String base = apiBase.endsWith("/") ? apiBase.substring(0, apiBase.length() - 1) : apiBase;
return new Request.Builder()
.url(base + "/v1/modules/" + module + "/verify")
.header("Authorization", "Bearer " + businessToken)
.header("Idempotency-Key", captureId)
.post(multipart.build())
.build();
}
private static String sha256(byte[] data) throws Exception {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(data);
char[] hex = "0123456789abcdef".toCharArray();
StringBuilder out = new StringBuilder(64);
for (byte b : digest) {
out.append(hex[(b & 0xff) >>> 4]);
out.append(hex[b & 0x0f]);
}
return out.toString();
}
}
// Upload asynchronously with your existing OkHttpClient and Callback:
// Request frozen = AndroidCaptureRequest.buildRgb8(API_BASE, businessToken, mode, frames, parts);
// client.newCall(frozen).enqueue(callback);
// Retry: create a NEW Call using the SAME frozen Request.
// New recording: call buildRgb8 again to create a new capture_id / Idempotency-Key.Call the builder once per new recording. For a timeout or disconnect, create a new OkHttp Call from the same Request; retain the key, metadata and image bytes. Rebuilding the packet creates a new key and may charge again. Let MultipartBody generate Content-Type and its boundary; metadata uses the two-argument addFormDataPart overload and has no filename.
| Response | Android action |
|---|---|
HTTP 200 · status=ok | Match capture_id to the active operation, then use alive and score. HTTP 200 alone is not a live verdict. |
HTTP 200 · status=retry | Recollect with a new capture_id and key; score/alive are null. A valid retry result is still one charged verification. |
401 / 403 / 429 | Handle credentials, module permission or quota; do not report a spoof verdict. Quota reset is UTC midnight for day grants. |
409 / 503 / network timeout | Follow Retry-After and the errors section. Keep the original key and bytes for a retry of the same verification. |
Recommended local states: Ready → Preparing → Capturing → Encoding → Uploading → Result / Retry / Error. Keep late callbacks associated with their original capture. Check GET /v1/me and GET /v1/modules when configuring the business token; these queries do not consume verification quota.