---
title: Build on ZevCloud with connected apps
description: "How partner apps, such as AI builders and developer tools, create, deploy and watch services in a person's ZevCloud team with their consent."
---

A connected app is a partner app that acts in a person's ZevCloud account with their consent. An AI builder can create a service from the code it wrote, set its environment variables, deploy it and read its logs. The person stays in control. They pick which teams the app may touch, and they can disconnect it at any time.

This page covers using ZevCloud once your app is connected. Getting approved, connecting a person, PKCE, tokens, refresh, webhooks and testing are covered in ZevAuth's guide, [Delegated access to Zev products](https://docs.zevauth.com/delegated-access/overview/).

## How it works

1. Your app is approved for ZevCloud through ZevAuth. See [Getting approved](https://docs.zevauth.com/delegated-access/getting-approved/).
2. Your app sends the person to connect. ZevID signs them in, shows the permissions you ask for and lets them pick teams. See [The flow](https://docs.zevauth.com/delegated-access/flow/).
3. ZevAuth gives your app a 10 minute access token and a refresh token. See [Using the access token](https://docs.zevauth.com/delegated-access/access-tokens/) and [Refresh tokens](https://docs.zevauth.com/delegated-access/refresh-tokens/).
4. Your app calls the ZevCloud API with that token and a team id.

## Teams

ZevCloud's targets are teams.

- The person picks teams on the ZevID consent screen.
- Only teams where they are an owner or admin can be picked.
- If your app asks for `zevcloud.services.delete`, only teams the person owns can be picked. Teams where they are only an admin show but cannot be chosen.
- If they have no team yet, they can create one from the consent screen. It opens ZevCloud's create-team page and brings them back to finish connecting.
- On every call, the team must be one the person connected, and they must still be an owner or admin of it. If they lost that role, the call fails with `role_lost`.

## Scopes

Ask only for the scopes your app needs. The person sees each one on the consent screen. See also [Scopes by product](https://docs.zevauth.com/delegated-access/scopes/).

| Scope | Risk | What it allows |
|---|---|---|
| `zevcloud.services.read` | standard | Read services, deployments, build logs, runtime logs, metrics and environment variable names. Values are always masked. |
| `zevcloud.services.create` | standard | Create projects and services, from public URLs or uploads. Free plan and free database tiers only. |
| `zevcloud.repos.read` | high | Build from GitHub repositories the team's ZevCloud GitHub App can see, and list them. A build can print the whole repository into logs your app reads. |
| `zevcloud.deploy` | standard | Deploy, roll back, approve or reject deployments, and start, stop and restart services. |
| `zevcloud.env.write` | standard | Set and delete environment variables. ZevCloud never returns values, but your build and runtime logs show whatever your code prints. |
| `zevcloud.services.delete` | high | Delete services the app created, only on teams the person owns. |
| `zevcloud.services.all` | high | Act on every service in the team, not only the ones the app created. |

### Routes by scope

All paths are relative to `https://api.zevcloud.net/v1`.

**`zevcloud.services.read`**

- `GET /environments/:environmentId/services`
- `GET /environments/:environmentId/services/:serviceId`
- `GET /environments/:environmentId/services/:serviceId/currently-running`
- `GET /services/:serviceId`
- `GET /services/:serviceId/sync-status`
- `GET /services/:serviceId/metrics`
- `GET /services/:serviceId/runtime-logs`
- `POST /services/:serviceId/logs/session`
- `GET /services/:serviceId/deployments`
- `GET /services/:serviceId/deployments/:deploymentId`
- `GET /services/:serviceId/deployments/:deploymentId/logs`
- `POST /services/:serviceId/deployments/:deploymentId/logs/session`
- `GET /services/:serviceId/env` (names only, values masked)

**`zevcloud.services.create`**

- `POST /projects` creates a project with its Production environment. The response includes `environments[0].id`.
- `POST /environments/:environmentId/services/application`
- `POST /environments/:environmentId/services/wordpress`
- `POST /environments/:environmentId/services/database`
- `POST /environments/:environmentId/services/upload`
- `POST /services/:serviceId/uploads/url`
- `POST /services/:serviceId/uploads/:uploadId/complete`

**`zevcloud.repos.read`**

- `GET /github/repos` lists the repositories ZevCloud can see for this team.
- `GET /github/repos/:owner/:repo/branches`
- `GET /github/repos/:owner/:repo/access` checks one repository before you create a service from it. See [Building from GitHub](#building-from-github).
- With this scope, `POST /environments/:environmentId/services/application` also accepts `sourceMode: "github"`. Deploying, rolling back or approving a deployment of a service that builds from GitHub needs it too.

**`zevcloud.deploy`**

- `POST /services/:serviceId/deployments`. The body may include `commitSha` to roll back, but only to a commit your app deployed.
- `POST /services/:serviceId/deployments/:deploymentId/approve`
- `POST /services/:serviceId/deployments/:deploymentId/reject`
- `POST /services/:serviceId/start`
- `POST /services/:serviceId/stop`
- `POST /services/:serviceId/restart`
- `POST /services/:serviceId/deploy`
- `POST /services/:serviceId/wp-restart-db`

**`zevcloud.env.write`**

- `POST /services/:serviceId/env`
- `POST /services/:serviceId/env/bulk`
- `DELETE /services/:serviceId/env/:envVarId`

**`zevcloud.services.delete`**

- `DELETE /environments/:environmentId/services/:serviceId`. Only on teams the person owns. Without `zevcloud.services.all`, only services your app created.

**`zevcloud.services.all`**

This scope has no routes of its own. It widens the other scopes from "services this app created" to every service in the team. Together with `zevcloud.services.delete`, your app can delete any service in teams the person owns, including ones the person created. Ask for both only if your app really manages the whole team.

### High-risk scopes

`zevcloud.repos.read`, `zevcloud.services.delete` and `zevcloud.services.all` are high risk. People see them unticked on the consent screen.

- The person must have signed in recently to grant them.
- They end after a confirmation window (30 days by default).
- After that, calls that need them return `high_risk_confirmation_required`. Send the person to confirm again through ZevAuth. See [High-risk permissions](https://docs.zevauth.com/delegated-access/high-risk/).

## Calling the API

Base URL:

```
https://api.zevcloud.net/v1
```

Send two headers on every call:

```
Authorization: Bearer <partner access token>
x-team-id: <team id>
```

- The access token comes from ZevAuth and lasts 10 minutes. Refresh it through ZevAuth before it runs out.
- `x-team-id` names the team you are acting on. It must be a team the person connected. ZevCloud never takes the team from the token or the request body.

### What apps can never do

Some actions belong to the person alone. They are refused for every app, whatever scopes it holds, with `not_available_to_partners`:

- Paying invoices, plan upgrades and anything else to do with billing
- Zev Credit
- Storage subscription changes
- Buying or transferring domains
- Team settings, members, invites and ownership
- Creating or deleting teams
- API keys
- Terminal access and running commands
- File access and downloads
- WordPress credentials, auto-login and SFTP
- Database public access and database credentials
- ZevBox
- Connecting or disconnecting GitHub, and linking GitHub accounts
- Security settings
- Saved addresses
- Policies
- Verification

Projects can be created but not listed or edited. Cross-team lists (`/aggregate/*`) are not available.

## Building from GitHub

Building from a public URL or an upload needs nothing extra. Building from GitHub needs `zevcloud.repos.read`, a high-risk scope: your app chooses the build steps, so a build can print the whole repository into logs your app reads. If ZevCloud's GitHub App is installed with access to all repositories, that can cover a whole organisation. Ask for it only if your app really builds people's GitHub code.

Your app can only build repositories the team's ZevCloud GitHub App can already see. Which repositories that is stays the person's choice on GitHub, so you never get wider access than they gave ZevCloud.

Check a repository first:

```bash
curl "$ZC/github/repos/acme/api/access" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-team-id: $TEAM"
```

If ZevCloud can see it, you get `{ "ok": true }`. Then create the service with `sourceMode: "github"` and `gitRepoUrl: "https://github.com/acme/api"`. Private repositories get a read-only deploy key automatically.

If not, you get `ok: false` with an `access` value, a `url` and `steps` to show the person. Creating the service anyway returns the same details with `409 github_access_required`.

| `access` | Meaning | `url` opens |
|---|---|---|
| `app_not_installed` | The ZevCloud GitHub App is not installed on the account or organisation that owns the repository. The team owner connects GitHub in ZevCloud. On an organisation, an organisation owner approves the install. | ZevCloud team settings, Integrations |
| `repo_not_visible` | The app is installed on the owner, but this repository is not in its "Repository access" list. On an organisation, only an organisation owner can change the list. | The installation's settings page on GitHub |

Show the `steps` as they come; they already cover personal accounts and organisations. When the person is done, check again.

If the person later removes access on GitHub, takes back `zevcloud.repos.read`, or lets its confirmation lapse, new builds from that repository stop, including approving a held deployment. While the confirmation has lapsed, those calls return `high_risk_confirmation_required`.

## Only your own services

Every service your app creates is marked as created by your app.

- Without `zevcloud.services.all`, every service route refuses services your app did not create, with `not_created_by_partner`.
- Lists only include your own services.
- With `zevcloud.services.all`, your app can act on every service in the team, within its other scopes.

Store the service ids your app creates. You cannot list projects to find them again.

## Secrets and money

### Secrets

ZevCloud never hands secrets to your app.

- Environment variable values are never returned. Reads show names only.
- Passwords, keys and connection strings are never returned to apps.
- WordPress, database and file-transfer passwords for a new WordPress site are emailed to the person, never to an address your app chooses.
- Billing details (plans, invoices, billing status) are left out of everything your app reads.

Variables are still given to the code you build and run. If that code prints a value, it shows in build or runtime logs, which your app can read. Don't print secrets.

### Money

Apps never pay for anything.

- Services are created on the free plan. Leave out `planId` and the free plan is used. Free plan limits come from the team's ZevCloud plans.
- Databases are created on a free database tier. Leave out `databasePlanSlug` and the free tier is used. Naming a paid tier (by `databasePlanSlug` or id) is refused the same way as a paid plan, and an unknown tier is a `400`.
- If you choose a paid plan or tier, the call returns `402 payment_required` with a `url`. Show that link to the person. They open it in ZevCloud and decide there.
- Your app can create a limited number of projects in each team (10 unless ZevCloud changes it). Past that, `POST /projects` returns `403 project_limit_reached` with the `limit`. Reuse your projects.

### Fair use

The person must have accepted ZevCloud's fair use policy in the console. Until they do, calls return `403 fair_use_required`. Send them to the [ZevCloud console](https://console.zevcloud.net) to accept it, then retry.

## End-to-end example

This example creates a project, adds an application from a public Git repository, sets environment variables, deploys it and follows the deployment.

```bash
export ZC=https://api.zevcloud.net/v1
export TOKEN=<partner access token>
export TEAM=<team id>
```

### 1. Create a project

Needs `zevcloud.services.create`.

```bash
curl -X POST "$ZC/projects" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-team-id: $TEAM" \
  -H "Content-Type: application/json" \
  -d '{"name": "Recipe Finder"}'
```

The project comes with a Production environment. Keep `environments[0].id` from the response.

### 2. Create an application

Needs `zevcloud.services.create`. Use `sourceMode: "public-url"` to build from a public Git repository with no access setup.

```bash
curl -X POST "$ZC/environments/<environmentId>/services/application" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-team-id: $TEAM" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Recipe Finder",
    "slug": "recipe-finder",
    "sourceMode": "public-url",
    "gitRepoUrl": "https://github.com/example/recipe-finder",
    "gitBranch": "main",
    "port": 3000
  }'
```

The `slug` becomes the service's default address, `<slug>.zevcloud.app`. Other optional fields include `rootDirectory`, `installCommand`, `buildCommand`, `startCommand`, `isStatic`, `isSpa`, `outputDirectory`, `runtime` and `runtimeVersion`. Keep the service `id` from the response.

### 3. Set environment variables

Needs `zevcloud.env.write`.

```bash
curl -X POST "$ZC/services/<serviceId>/env/bulk" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-team-id: $TEAM" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {"key": "NODE_ENV", "value": "production"},
      {"key": "RECIPES_API_KEY", "value": "rk_123", "isSecret": true}
    ]
  }'
```

The response lists the result for each item. Values are never sent back.

### 4. Deploy

Needs `zevcloud.deploy`.

```bash
curl -X POST "$ZC/services/<serviceId>/deployments" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-team-id: $TEAM"
```

Keep the deployment `id` from the response.

### 5. Follow the deployment

Needs `zevcloud.services.read`. Poll the deployment every few seconds until it reaches a final status: `running`, `failed`, `cancelled` or `rolled_back`.

- `queued`, `building` and `deploying` mean keep polling.
- `pending_approval` means the team approves deployments before they go out. Keep polling, or approve it with `zevcloud.deploy` if the person asked you to.
- `blocked` means the team has an unpaid invoice. Stop and tell the person to check ZevCloud.

```bash
curl "$ZC/services/<serviceId>/deployments/<deploymentId>" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-team-id: $TEAM"
```

Read the build logs:

```bash
curl "$ZC/services/<serviceId>/deployments/<deploymentId>/logs" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-team-id: $TEAM"
```

See [Deployment statuses](/api/deployments#deployment-statuses) for every status.

> **Tip:**
>
> The access token lasts 10 minutes. A long build can outlast it. Refresh the token through ZevAuth and keep polling with the new one.

## Errors

Errors are JSON:

```json
{
  "statusCode": 403,
  "code": "not_created_by_partner",
  "reason": "not_created_by_partner",
  "message": "This app can only act on services it created."
}
```

Branch on `reason`. The `message` is for people and may change.

| Reason | Status | What to do |
|---|---|---|
| `invalid_token` | 401 | The token is not valid. Get a new one through ZevAuth. |
| `stale_token` | 401 | The connection changed. Refresh the access token through ZevAuth and retry once. |
| `grant_not_found` | 401 | The person disconnected your app. Ask them to connect again. |
| `grant_not_found` | 503 | ZevCloud could not confirm the connection with ZevID just now. Retry after a few seconds. |
| `grant_revoked` | 401 | The person disconnected your app. Ask them to connect again. |
| `grant_suspended` | 403 | The connection is suspended. Do not retry. Contact ZevID through ZevAuth. |
| `grant_expired` | 401 | A test connection ended. ZevAuth reports the same ending as `test_grant_expired`. Connect again. |
| `not_available_to_partners` | 403 | This action is for the person only. Send them to ZevCloud. |
| `scope_not_granted` | 403 | Your app does not hold the scope. Ask the person to connect again with it. |
| `scope_paused` | 403 | The permission is paused, for this connection or for your app everywhere (`level: "approval"`). Do not retry. See [Paused permissions](https://docs.zevauth.com/delegated-access/paused-permissions/). |
| `high_risk_confirmation_required` | 403 | Send the person to confirm the high-risk permission again through ZevAuth. |
| `team_required` | 400 | Add the `x-team-id` header. |
| `team_not_in_grant` | 403 | The person did not connect this team. Ask them to add it. |
| `team_suspended` | 403 | The team is suspended. Do not retry. The person can see why in ZevCloud. |
| `role_lost` | 403 | The person is no longer an owner or admin of this team. |
| `owner_required` | 403 | Deleting needs the person to own the team. |
| `not_created_by_partner` | 403 | Your app did not create this service. |
| `rollback_not_own_commit` | 403 | You can only roll back to a commit your app deployed. |
| `payment_required` | 402 | Show the `url` from the response to the person. |
| `project_limit_reached` | 403 | Your app has created as many projects as it can in this team. Reuse one. |
| `github_access_required` | 409 | ZevCloud can't see the repository. Show the person the `url` and `steps` from the response. |
| `fair_use_required` | 403 | Send the person to the ZevCloud console to accept the fair use policy. |
| `rate_limited` | 429 | Wait for the `Retry-After` header or `retryAfterSeconds`, then retry. |

ZevAuth's [Errors](https://docs.zevauth.com/delegated-access/errors/) page covers errors from connecting, tokens and refresh.

## Rate limits

Limits apply per app:

- 120 requests a minute
- 10 deploys a minute

Over the limit, calls return `429 rate_limited` with a `Retry-After` header and `retryAfterSeconds` in the body. See also ZevAuth's [Rate limits](https://docs.zevauth.com/delegated-access/rate-limits/).

## Attribution

Everything your app does is recorded with your app and the person it acts for.

- People see "Deployed by &lt;your app&gt;" on deployments your app starts.
- Team owners and admins see connected apps in their team settings.
- People manage and disconnect apps on the ZevID account site.

When a person disconnects, your calls start failing with `grant_revoked`. ZevAuth can also tell you through [webhooks](https://docs.zevauth.com/delegated-access/webhooks/). See [Revocation](https://docs.zevauth.com/delegated-access/revocation/).

## Testing

Test versions of your app use ZevAuth test environments.

- A test version connects only to the ZevCloud teams of people on your own team.
- Calls work the same as for a live app.
- Test connections end on their own. After that, calls fail with `grant_expired` (ZevAuth calls it `test_grant_expired`) and you connect again.

See [Test environments](https://docs.zevauth.com/delegated-access/testing/).
