Mobile Apps
Turn your web app into store-ready Android and iOS apps.
A project's mobile app is a native shell that opens the published site, so publish first. Ask the agent for a mobile app ("make it work as a mobile app") and it prepares the project; then build here. Mobile apps need a paid plan.
- Builds run on Totalum's build servers one at a time for everyone: a build may wait in a queue (
queuePosition) and can be cancelled while it waits. Each project builds at most once per hour. - A build is metered by its build time, in credits; a reservation of 13.3 credits is held when it starts (
creditsCharged) and settled on the real build time when it ends (given back when it fails). Queue time is never charged. - Faster builds on your own Codemagic account: set your Codemagic API token (Set Signing Key with
slot = codemagic; in Codemagic: Account settings → API token). Builds then run on your Codemagic account: they start at once (no queue, no hourly limit), Totalum charges no build credits (creditsCharged: 0) and Codemagic bills its build minutes to you. Store uploads to TestFlight still run on Totalum's build servers. - Signed builds (
aab,apkwith a key,ipa) need the signing key (Set Signing Key); unsigned debug builds do not. - With "send to stores on publish" (Set Mobile Settings) every successful publish builds and uploads the signed apps to the Google Play internal track and TestFlight.
Signing, stores, settings and store uploads are the account owner's.
#Create Mobile Build
/api/v1/vcaas/projects/:projectId/mobile/buildsapi-keyMetered (build time)Start a build. Answers 202 with the build queued.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | "android" or "ios" |
artifact | string | No | Android: "debug-apk", "apk" (default) or "aab" (for Google Play). iOS: "simulator-app" (default, no Apple account needed) or "ipa" (signed, for TestFlight) |
mode | string | No | "remote_shell" (Android default: the app loads the site) or "hybrid" (iOS default: a bundled shell that loads the site) |
signed | boolean | No | Android: sign the apk (default: signed when a key is set) |
Response fields
| Field | Type | Description |
|---|---|---|
data.buildId | string | The build |
data.status | string | "queued" |
data.creditsReserved | number | Credits held for the build |
data.creditsCharged | number | Credits taken now (the reservation) |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"platform":"android","artifact":"aab"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/buildsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"platform": "android",
"artifact": "aab"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "buildId": "mb_01k6y4", "status": "queued", "creditsReserved": 13.3, "creditsCharged": 13.3 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | The artifact does not match the platform |
PLAN_REQUIRED | 403 | Mobile builds need a paid plan |
OWNER_ONLY | 403 | A signed build is the owner's |
FEATURE_DISABLED | 403 | That build type is not available yet |
MOBILE_REQUIRES_PUBLISHED_PROJECT | 409 | Publish first: the app opens the published site |
SIGNING_NOT_CONFIGURED | 409 | A signed build without a signing key |
MOBILE_BUILD_RUNNING | 409 | A build is already running |
PROCESS_IN_PROGRESS | 409 | A signed Android build while a prompt runs |
INSUFFICIENT_CREDITS | 402 | Not enough credits |
MOBILE_BUILD_RATE_LIMITED | 429 | One build per hour; errorDetails.retryAt says when |
#List Mobile Builds
/api/v1/vcaas/projects/:projectId/mobile/buildsapi-keyFreeThe project's builds, newest first.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.builds | object[] | Builds, with the fields of Get Mobile Build |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/buildsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "builds": [{ "id": "mb_01k6y4", "platform": "android", "mode": "remote_shell", "status": "success", "startedAt": "2026-10-08T13:00:10.000Z", "finishedAt": "2026-10-08T13:06:40.000Z", "artifact": { "type": "aab", "sizeBytes": 4194304, "expiresAt": "2026-11-07T13:06:40.000Z" }, "signed": true, "versionName": "1.0.3", "versionCode": 3, "trigger": "user", "hasLog": true }] } }#Get Mobile Build
/api/v1/vcaas/projects/:projectId/mobile/builds/:buildIdapi-keyFreeOne build. Poll it until status is "success", "failed" or "cancelled".
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
buildId | string | The build id |
Response fields
| Field | Type | Description |
|---|---|---|
data.id | string | Build id |
data.platform | string | "android" or "ios" |
data.mode | string | "remote_shell" or "hybrid" |
data.status | string | "queued", "running", "success", "failed" or "cancelled" |
data.queuePosition | number | undefined | Place in the queue while queued |
data.startedAt | string | null | Start |
data.finishedAt | string | null | End |
data.artifact | object | null | { type, sizeBytes, expiresAt } once built |
data.signed | boolean | Signed build |
data.signingFingerprintSha256 | string | undefined | Certificate fingerprint |
data.versionName | string | undefined | Version name |
data.versionCode | number | undefined | Version code |
data.trigger | string | undefined | "user" or "publish" |
data.error | object | undefined | { code, message, logExcerpt? } of a failed build |
data.hasLog | boolean | undefined | A full log is available |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "id": "mb_01k6y4", "platform": "android", "mode": "remote_shell", "status": "queued", "queuePosition": 2, "startedAt": null, "finishedAt": null, "artifact": null, "signed": true } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
MOBILE_BUILD_NOT_FOUND | 404 | No such build |
#Get Mobile Build Download
/api/v1/vcaas/projects/:projectId/mobile/builds/:buildId/downloadapi-keyFreeA 15-minute link to the built file.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
buildId | string | The build id |
Response fields
| Field | Type | Description |
|---|---|---|
data.url | string | Download URL |
data.fileName | string | File name |
data.type | string | The artifact type |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/downloadconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/download", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "url": "https://files.totalum.app/mobile/mb_01k6y4/app-release.aab?sig=…", "fileName": "barber-bookings-1.0.3.aab", "type": "aab" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
MOBILE_BUILD_NOT_READY | 409 | The build has no file (yet) |
DOWNLOAD_EXPIRED | 410 | The file has expired |
#Get Mobile Build Log
/api/v1/vcaas/projects/:projectId/mobile/builds/:buildId/logapi-keyFreeA 15-minute link to the build's full log. Ask the agent to fix a failed build ("the Android build fails"): it reads the log itself.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
buildId | string | The build id |
Response fields
| Field | Type | Description |
|---|---|---|
data.url | string | Log URL |
data.fileName | string | "build.log" |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/logconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/log", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "url": "https://files.totalum.app/mobile/mb_01k6y4/build.log?sig=…", "fileName": "build.log" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
MOBILE_BUILD_NOT_READY | 409 | The build has no log |
#Cancel Mobile Build
/api/v1/vcaas/projects/:projectId/mobile/builds/:buildId/cancelapi-keyFreeCancel a build that is still waiting in the queue (its reservation is given back). A build that has started runs to its end.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
buildId | string | The build id |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The build after cancelling, as in Get Mobile Build |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/cancelconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/cancel", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "id": "mb_01k6y4", "platform": "android", "mode": "remote_shell", "status": "cancelled", "startedAt": null, "finishedAt": "2026-10-08T13:01:00.000Z", "artifact": null, "signed": true } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
MOBILE_BUILD_NOT_CANCELLABLE | 409 | The build is no longer waiting in the queue |
#Upload Build To Store
/api/v1/vcaas/projects/:projectId/mobile/builds/:buildId/store-uploadapi-keyFree (Android) · metered (iOS)Send a signed build to its store: an aab to the Google Play internal track, an ipa to TestFlight. Promotion to production stays your step in each store's console. At most 10 store uploads a day.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
buildId | string | A signed aab or ipa build |
Response fields
| Field | Type | Description |
|---|---|---|
data.uploadId | string | The upload |
data.store | string | "google-play" or "app-store" |
data.track | string | "internal" or "testflight" |
data.status | string | "queued" |
data.creditsReserved | number | Credits held |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/store-uploadconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/store-upload", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "uploadId": "su_01k6y5abcdefghjkmnpqrstvwx", "store": "google-play", "track": "internal", "status": "queued", "creditsReserved": 0 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
MOBILE_BUILD_NOT_SIGNED | 409 | Only a signed aab or ipa |
STORE_NOT_CONFIGURED | 409 | Connect the store first |
STORE_UPLOAD_RUNNING | 409 | An upload is already running |
STORE_UPLOAD_LIMIT_REACHED | 429 | 10 uploads today |
#List Store Uploads
/api/v1/vcaas/projects/:projectId/mobile/store-uploadsapi-keyFreeThe project's store uploads.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.uploads[].id | string | Upload id |
data.uploads[].buildId | string | The build |
data.uploads[].store | string | "google-play" or "app-store" |
data.uploads[].track | string | "internal" or "testflight" |
data.uploads[].status | string | "queued", "uploading", "processing", "success", "failed" or "manual_required" (Google Play's first upload of an app must be made by hand) |
data.uploads[].versionName | string | Version name |
data.uploads[].versionCode | number | Version code |
data.uploads[].consoleUrl | string | null | Link to the store console |
data.uploads[].error | object | undefined | { code, message } |
data.uploads[].createdAt | string | ISO 8601 |
data.uploads[].finishedAt | string | null | ISO 8601 |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/store-uploadsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/store-uploads", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "uploads": [{ "id": "su_01k6y5abcdefghjkmnpqrstvwx", "buildId": "mb_01k6y4", "store": "google-play", "track": "internal", "status": "success", "versionName": "1.0.3", "versionCode": 3, "consoleUrl": "https://play.google.com/console/…", "createdAt": "2026-10-08T13:07:00.000Z", "finishedAt": "2026-10-08T13:09:00.000Z" }] } }#Get Store Upload
/api/v1/vcaas/projects/:projectId/mobile/store-uploads/:uploadIdapi-keyFreeOne store upload, with the fields of List Store Uploads.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
uploadId | string | The upload id |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The upload |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/store-uploads/su_01k6y5abcdefghjkmnpqrstvwxconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/store-uploads/su_01k6y5abcdefghjkmnpqrstvwx", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "id": "su_01k6y5abcdefghjkmnpqrstvwx", "buildId": "mb_01k6y4", "store": "google-play", "track": "internal", "status": "processing", "versionName": "1.0.3", "versionCode": 3, "consoleUrl": null, "createdAt": "2026-10-08T13:07:00.000Z", "finishedAt": null } }#Get Mobile Settings
/api/v1/vcaas/projects/:projectId/mobile/settingsapi-keyFreeThe app name and whether publishing also sends the apps to the stores.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.sendToStoresOnPublish | boolean | Every successful publish builds and uploads the signed apps |
data.appName | string | null | The name under the icon (≤ 30 characters); null = the store listing title, else the site's name |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/settingsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/settings", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "sendToStoresOnPublish": false, "appName": "Barber Book" } }#Set Mobile Settings
/api/v1/vcaas/projects/:projectId/mobile/settingsapi-keyFreeChange either setting. Turning on sendToStoresOnPublish needs a connected store.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
sendToStoresOnPublish | boolean | No¹ | Build and upload on every publish |
appName | string | null | No¹ | 1–30 characters; null = default |
¹ At least one.
Response fields
| Field | Type | Description |
|---|---|---|
data.sendToStoresOnPublish | boolean | After the change |
data.appName | string | null | After the change |
Example request
curl -X PUT \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"appName":"Barber Book"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/settingsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/settings", {
method: "PUT",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"appName": "Barber Book"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "sendToStoresOnPublish": false, "appName": "Barber Book" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
STORE_NOT_CONFIGURED | 409 | Connect Google Play or the App Store first |
#Get Signing Key
/api/v1/vcaas/projects/:projectId/mobile/signing/:slotapi-keyFreeThe signing key of one platform (metadata only, never a secret). slot is android, ios, or codemagic (your own Codemagic API token: configured, tokenHint = its last 4 characters, addedAt).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
slot | string | "android", "ios" or "codemagic" |
Response fields
| Field | Type | Description |
|---|---|---|
data.configured | boolean | A key is set |
data.source | string | null | Android: "uploaded" or "generated" |
data.keyAlias | string | null | Android key alias |
data.fingerprintSha256 | string | null | Android certificate fingerprint |
data.validUntil | string | null | Android certificate expiry |
data.keystoreDownloadAvailable | boolean | Android: a generated keystore can still be downloaded |
data.keyId, data.issuerId, data.teamId, data.bundleId, data.validatedAt | string | null | iOS: the App Store Connect key |
data.createdAt, data.lastUsedAt | string | null | Dates |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/androidconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/android", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "configured": true, "source": "generated", "keyAlias": "upload", "fingerprintSha256": "AB:CD:…", "validUntil": "2051-10-08T00:00:00.000Z", "keystoreDownloadAvailable": false, "createdAt": "2026-10-08T12:50:00.000Z", "lastUsedAt": "2026-10-08T13:00:10.000Z" } }#Set Signing Key
/api/v1/vcaas/projects/:projectId/mobile/signing/:slotapi-keyFreeSet the signing key. Android (slot = android): multipart/form-data with the keystore file (keystore, a .jks/.keystore up to 64 KB) and its passwords — or generate one with Android Signing Action. iOS (slot = ios): JSON with the App Store Connect API key, checked against Apple. Codemagic (slot = codemagic): JSON { "apiToken": "…" }, checked against Codemagic; Totalum adds a totalum-mobile-builds app to that Codemagic account and runs the project's next builds there (refused 409 MOBILE_BUILD_RUNNING while a build runs).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
slot | string | "android", "ios" or "codemagic" |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
keystore | file | Android | The keystore file |
keyAlias | string | Android | Key alias |
storePassword | string | Android | 6–200 characters |
keyPassword | string | Android | 6–200 characters |
apiKeyP8 | string | iOS | The .p8 key, PEM text |
keyId | string | iOS | 10-character key id |
issuerId | string | iOS | Issuer UUID |
bundleId | string | iOS | The app's bundle id (com.example.app) |
apiToken | string | Codemagic | Your Codemagic API token (Account settings → API token) |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The key, as in Get Signing Key |
Example request
curl -X PUT \
-H "api-key: tlm_sk_your_key" \
-F "keystore=@./upload-keystore.jks" -F "keyAlias=upload" \
-F "storePassword=your-store-password" -F "keyPassword=your-key-password" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/androidimport { readFile } from "node:fs/promises";
const form = new FormData();
form.append("keystore", new Blob([await readFile("./upload-keystore.jks")]), "upload-keystore.jks");
form.append("keyAlias", "upload");
form.append("storePassword", "your-store-password");
form.append("keyPassword", "your-key-password");
const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/android", {
method: "PUT",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
body: form,
});
const { errors, data } = await res.json();{ "errors": null, "data": { "configured": true, "source": "uploaded", "keyAlias": "upload", "fingerprintSha256": "AB:CD:…", "validUntil": "2051-10-08T00:00:00.000Z", "keystoreDownloadAvailable": false, "createdAt": "2026-10-08T12:50:00.000Z", "lastUsedAt": null } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
SIGNING_INVALID_KEYSTORE | 400 | The keystore was refused (errorDetails.reason: parse, password, alias, expired, weak_key) |
SIGNING_ALREADY_CONFIGURED | 409 | Delete the current keystore first |
VALIDATION_ERROR | 400 | Codemagic refused the API token |
PLAN_REQUIRED | 403 | Needs a paid plan |
OWNER_ONLY | 403 | Owner only |
#Android Signing Action
/api/v1/vcaas/projects/:projectId/mobile/signing/android/:actionapi-keyFreeaction = generate: create an Android upload key (RSA 4096, 25 years, alias upload) and get a one-time, 15-minute token to download it. action = keystore-download: exchange that token for the keystore file and its passwords — keep them safe; Google Play needs the same key for every update.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
action | string | "generate" or "keystore-download" |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
commonName | string | No | generate: certificate name |
organization | string | No | generate: organisation |
countryCode | string | No | generate: two-letter country |
token | string | No | keystore-download: the token from generate |
Response fields
| Field | Type | Description |
|---|---|---|
data.keystoreDownload | object | generate: { token, expiresAt } plus the key's fields |
data.url | string | keystore-download: a single-use URL (5 minutes) |
data.fileName | string | "upload-keystore.jks" |
data.keyAlias, data.storePassword, data.keyPassword | string | keystore-download: shown once |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"organization":"Barber Shop SL","countryCode":"ES"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/android/generateconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/android/generate", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"organization": "Barber Shop SL",
"countryCode": "ES"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "configured": true, "source": "generated", "keyAlias": "upload", "fingerprintSha256": "AB:CD:…", "validUntil": "2051-10-08T00:00:00.000Z", "keystoreDownloadAvailable": true, "createdAt": "2026-10-08T12:50:00.000Z", "lastUsedAt": null, "keystoreDownload": { "token": "kd_9f2c…", "expiresAt": "2026-10-08T13:05:00.000Z" } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
SIGNING_ALREADY_CONFIGURED | 409 | A key exists; delete it first |
KEYSTORE_NOT_DOWNLOADABLE | 409 | An uploaded keystore is never downloadable |
KEYSTORE_DOWNLOAD_EXPIRED | 410 | The token was used or expired |
#Delete Signing Key
/api/v1/vcaas/projects/:projectId/mobile/signing/:slotapi-keyFreeRemove a platform's signing key (or, with slot = codemagic, your Codemagic API token: builds go back to Totalum's build servers). Answers 204 with no body.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
slot | string | "android", "ios" or "codemagic" |
Response fields
| Field | Type | Description |
|---|---|---|
| (none) | 204 No Content |
Example request
curl -X DELETE -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/androidconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/android", {
method: "DELETE",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();Error codes
| Code | HTTP | Meaning |
|---|---|---|
MOBILE_BUILD_RUNNING | 409 | A signed build is using the key |
#List Store Connections
/api/v1/vcaas/projects/:projectId/mobile/storesapi-keyFreeThe connected store accounts (metadata only).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.googlePlay | object | null | { clientEmail, packageName, packageFound, validatedAt } |
data.appStore | object | null | { appAppleId, bundleId, validatedAt } |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/storesconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "googlePlay": { "clientEmail": "uploader@my-play.iam.gserviceaccount.com", "packageName": "com.barbershop.book", "packageFound": true, "validatedAt": "2026-10-08T12:55:00.000Z" }, "appStore": null } }#Get Store Connection
/api/v1/vcaas/projects/:projectId/mobile/stores/:storeapi-keyFreeOne store's connection (metadata only). The answer has only that store's field: googlePlay for google-play, appStore for app-store; null when it is not connected.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
store | string | "google-play" or "app-store" |
Response fields
| Field | Type | Description |
|---|---|---|
data.googlePlay | object | null | google-play only: { clientEmail, packageName, packageFound, validatedAt } |
data.appStore | object | null | app-store only: { appAppleId, bundleId, validatedAt } |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores/app-storeconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores/app-store", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "appStore": null } }#Connect Store
/api/v1/vcaas/projects/:projectId/mobile/stores/:storeapi-keyFreeConnect a store account. Google Play: a Play Console service-account key (the JSON Google downloads) with release rights on the app, and the package name; a package Google does not know yet is stored with packageFound: false and its first bundle must be uploaded by hand. App Store: uses the iOS signing key; the app is found by its bundle id (or give appAppleId). The answer has only the store you connected.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
store | string | "google-play" or "app-store" |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
serviceAccountJson | string | Google Play | The service-account JSON, as text |
packageName | string | Google Play | com.example.app |
appAppleId | string | No | App Store: the app's Apple id |
Response fields
| Field | Type | Description |
|---|---|---|
data.googlePlay | object | google-play only: the connection, as in List Store Connections |
data.appStore | object | app-store only: the connection, as in List Store Connections |
Example request
curl -X PUT \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"packageName":"com.barbershop.book","serviceAccountJson":"{\"type\":\"service_account\",…}"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores/google-playconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores/google-play", {
method: "PUT",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"packageName": "com.barbershop.book",
"serviceAccountJson": "{\"type\":\"service_account\",…}"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "googlePlay": { "clientEmail": "uploader@my-play.iam.gserviceaccount.com", "packageName": "com.barbershop.book", "packageFound": true, "validatedAt": "2026-10-08T12:55:00.000Z" } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | Invalid key or package name |
SIGNING_NOT_CONFIGURED | 409 | App Store: add the iOS signing key first |
#Disconnect Store
/api/v1/vcaas/projects/:projectId/mobile/stores/:storeapi-keyFreeRemove a store connection. Answers 204 with no body.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
store | string | "google-play" or "app-store" |
Response fields
| Field | Type | Description |
|---|---|---|
| (none) | 204 No Content |
Example request
curl -X DELETE -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores/google-playconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores/google-play", {
method: "DELETE",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();