---
title: Leaving Zeabur checklist — reference for coding agents
audience: coding agents (Claude Code, Codex, Cursor, …) helping a Zeabur user confirm nothing is left on Zeabur before they request a refund
source: https://zeabur.com/blogs/leaving-zeabur-checklist.md
---

# Leaving Zeabur checklist

Zeabur is shutting down. Your user wants to know what they still have on Zeabur, finish what is left, and then request a refund of their account balance from Zeabur Support. Your job is to inventory, report, help them close out each item with their approval, and draft the support ticket. The refund itself is handled by Zeabur Support in that ticket, not by you.

If the user has not moved their services yet, they need https://zeabur.com/blogs/migrate-from-zeabur.md first. Tell them, and stop here unless they only want the inventory.

Talk to the user in their language. This document is updated as the shutdown progresses. Fetch it again from the source URL at the start of each session.

## Ground rules

- **Read only by default.** Do not stop, suspend, delete or change anything on Zeabur unless the user asks for that specific item and confirms it. Deletion is final: there is no way back to Zeabur afterwards.
- **Never print secrets.** API keys, passwords, tokens, transfer codes and connection strings stay out of replies, logs and files. Show names and masked values only. `zeabur project export` and `zeabur variable list` output contains secrets; if you need them, keep them in `chmod 600` files on the user's machine.
- **Never ask the user to paste a Zeabur API token into the chat.** Use the CLI already signed in on their machine: `npx zeabur@latest`. Add `-i=false` to every command: without it, some commands (for example `domain list`) open an interactive prompt and exit with 1. Add `--json` for structured output, and wrap remote calls in a timeout, for example `timeout 60 npx zeabur@latest -i=false …`.
- **Use `api graphql` only for queries.** Some fields are not in the regular commands, so this checklist reads them with `npx zeabur@latest api graphql`. It uses the same login and prints the raw JSON response. Do not send mutations with it.
- **Do not talk about refund amounts or methods.** Do not calculate, estimate or promise an amount, and do not suggest a refund method. Zeabur Support confirms both in the ticket.
- **Do not submit the ticket.** Draft it; the user sends it.
- When something here does not match what you observe, trust the observation and tell the user.

## Shutdown timeline (2026)

| Date | What happens |
| --- | --- |
| Now | No new dedicated servers or domain purchases. Domains bought on Zeabur can be transferred out from the dashboard. Account balance refunds are handled through Zeabur Support. |
| From Dec 1 | Dedicated servers stop renewing and shut down on their own expiry dates. Shared-cluster services are suspended; users can resume them manually to back up data. |
| Dec 31 | All dedicated servers and shared clusters stop. |

Data deletion dates have not been announced.

## Before you start

1. Run `npx zeabur@latest version`. This checklist needs 0.24.0 or later; if npx serves an older cached copy, use `npx zeabur@0.24.0` or newer explicitly.
2. Run `npx zeabur@latest auth status`. It also prints the account's credit; do not repeat it to the user. If it is not logged in, ask the user to run `npx zeabur@latest auth login` themselves (in Claude Code: `! npx zeabur@latest auth login`). Confirm the account is the one that owns the services and pays for them.
3. Run `npx zeabur@latest workspace list`. It prints a table without headers, even with `--json`: the personal workspace is the line starting with `*`, and each team line has its ID, name, `team` and the user's role. Resources can live in the personal workspace and in any team. Ask which workspaces the user wants to close out.

For a team, the sections below say how to scope each check. Not every command honours `--workspace`: the `email` commands silently ignore it and return the personal workspace, so use the team queries given in each section.

The account can be big: dozens of projects and hundreds of services. Prefer the GraphQL queries below, which return a whole project at a time, over running `service get` per service.

## Inventory

