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
- Your app is approved for ZevCloud through ZevAuth. See Getting approved.
- 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.
- ZevAuth gives your app a 10 minute access token and a refresh token. See Using the access token and Refresh tokens.
- 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.
| 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/servicesGET /environments/:environmentId/services/:serviceIdGET /environments/:environmentId/services/:serviceId/currently-runningGET /services/:serviceIdGET /services/:serviceId/sync-statusGET /services/:serviceId/metricsGET /services/:serviceId/runtime-logsPOST /services/:serviceId/logs/sessionGET /services/:serviceId/deploymentsGET /services/:serviceId/deployments/:deploymentIdGET /services/:serviceId/deployments/:deploymentId/logsPOST /services/:serviceId/deployments/:deploymentId/logs/sessionGET /services/:serviceId/env(names only, values masked)
zevcloud.services.create
POST /projectscreates a project with its Production environment. The response includesenvironments[0].id.POST /environments/:environmentId/services/applicationPOST /environments/:environmentId/services/wordpressPOST /environments/:environmentId/services/databasePOST /environments/:environmentId/services/uploadPOST /services/:serviceId/uploads/urlPOST /services/:serviceId/uploads/:uploadId/complete
zevcloud.repos.read
GET /github/reposlists the repositories ZevCloud can see for this team.GET /github/repos/:owner/:repo/branchesGET /github/repos/:owner/:repo/accesschecks one repository before you create a service from it. See Building from GitHub.- With this scope,
POST /environments/:environmentId/services/applicationalso acceptssourceMode: "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 includecommitShato roll back, but only to a commit your app deployed.POST /services/:serviceId/deployments/:deploymentId/approvePOST /services/:serviceId/deployments/:deploymentId/rejectPOST /services/:serviceId/startPOST /services/:serviceId/stopPOST /services/:serviceId/restartPOST /services/:serviceId/deployPOST /services/:serviceId/wp-restart-db
zevcloud.env.write
POST /services/:serviceId/envPOST /services/:serviceId/env/bulkDELETE /services/:serviceId/env/:envVarId
zevcloud.services.delete
DELETE /environments/:environmentId/services/:serviceId. Only on teams the person owns. Withoutzevcloud.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/v1Send 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-idnames 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.
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, withnot_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
planIdand 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
databasePlanSlugand the free tier is used. Naming a paid tier (bydatabasePlanSlugor id) is refused the same way as a paid plan, and an unknown tier is a400. - If you choose a paid plan or tier, the call returns
402 payment_requiredwith aurl. 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 /projectsreturns403 project_limit_reachedwith thelimit. 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,buildinganddeployingmean keep polling.pending_approvalmeans the team approves deployments before they go out. Keep polling, or approve it withzevcloud.deployif the person asked you to.blockedmeans 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.
| 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. |
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 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 ittest_grant_expired) and you connect again.
See Test environments.
Updated at, Tuesday, October 20, 2026