---
title: "Create a test plan"
method: POST
path: "/api/plans"
tags: ["Test Plans"]
---

# Create a test plan

`POST /api/plans`

## Request

- Base URL: `https://aximo.autify.com`
- URL: `https://aximo.autify.com/api/plans`
- Auth: API key in header `x-api-key`
- TypeScript: <https://skmtc.dev/autify/apis/aximo-api/docs/api/plans/post.ts> (a function with typed inputs; its response is checked against the schema)

Creates an empty test plan in a workspace. Add cases with
`POST /api/plans/{planId}/cases`, then trigger it with
`POST /api/triggers/plans/{planId}/run`. Plans created via the API have
**API access enabled by default**.

## Request body

- object
  - `projectId` string, uuid, required: ID of the workspace to create the plan in.
  - `title` string, required
  - `description` string, nullable
  - `executionMode` 'parallel' | 'sequential'
  - `stopOnFailure` boolean: Stop remaining cases after a failure. Sequential runs only.
  - `deviceMode` 'pool' | 'matrix': pool: each case runs once on whichever configured device is free. matrix: each case runs on every configured device.
  - `mobileConfig` PlanMobileConfig: Per-platform target devices and app for a plan's mobile cases, as saved on the plan settings page. Omit a platform (or set it to `null`) to run that platform's cases with no plan-level device; an empty object clears both. A single app given through the flat fields is stored as `apps[0]` too, so `GET` returns both forms. Unknown keys are rejected: this is not the single-device `mobileConfig` of the trigger endpoint.
    - `ios` PlanMobilePlatformConfig: Devices and app for one platform. The app fields are the same as `CaseMobileConfig`: a single app via `uploadArn`/`uploadType`/`appName`/`bundleId`, or up to three via `apps`. When `apps` is given, `apps[0]` is stored as the primary app and takes precedence over the single-app fields; a `bundleId` that contradicts `apps[0].bundleId` is rejected. The app is installed and launched on every device listed here, Device Farm or local; a local phone whose platform has no plan app installs and launches the case's own app; its run case fails at dispatch when neither names an app for it.
      - `devices` PlanMobileDevice[], required
        - `deviceArn` string: Encrypted device identifier token from `GET /api/devices`. Not set for a local phone.
        - `deviceName` string
        - `os` string, nullable
        - `formFactor` string
        - `userDeviceId` string, uuid: Registered local phone to run on instead of Device Farm.
      - `uploadArn` string: Encrypted upload identifier token from `GET /api/uploads`. The upload must have status `succeeded` and match the platform.
      - `uploadType` 'own_app' | 'existing' | 'sample'
      - `appName` string
      - `bundleId` string
      - `apps` CaseAppInfo[]
        - `uploadArn` string, required: Encrypted upload identifier token.
        - `uploadType` 'own_app' | 'existing' | 'sample', required
        - `appName` string, nullable, required
        - `bundleId` string
    - `android` PlanMobilePlatformConfig: Devices and app for one platform. The app fields are the same as `CaseMobileConfig`: a single app via `uploadArn`/`uploadType`/`appName`/`bundleId`, or up to three via `apps`. When `apps` is given, `apps[0]` is stored as the primary app and takes precedence over the single-app fields; a `bundleId` that contradicts `apps[0].bundleId` is rejected. The app is installed and launched on every device listed here, Device Farm or local; a local phone whose platform has no plan app installs and launches the case's own app; its run case fails at dispatch when neither names an app for it.
      - `devices` PlanMobileDevice[], required
        - `deviceArn` string: Encrypted device identifier token from `GET /api/devices`. Not set for a local phone.
        - `deviceName` string
        - `os` string, nullable
        - `formFactor` string
        - `userDeviceId` string, uuid: Registered local phone to run on instead of Device Farm.
      - `uploadArn` string: Encrypted upload identifier token from `GET /api/uploads`. The upload must have status `succeeded` and match the platform.
      - `uploadType` 'own_app' | 'existing' | 'sample'
      - `appName` string
      - `bundleId` string
      - `apps` CaseAppInfo[]
        - `uploadArn` string, required: Encrypted upload identifier token.
        - `uploadType` 'own_app' | 'existing' | 'sample', required
        - `appName` string, nullable, required
        - `bundleId` string
  - `desktopConfig` PlanDesktopConfigInput: Registered BYOD desktop devices by `userDeviceId` (`GET /api/devices/user`). An empty list clears them.
    - `devices` object[], required
      - `userDeviceId` string, uuid, required

## Response `201`

Plan created.

- object
  - `success` boolean, required
  - `planId` string, uuid, required

## Other responses

- `400`: Invalid request body. Schema failures carry the zod error tree in `details`; an undecryptable token carries `details.path`; misplaced top-level fields carry `details.unknownFields` and `details.hint`; an upload that is not `succeeded` or built for the other platform, and a phone or desktop under the wrong platform, carry `details.path`.
- `401`: Missing or invalid API key.
- `403`: The API key's user is not a member of the organization, or a local phone or desktop device was added without the organization's BYOD feature. Clearing a config is never gated.
- `404`: Project not found, or a referenced upload or registered device does not belong to the organization (`details.path` names the slot).
- `429`: Rate limit exceeded — e.g. the daily session limit was reached for the organization, or too many API requests were sent in a short window (retry after the number of seconds in the `Retry-After` header).
- `500`: Unexpected server error.

---

[API](https://skmtc.dev/autify/apis/aximo-api.md) - [All operations](https://skmtc.dev/autify/apis/aximo-api/llms.txt) - [OpenAPI document](https://skmtc.dev/autify/apis/aximo-api/revisions/590722011284?raw)
