ZevCloud
Dashboard

Build on ZevCloud

Build on ZevCloud with connected apps

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.

How it works

  1. Your app is approved for ZevCloud through ZevAuth. See 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.
  3. ZevAuth gives your app a 10 minute access token and a refresh token. See Using the access token and 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.

ScopeRiskWhat it allows
zevcloud.services.readstandardRead services, deployments, build logs, runtime logs, metrics and environment variable names. Values are always masked.
zevcloud.services.createstandardCreate projects and services, from public URLs or uploads. Free plan and free database tiers only.
zevcloud.repos.readhighBuild 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.deploystandardDeploy, roll back, approve or reject deployments, and start, stop and restart services.
zevcloud.env.writestandardSet and delete environment variables. ZevCloud never returns values, but your build and runtime logs show whatever your code prints.
zevcloud.services.deletehighDelete services the app created, only on teams the person owns.
zevcloud.services.allhighAct 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.
  • 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.

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:

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.

accessMeaningurl opens
app_not_installedThe 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_visibleThe 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 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.

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

1. Create a project

Needs zevcloud.services.create.

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.

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.

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.

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.
curl "$ZC/services/<serviceId>/deployments/<deploymentId>" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-team-id: $TEAM"

Read the build logs:

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

See Deployment statuses for every status.

Errors

Errors are 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.

ReasonStatusWhat to do
invalid_token401The token is not valid. Get a new one through ZevAuth.
stale_token401The connection changed. Refresh the access token through ZevAuth and retry once.
grant_not_found401The person disconnected your app. Ask them to connect again.
grant_not_found503ZevCloud could not confirm the connection with ZevID just now. Retry after a few seconds.
grant_revoked401The person disconnected your app. Ask them to connect again.
grant_suspended403The connection is suspended. Do not retry. Contact ZevID through ZevAuth.
grant_expired401A test connection ended. ZevAuth reports the same ending as test_grant_expired. Connect again.
not_available_to_partners403This action is for the person only. Send them to ZevCloud.
scope_not_granted403Your app does not hold the scope. Ask the person to connect again with it.
scope_paused403The permission is paused, for this connection or for your app everywhere (level: "approval"). Do not retry. See Paused permissions.
high_risk_confirmation_required403Send the person to confirm the high-risk permission again through ZevAuth.
team_required400Add the x-team-id header.
team_not_in_grant403The person did not connect this team. Ask them to add it.
team_suspended403The team is suspended. Do not retry. The person can see why in ZevCloud.
role_lost403The person is no longer an owner or admin of this team.
owner_required403Deleting needs the person to own the team.
not_created_by_partner403Your app did not create this service.
rollback_not_own_commit403You can only roll back to a commit your app deployed.
payment_required402Show the url from the response to the person.
project_limit_reached403Your app has created as many projects as it can in this team. Reuse one.
github_access_required409ZevCloud can’t see the repository. Show the person the url and steps from the response.
fair_use_required403Send the person to the ZevCloud console to accept the fair use policy.
rate_limited429Wait for the Retry-After header or retryAfterSeconds, then retry.

ZevAuth’s 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.

Attribution

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

  • People see “Deployed by <your app>” 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. See 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.

Updated at, Tuesday, October 20, 2026