Join the V-Modal beta by filling out the contact form.
Search video with plain text from your Kotlin Android app. No video-processing stack, no vector database — just a client, an API key, and coroutines.
Quick start • Runtime API keys • Search • Upload • Examples • Search app • API reference • Troubleshooting
val result = sdk.searches.searchVideo(
queryText = "red car at night", // describe the moment in plain words
groupName = "traffic-cameras", // your collection
streamName = "astream",
limit = 20,
)That is the whole idea: upload videos into collections, then find moments in
them with natural language. The SDK also manages collections, uploads large
files with resumable multipart streaming, and plays nicely with
Dispatchers.IO, lifecycleScope, and WorkManager.
Release artifacts are configured to use the Maven Central coordinates
com.vmodal:vmodal-sdk-android:<version>. Source-project inclusion remains
available for contributors.
⚠️ Never bundle a real API key in source,BuildConfig, resources, orAndroidManifest.xml. The parent application must inject it at runtime.
Three steps from zero to your first API response.
Confirm that the requested SDK version is published in Maven Central. Until the first registry release completes, contributors must use the source project.
Make sure your Android project's settings.gradle.kts contains Maven Central:
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}In the app module's build.gradle.kts, use Java 17 and add the SDK dependency:
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("com.vmodal:vmodal-sdk-android:1.0.0")
}Sync the Gradle project, then allow network access in
app/src/main/AndroidManifest.xml (directly inside <manifest>):
<uses-permission android:name="android.permission.INTERNET" />V-Modal calls perform network I/O. Run them from Dispatchers.IO, WorkManager,
or another worker thread — never the Android main thread.
The following function authenticates the API key, creates the ready-to-use client, and returns the first visible result:
import com.vmodal.sdk.Client
import com.vmodal.sdk.MutableApiKeyProvider
import com.vmodal.sdk.PUBLIC_GATEWAY_URL
import com.vmodal.sdk.SdkConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun checkVmodal(apiKeys: MutableApiKeyProvider): Client = withContext(Dispatchers.IO) {
val firstClient = Client(
SdkConfig(
baseUrl = PUBLIC_GATEWAY_URL,
userId = "",
mode = "gateway",
apiKeyProvider = apiKeys,
)
)
val me = firstClient.auth.me()
val sdk = Client(
firstClient.cfg.copy(
userId = requireNotNull(me.userId),
tenantId = me.tenantId.orEmpty(),
email = me.email.orEmpty(),
)
)
val health = sdk.health()
println("VModal connected: ${health.status}")
sdk
}Call it from an Activity or Fragment lifecycle scope:
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
lifecycleScope.launch {
val apiKeys = MutableApiKeyProvider(apiKeyLoadedByYourApp)
val sdk = checkVmodal(apiKeys)
// Keep or pass sdk to the code that needs V-Modal.
// Retain apiKeys at application scope, then call apiKeys.rotate(freshKey).
// On logout/account switch: clear persisted state, then apiKeys.clear().
}Use viewModelScope.launch { ... } instead when the client belongs to a
ViewModel. A printed VModal connected: ... message means installation,
authentication, and network access are all working. 🎉
flowchart LR
A["🔑 Runtime API key"] --> B["MutableApiKeyProvider"]
B --> C["Client(mode = gateway)"]
C --> D["auth.me()"]
D --> E["Client(cfg + userId)"]
E --> F["health() / search / upload"]
The parent Android application and this SDK have separate responsibilities. The app owns the credential lifecycle; the SDK only reads the injected current value while building an authenticated request.
| Parent application owns | SDK owns |
|---|---|
| Authenticate the signed-in app user | ApiKeyProvider request-time contract |
| Fetch the API key from the app backend | Atomic swaps and fail-closed clearing in MutableApiKeyProvider |
| Choose secure, app-owned persistence | One key snapshot per authenticated request |
| Refresh, version, and serialize rotations | Authorization: Bearer <key> on existing authenticated routes |
| Decide whether a failed operation is safe to retry | Filtering auth headers from presigned R2 upload requests |
ApiKeyProvider.current() is synchronous. It must return an already-loaded
value and must not perform storage or network I/O. Keep the provider and
Client at application scope so Activities, coroutines, and WorkManager jobs
all observe the same rotations.
flowchart LR
subgraph App["Parent Android application"]
A["App-owned secure storage"] --> B["Load cached key on worker thread"]
C["Authenticated app backend"] --> D["Fetch latest key"]
B --> E["MutableApiKeyProvider"]
D -->|"persist, then rotate(newKey)"| E
end
subgraph SDK["uinterface/sdk_android"]
F["SdkConfig(apiKeyProvider)"] --> G["Client"]
G --> H["Build authenticated request"]
H --> I["Authorization: Bearer key snapshot"]
end
E --> F
At application startup:
- On
Dispatchers.IO, load the signed-in session and cached API key from app-owned storage. If no key exists, complete sign-in and fetch the first key before creating an authenticated client. - Create one
MutableApiKeyProvider, inject it throughSdkConfig, callauth.me(), and install the resolvedClientin the app dependency graph. - Fetch a newer key in the background. Validate and persist it using the
app's policy, then call
rotate(newKey).
Rotation changes the next request; a request already created keeps its original
header. A blank initial or rotated key raises ValidationFailed, and a failed
rotation leaves the last working key active. Rotation is only valid for another
key belonging to the same V-Modal identity. For a different user or tenant,
create a new Client and resolve auth.me() again.
On logout or account switch, first stop/cancel work that uses the client, clear
the app's persisted credential, and call apiKeys.clear() (or close()). The
operation is idempotent. Later authenticated requests fail closed with
AuthError; a configured provider never falls back to the legacy static token.
Clearing removes the SDK's live reference, but immutable JVM strings cannot be
guaranteed to be zeroized from every old heap copy.
Authenticated API calls require HTTPS except for literal loopback development hosts, reject cross-origin absolute URLs, and do not follow redirects. Signed uploads apply the same HTTPS policy, strip API identity headers, and do not follow redirects.
On 401, the app may refresh the key and retry one safe, idempotent operation
once. Do not treat 403 as proof of expiration, and do not automatically replay
uploads or mutating POST requests. See the framework-free
rotation example.
The legacy token = "..." constructors and SdkConfig.fromEnv() remain
supported for JVM tools, CI, and existing integrations. apiKeyProvider takes
precedence when both are supplied.
Once the quick start works, use the returned sdk client on the same worker
context:
val groups = sdk.collections.listGroups(mode = "vid_file")
println("Collections: ${groups.total}")
groups.data.forEach(::println)This is a useful second check because it confirms that the authenticated user can reach their V-Modal data.
Replace traffic-cameras with a collection returned by listGroups():
val result = sdk.searches.searchVideo(
queryText = "red car at night",
groupName = "traffic-cameras",
streamName = "astream",
limit = 20,
)
println("Matches returned: ${result.cntActual}")
result.data.forEach(::println)💡 If the call succeeds but returns no matches, first confirm the collection name, stream name, and query text. An empty result is different from an API error.
After authentication and search work, continue with the upload examples. The Android-safe path is:
flowchart LR
A["🎬 User picks video<br/>(content:// URI)"] --> B["UploadSource<br/>example 08"]
B --> C["videoUploadAsync()<br/>example 09"]
C --> D["UploadHandle<br/>(progress / cancel)"]
- Let the user select a video and obtain a
content://URI. - Convert the URI to an
UploadSourcewith example 08. - Start the upload with example 09.
- Keep the returned
UploadHandleif the UI needs a Cancel action.
The SDK streams the video instead of loading the whole file into memory. Files of at least 100 MiB select multipart upload by default.
TODO: production does not currently expose the multipart route family. Pass
VideoUploadOptions(multipart = false) for production uploads until those
routes are available. The multipart implementation is retained for Python SDK
parity and is covered by offline regression tests.
| Symptom | Fix |
|---|---|
VMODAL_API_KEY is required |
Client.fromEnv() is intended for JVM tools and CI, where environment variables exist. In an Android app, inject a runtime API-key provider as shown in the quick start. |
auth/me returned no user_id or auth error |
Confirm the API key is current and belongs to the environment identified by PUBLIC_GATEWAY_URL. Do not invent or hard-code a user ID; auth.me() resolves the key owner. |
NetworkOnMainThreadException or frozen UI |
Move blocking calls (auth.me(), health(), listGroups(), searchVideo()) to Dispatchers.IO or WorkManager. videoUploadAsync() already runs off the main thread, but its callbacks do too — switch to Dispatchers.Main before updating views. |
| Gradle cannot resolve the SDK | Confirm mavenCentral() is configured and use a released version from the public repository. |
These commands test the SDK itself; they are not required each time the Android app runs:
gradle --no-daemon clean build publishToMavenLocal
cd examples/02_search
./gradlew --no-daemon :app:assembleDebug \
-PvmodalUseMavenLocal=true -PvmodalSdkVersion=1.0.0This verifies both the Maven publication and Android consumption. No emulator or API token is required. Maintainers can follow the Maven Central release guide.
| Step | Where | What you get |
|---|---|---|
| 1 | This page | Working client, first API response |
| 2 | Examples | Copy-paste building blocks, grouped by task |
| 3 | Upload guide | Android URI uploads, cancellation, WorkManager, process-death resume |
| 4 | API quick reference | Every method and response type |
All typed response objects expose raw: Map<String, Any?> for server fields
that do not yet have a typed property. All SDK failures derive from SdkError;
applications can handle AuthError, ValidationFailed, ApiError, and
FeatureDisabled separately when needed.