# Totalum App Builder API — Mobile Apps

This file is the complete reference for this area. The short guide for agents is https://www.totalum.app/totalum-api.md.

- Base URL: `https://builder-api.totalum.app/api/v1/vcaas`
- Authentication: header `api-key: tlm_sk_…` on every request, from a server only.
- Response envelope: `{ "errors": null | { "errorCode", "errorMessage", "errorDetails?" }, "data": … }`.


---

# 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](#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`, `apk` with a key, `ipa`) need the signing key ([Set Signing Key](#set-signing-key)); unsigned debug builds do not.
- With "send to stores on publish" ([Set Mobile Settings](#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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/mobile/builds
cost: Metered (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**

```bash
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/builds
```

:::success Success · 202 Accepted
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/builds
cost: Free
```

The 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](#get-mobile-build) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/builds/:buildId
cost: Free
```

One 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/builds/:buildId/download
cost: Free
```

A 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/download
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/builds/:buildId/log
cost: Free
```

A 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/builds/mb_01k6y4/log
```

:::success Success · 200 OK
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/mobile/builds/:buildId/cancel
cost: Free
```

Cancel 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](#get-mobile-build) |

**Example request**

```bash
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/cancel
```

:::success Success · 200 OK
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/mobile/builds/:buildId/store-upload
cost: Free (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**

```bash
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-upload
```

:::success Success · 202 Accepted
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/store-uploads
cost: Free
```

The 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/store-uploads
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/store-uploads/:uploadId
cost: Free
```

One store upload, with the fields of [List Store Uploads](#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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/store-uploads/su_01k6y5abcdefghjkmnpqrstvwx
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/settings
cost: Free
```

The 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/settings
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "sendToStoresOnPublish": false, "appName": "Barber Book" } }
```
:::

## Set Mobile Settings

```endpoint
method: PUT
path: /api/v1/vcaas/projects/:projectId/mobile/settings
cost: Free
```

Change 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**

```bash
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/settings
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/signing/:slot
cost: Free
```

The 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/android
```

:::success Success · 200 OK
```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

```endpoint
method: PUT
path: /api/v1/vcaas/projects/:projectId/mobile/signing/:slot
cost: Free
```

Set 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](#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](#get-signing-key) |

**Example request**

```bash
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/android
```

:::success Success · 200 OK
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/mobile/signing/android/:action
cost: Free
```

`action = 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**

```bash
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/generate
```

:::success Success · 200 OK
```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

```endpoint
method: DELETE
path: /api/v1/vcaas/projects/:projectId/mobile/signing/:slot
cost: Free
```

Remove 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**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/signing/android
```

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MOBILE_BUILD_RUNNING` | 409 | A signed build is using the key |

## List Store Connections

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/stores
cost: Free
```

The 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/mobile/stores/:store
cost: Free
```

One 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores/app-store
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "appStore": null } }
```
:::

## Connect Store

```endpoint
method: PUT
path: /api/v1/vcaas/projects/:projectId/mobile/stores/:store
cost: Free
```

Connect 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](#list-store-connections) |
| `data.appStore` | object | `app-store` only: the connection, as in List Store Connections |

**Example request**

```bash
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-play
```

:::success Success · 200 OK
```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

```endpoint
method: DELETE
path: /api/v1/vcaas/projects/:projectId/mobile/stores/:store
cost: Free
```

Remove 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**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/mobile/stores/google-play
```
