Skip to main content
Encrypted Upload v2 uploads the device’s existing bota_enc_v2 recording ciphertext unchanged. Apps relay the authorization, ciphertext, manifest, and completion receipt as opaque bytes. The backend verifies and decrypts the ciphertext in an isolated worker before publishing the final OGG recording.
This API is batch-only. It does not define encrypted streaming behavior, and calling it does not enable v2 on any SDK or device. Use it only after explicit capability and backend-policy negotiation.

Authentication

React Native applications can use the SDK’s managed native backend adapter in App SDK 2.0.0-beta.11 through createEncryptedUploadV2Backend. It owns session persistence, manifest replay, verification polling and receipt delivery. Configure your authenticated backend URL, fresh app-token callback, exact identity and cancellation scope; invoke sync again after reopening/reconnecting. See Client SDKs. The existing beta.10 package does not contain this helper. A backend proxy exposing only legacy upload-complete must expose the v2 routes; upgrading the package alone does not migrate a legacy integration. All operations accept a secret API key, a restricted key with recordings:write, or the device token bound to the exact device and end user. Every recording, device, and session lookup is scoped to the authenticated project. The first-party Cognito application has an equivalent dashboard bridge under /dashboard/projects/{projectId}/recordings/{recordingId}/encrypted-upload-v2/sessions. It checks organization ownership of the project and uses the same scoped session services. Customer mobile apps must not embed secret API keys; keep their session provider on their own authenticated backend.

Lifecycle

  1. Create a session and receive a signed 408-byte authorization plus a private, checksum-bound staging target.
  2. Before delivering that authorization to the device, establish a fresh upload-only credential context using the device’s independent context nonce and opaque proof. This does not change pairing.
  3. PUT the device ciphertext directly to the returned URL with every returned header exactly as supplied.
  4. Submit the exact authenticated 580-byte manifest.
  5. Poll session status while the worker verifies, decrypts, validates OGG, and publishes the final recording.
  6. Reacquire fresh context and relay the completion receipt. Delete the device recording only after status is published and the device has verified the signed 336-byte completion receipt for the exact recording and generation.
An S3 PUT response, 202 manifest response, or processing state is never device-deletion authorization.

Establish upload context

Upload context is an additive source preview. It requires matching firmware, standalone React Native SDK, backend migration, and dedicated environment signing-key configuration. It does not enable v2 or alter legacy provisioning.
POST /v1/devices/{id}/encrypted-upload-v2/contexts
The 201 response contains context_id (UUID), state: "challenge", challenge_base64 (196 opaque bytes), result_base64: null, and expires_at. Relay the challenge unchanged to the device. The device verifies it and returns an encrypted proof of its installed token; the app never supplies or opens the token. This nonce is separate from auth_nonce_base64 and Grant nonces. POST /v1/devices/{id}/encrypted-upload-v2/contexts/{context_id}/proof
The response has the same shape: 202 with state: "pending", or 200 with state: "complete". Poll GET /v1/devices/{id}/encrypted-upload-v2/contexts/{context_id} until complete, then relay the 264-byte result_base64 unchanged. The backend worker checks the actual credential against the current confirmed binding and rejects revoked, provisional, cross-device or stale credentials and active factory resets. All three operations have a first-party dashboard equivalent under /dashboard/projects/{projectId}/devices/{deviceId}/encrypted-upload-v2/contexts. A device token may use me in the API path, only for that exact device. The challenge expires after 60 seconds; the device permits at most 30 seconds for the whole exchange. Polling and identical retries never extend either deadline. Retrying create with the same nonce returns the exact challenge; retrying the same proof repairs a lost enqueue. A different proof conflicts. Expired/failed contexts require a new device-generated attempt. Unknown fields and noncanonical base64 are rejected. Never substitute app time or trust keys. At most eight unexpired contexts may coexist for a device. Capacity exhaustion returns 429 (rate_limit_exceeded); retry with bounded backoff, respecting the original attempt deadline. If that deadline expires, start a fresh device attempt. Keep the recording and its session identity while waiting, including when publication succeeded but fresh context for receipt validation is pending.

Create a session

POST /v1/recordings/{id}/encrypted-upload-v2/sessions The recording must be a pending device recording whose immutable device, recording UUID, generation, and binding generation match the request.
Unknown fields are rejected.
The authorization is always canonical base64 for exactly 408 bytes. The session and presigned target expire after 15 minutes. Every returned staging header is authenticated by the presigned URL and must be sent exactly as returned; omitting or changing one makes the PUT fail. Treat the URL as opaque: use its exact host, path and query, including after renewal. Upload the ciphertext without decoding or transforming it:

Submit the manifest

Recover the staging target before submission

POST /v1/recordings/{id}/encrypted-upload-v2/sessions/{session_id}/staging-url If an app retained session identifiers but no presigned URL, request the same target with this body:
The 200 response contains url, method: "PUT", and required headers, in the same shape as the session’s staging object. It re-signs the original checksum-bound ciphertext object only. It does not create a new session or recipient, change owner, or extend the original 15-minute expiry. Renewal requires staging state and unchanged live binding generation, pending recording identity, policy and configuration. A stale context returns 409. After manifest acceptance, reconcile session status instead of re-uploading.

Replace a pre-manifest session (capability gated)