Go through every section for every workspace in scope. Record each item with: what it is, its state, its deadline if any, and which list it belongs to (see [Report](#report)).

### Subscription and balance

```bash
npx zeabur@latest --json profile info -v
```

`subscription.plan` is the current plan. Ignore `credit`; balances are for Zeabur Support. To see when a paid plan charges next:

```bash
npx zeabur@latest api graphql -f query='query { me { subscription { plan nextBillingAt } } }'
```

`subscription` is `null` when the user has no subscription. The response does not say whether the user has cancelled renewal, so for a paid plan, ask them, or ask them to check the dashboard's billing page. This covers the personal account only; for a team, ask the user to read the plan from the team's billing page.

A paid plan that still renews belongs on the **still billing** list.

### Dedicated servers

```bash
npx zeabur@latest --json server list
npx zeabur@latest --json server get <serverID>
npx zeabur@latest api graphql -f query='query { servers { _id name isManaged expiresAt isAutoRenewDisabled } }'
```

For a team, list the team's servers with `query($ownerID: ObjectID!) { servers(ownerID: $ownerID) { … } }` and `-f ownerID=<workspaceID>`.

`isManaged: true` (`IsManaged` in `server get`; `server list` does not include it) is a server rented through Zeabur: it is billed until it expires, and its disk is deleted afterwards. `isManaged: false` is a machine in the user's own cloud account; it stays with the user, but Zeabur's components on it stop working.

For a rented server, `expiresAt` is the date it stops billing and shuts down, and `isAutoRenewDisabled: false` means it still renews until Dec 1: a server that expires before Dec 1 with auto-renew on will be renewed, and billed, once more. Put it on the **still billing** list with `expiresAt` as its deadline, and on the **take with you** list if it holds data. If `expiresAt` is `null`, or the response has an error whose `path` points at that server's `expiresAt` (the command then exits with 1 but still prints the other servers in `data`), ask the user to read the date from the server's **Settings** tab in the dashboard. Do not run `server ssh-info`: it prints the password.

### Projects and services

```bash
npx zeabur@latest -i=false --json project list
```

`project list` also returns projects the user only collaborates on. For each project, read its owner, region and environments:

```bash
npx zeabur@latest api graphql -f query='query($id: ObjectID!) { project(_id: $id) { name owner { username } region { id } environments { _id name } } }' -f id=<projectID>
```

Keep only projects whose `owner.username` is the user's; the others bill their owners. For a team, list its projects with `query($o: ObjectID) { projects(ownerID: $o, limit: 100) { edges { node { _id name region { id } } } } }` and `-f o=<workspaceID>` (page with `skip`); team projects have no `owner`.

Then, for each environment, read the services with their status and volumes:

```bash
npx zeabur@latest api graphql -f query='query($id: ObjectID!, $env: ObjectID!) { project(_id: $id) { services { _id name status(environmentID: $env) volumes(environmentID: $env) { dir } domains(environmentID: $env) { domain isGenerated } } } }' -f id=<projectID> -f env=<environmentID>
```

A project runs on a dedicated server when its region ID is `server-<serverID>`; otherwise it runs on a shared cluster. If that server is not in the workspace's server list, it belongs to someone else (often a team the user is in): the services still need their data taken out, but the server's billing is not the user's. Say so in the report.

- A shared-cluster service whose `status` is not `SUSPENDED` goes on the **still billing** list.
- A service with volumes, or a database, goes on the **take with you** list until the user confirms they have the data. Its deadline is the server's `expiresAt`, or Dec 31 on a shared cluster.
- Domains with `isGenerated: false` are custom domains; check them in [Domains](#domains).

For each service with volumes or a database, also list its Zeabur backups. They may be the only older copies of the data:

```bash
npx zeabur@latest api graphql -f query='query($env: ObjectID!, $svc: ObjectID!) { backups(environmentID: $env, serviceID: $svc) { createdAt status fileSize } }' -f env=<environmentID> -f svc=<serviceID>
```

Backups the user wants to keep go on the **take with you** list. Do not query `downloadURL`: it is a signed link to the data. The user downloads backups from the service's page in the dashboard.

Do not use `project export` to find volumes: its output contains secrets.

### Domains

```bash
npx zeabur@latest --json domain list-registered          # bought on Zeabur
npx zeabur@latest --json domain get-registered --id <domainID>
```

Custom domains bound to services come from the `domains` field in [Projects and services](#projects-and-services).

For a team, use `query($o: ObjectID!) { registeredDomains(ownerID: $o) { __typename } }` with `-f o=<workspaceID>` to see whether it has any; an empty list means none.

Domains bought on Zeabur go on the **take with you** list until transferred. The user transfers them from https://zeabur.com/domains; do not ask for or handle transfer codes.

For every custom domain bound to a service, look up its A and CNAME records with `dig +short <domain> A` and `dig +short <domain> CNAME`. If `dig` is not installed, use DNS over HTTPS: `curl -s -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=<domain>&type=A'` (and `type=CNAME`). A domain still points at Zeabur when it resolves to:

- a `*.zeabur-dns.com`, `*.servers.onzeabur.com` or `*.zeabur.app` name, or
- the IP of a server rented through Zeabur (`isManaged: true`).

Those go on the **still pointing at Zeabur** list. A domain that resolves to the user's own server (`isManaged: false`) is not stale by itself: the user may have moved the service off Zeabur on the same machine. Ask whether that domain is still served by a Zeabur service; only then does it go on the list. A record behind the Cloudflare proxy, or one that resolves to an IP you cannot place, is unknown: list it and ask the user. A domain with no record at all is a leftover binding and needs nothing.

### Email

```bash
npx zeabur@latest --json email status
npx zeabur@latest --json email domains list
npx zeabur@latest --json email keys list
npx zeabur@latest --json email webhooks list
npx zeabur@latest --json email scheduled list
```

These cover the personal workspace only; `--workspace` is ignored. For a team, list its sending domains with `query($o: ObjectID) { listZSendDomains(ownerID: $o, page: 1, pageSize: 50) { totalCount } }` and `-f o=<workspaceID>`, and ask the user to check the team's API keys, webhooks and scheduled emails in the dashboard.

Scheduled emails will still be sent unless cancelled. `email scheduled list` uses the CLI login; do not ask the user for a Z-Send API key. API keys and sending domains still in use mean some application still sends mail through Zeabur: put those on the **still pointing at Zeabur** list. Revoking keys or cancelling scheduled emails needs the user's approval for each one.

### AI Hub

AI Hub has been removed from the Zeabur API and from the CLI (0.24.0 dropped the `ai-hub` commands), so you cannot read keys or auto-recharge settings. Ask the user whether they ever used AI Hub. If they did, add a line to the ticket asking Zeabur Support to confirm that AI Hub auto-recharge is off. AI Hub keys and endpoints still in their code turn up in [Outside Zeabur](#outside-zeabur).

### Outside Zeabur

Ask the user which repositories and systems to check, then look for anything that still depends on Zeabur:

- In code and config: `grep -rIno -i -E '[A-Za-z0-9._-]*zeabur[A-Za-z0-9._-]*' --exclude-dir={node_modules,.git,dist,build} . | sort -u`. `-o` prints only the matched name, such as a host or a variable name, not the whole line, so values like tokens or connection strings on the same line stay out of the output. Do not run a plain `grep -n` that prints whole lines. Look for `*.zeabur.app` URLs, `*.zeabur.internal` hosts, Zeabur API, Email and AI Hub endpoints, and `ZEABUR_` variables.
- In CI: secret names containing `ZEABUR` (for GitHub, `gh secret list`). Show names only.
- The Zeabur GitHub App: ask the user to check GitHub **Settings → Applications → Installed GitHub Apps** and uninstall it once nothing deploys from Zeabur.
- Things the user has to check by hand, because you cannot see them: OAuth callback URLs, webhooks and payment provider settings that use a Zeabur URL.

Everything found here goes on the **still pointing at Zeabur** list. It does not block the refund, but it stops working after Dec 31.

## Report

Give the user three lists, one row per item, with the deadline where there is one:

1. **Still billing.** Anything that still charges the user: a renewing plan, a rented server before its expiry, running shared-cluster services, AI Hub auto-recharge. The refund request waits until this list is empty.
2. **Take with you.** Data and domains that disappear after their deadline: volumes, databases, backups, registered domains.
3. **Still pointing at Zeabur.** References that break after Dec 31. They do not block the refund.

End the report with one line: either "ready to request the refund" or what is still blocking it.

Then offer to help with each item, one at a time, and only act after the user confirms that item. For deleting services, follow the Cleanup section of https://zeabur.com/blogs/migrate-from-zeabur.md. If something cannot be stopped, or its state looks wrong, do not work around it: note it for the ticket.

## Ticket draft

When the **still billing** list is empty, or the user wants to send the ticket anyway, draft the ticket in the user's language. Include:

- the account email or username, and the workspaces in scope
- the request: account balance refund
- what was stopped or removed, and when
- anything still open, and why (for example: a server that ends on its own expiry date, or a resource that could not be stopped)

Do not include secrets, amounts or a preferred refund method.

Tell the user to open a ticket at https://zeabur.com/support and paste the draft. The draft must say that this is an account balance refund request. From then on everything happens in that ticket: Zeabur Support confirms the amount, decides the refund method and replies there with progress. Your work ends here.

## Help

If the user is stuck, summarise what you found and what is blocking, and point them to https://zeabur.com/support.