Recovery requires a matching backend, SDK/provider and firmware with capability bit 0x200. It is not enabled by a backend key or app setting alone. Target validation is pending; older firmware must not receive a replacement authorization.
POST /v1/recordings/{id}/encrypted-upload-v2/sessions/{session_id}/recover
The optional reason is "expired" (default when omitted), "nonce_changed", or "channel_changed". Keep the default for expiry recovery. On reconnect or device reboot, nonce_changed permits recovery before expiry only when the supplied device nonce differs from the immutable signed authorization and no manifest was ever accepted. The required capability mask is 0x37f, including upload context and recovery. This is not an app assertion that device admission never happened: the device must independently validate the signed replacement, current nonce, fresh authenticated context, increasing owner revision and absence of receipts. The response identifies the durable successor:
Save that identity under the same recording, then retrieve its status and staging target. The new signed authorization remains 408 bytes and contains the replacement flag 0x0008 and a higher owner revision. The recording and ciphertext remain unchanged; the recipient key, session and staging object are new. Do not erase device or app evidence to force a retry. Do not confuse the binary document version (2) with its transport profile: BOTAAUT2 byte 13 is profile 3, and byte 14 is storage format 3 for both initial and replacement authorizations. Only a pre-manifest session satisfying the selected reason is eligible. Current project/device/binding/recording/policy checks still apply. Failed, cancelled, processing and published sessions require reconciliation, not replacement. An identical parent retry returns its existing successor; it never creates a second child or extends the old authorization. If that child also expired or has a stale nonce, follow that exact child and use the applicable reason within a bounded retry limit. Never refresh its nonce in place. Accepted-manifest/processing/published sessions must resume status reconciliation rather than replacement or restaging. Direct WiFi/cellular handoff uses reason: "channel_changed" together with channel: "wifi" or "cellular". Both parent and target must be different direct channels; this does not take over a BLE owner. Other reasons must omit channel. Close the previous transport before handoff. Same-channel retries retain the session and renew staging credentials; handoff creates the signed successor described above and restarts ciphertext from byte zero. Provisioned devices can reconcile lost create/recovery responses with POST /v1/devices/me/encrypted-upload-v2/reconcile, authenticated by their device token. The request uses the identity fields below. It returns null or the recording/session/revision lookup, plus device_id and the actual direct channel. A WiFi caller can discover its cellular owner and vice versa, but this lookup does not authorize upload, handoff or deletion. Firmware direct-v2 remains disabled by default pending transport and hardware acceptance. The direct firmware coordinator distinguishes transient network/pending-worker results from terminal authorization or integrity failures. Context retries reuse the same nonce and original deadline; successful HTTP transfer still does not permit deleting the local recording without its signed receipt. The October 2 recovery source update schedules transient results after 30, 60, 120, then 300 seconds, with further attempts capped at 300 seconds. A 30-second timer checks eligibility, so actual starts can be later. Bluetooth preference, another upload or worker allocation failure defers the attempt without losing it; reset cancels it. Each attempt releases its radio ownership after a bounded publication check. Restart restores the durable recording/session identity; the in-memory backoff restarts. This update still requires firmware rollout and physical interruption qualification; default direct-v2 gates remain unchanged. The dashboard bridge exposes the same /recover action. Recovery does not change pairing, permit plaintext fallback or authorize deletion. Deletion still requires a published, exact-session signed receipt verified by the device.

Reconcile a lost creation response (first-party dashboard)

The organization-authorized dashboard provides POST /dashboard/projects/{projectId}/devices/{deviceId}/encrypted-upload-v2/reconcile. This is not an additional public API-key endpoint. Send the create-session identity fields except device_id (owned by the path) and auth_nonce_base64: binding_generation, recording_uuid, recording_generation, storage_format, channel, capabilities_base64, ciphertext_length and ciphertext_sha256. After current tenant/binding and exact physical recording checks, 200 returns null when no matching recording exists, or an identity-only result:
session_id and owner_revision are both null when the recording committed but no session did. When a session exists, exact ciphertext/channel/profile checks must also pass before returning the latest retained owner. Deleted or conflicting identities fail closed. The result contains no signed document, receipt, key or staging URL and is not upload/deletion authority. Reconcile an uncertain create before retrying, including after a concurrent-create conflict; do not invent a new recording UUID or reset terminal history to revision 1.

Submit the authenticated bytes

POST /v1/recordings/{id}/encrypted-upload-v2/sessions/{session_id}/manifest
A newly accepted manifest returns 202; an exact replay after publication returns 200.
state is ready, processing, or published. Conflicting bytes or a stale owner revision fail closed. For an uncertain PUT or manifest response, reconcile the saved session first. If it is ready or processing, poll that session; if published, obtain its signed receipt. If it remains before manifest acceptance and the saved owner is still valid, replay the exact saved manifest before repeating the PUT. An accepted replay proceeds to status polling without uploading bytes again. The recovery source update adds 409 encrypted_upload_v2_staging_missing when S3 affirmatively reports that the session’s object is absent. Only this result permits a fresh staging URL and whole-object PUT for that same owner. Timeouts, 403, 503, or an unrelated 409 do not prove absence. An expired/replaced owner still requires the existing signed recovery procedure. Deploy the backend update before firmware that relies on the new error; older backends may return 503 for an absent object and the new firmware retains the file while retrying.

Get session status

GET /v1/recordings/{id}/encrypted-upload-v2/sessions/{session_id}
Possible states are created, staging, staged, ready, processing, published, failed, cancelled, and expired. Plaintext fields appear only after the manifest establishes them. Receipt and publication fields appear only for published sessions. The receipt is canonical base64 for exactly 336 bytes and remains valid for 30 days from issuance.

Cancel a session

DELETE /v1/recordings/{id}/encrypted-upload-v2/sessions/{session_id}
Cancellation returns 204 and is allowed only before worker processing owns the session. It schedules idempotent cleanup of staged ciphertext and live one-time recipient material; it never deletes the device recording.

Errors

Cryptographic failures use stable public errors and do not expose private verification details.