| name | subdomain |
| description | Use when binding a zeabur.app subdomain to a server — claiming <label>.zeabur.app, pointing it at a VPS IP, getting an HTTPS certificate for it, changing the target IP, or deleting a binding. Use when the user says "give my server a domain", "bind a subdomain", "I want HTTPS on my VPS", or "change where my subdomain points". |
Zeabur.app Subdomain
Bind <label>.zeabur.app to any public server IP — the DNS record points directly at the machine (this is not a proxy, and no Zeabur project is involved). The target does not have to be a Zeabur-rented server: any VPS the user owns works. Because DNS resolves straight to the server and port 80 is reachable, Let's Encrypt HTTP-01 issuance works out of the box for HTTPS.
Set up the zapi helper from the auth skill first.
Quota per workspace: Free 5 · Dev 20 · Pro 100 · Team 500 bindings.
1. Claim a subdomain
label is the part before .zeabur.app:
zapi '{"query":"mutation($label: String!, $t: ZeaburAppBindingTargetInput!){ createZeaburAppBinding(label: $label, target: $t) { _id hostname dnsStatus } }","variables":{"label":"myapp","t":{"ipv4":"203.0.113.10"}}}' \
| jq '.data.createZeaburAppBinding'
ipv6 is optional in the target. If the label is taken or invalid, the call returns an error — pick another label. For a team workspace, add ownerID.
2. Wait for DNS to go ACTIVE
zapi '{"query":"query($id: ObjectID!){ zeaburAppBinding(id: $id) { hostname dnsStatus dnsErrorCode } }","variables":{"id":"<binding-id>"}}' \
| jq '.data.zeaburAppBinding'
Poll every ~10 seconds until dnsStatus is ACTIVE (PENDING → ACTIVE; on ERROR, surface dnsErrorCode). Then verify HTTP works — assuming something is listening on port 80:
curl -sI "http://<label>.zeabur.app" | head -1
3. HTTPS
Pick by how the server is run (the deploy skill decides this):
- ZeaburOS (the recommended path) — nothing to set up: the Zeabur ingress-controller auto-issues and renews the certificate for every Ingress host (built-in ACME). Do not add cert-manager for public TLS there — it fights the controller over the ACME challenge. Full details in the deploy skill.
- Vanilla k3s — cert-manager with an HTTP-01 ClusterIssuer; the certificate lives in a k8s Secret referenced by the Ingress and renews automatically. Recipe in the deploy skill.
- Self-managed machine (docker / plain nginx / …) — run
certbot on the server (certbot --nginx, or certbot certonly --standalone if nothing serves port 80 yet). Auto-renewal comes from the certbot systemd timer.
Either way, the binding must be ACTIVE before issuance — HTTP-01 validates by fetching from the hostname. Verify afterwards:
curl -sI "https://<label>.zeabur.app" | head -1
Zeabur-issued certificate (CSR flow) — currently broken, do not use
The API also has refreshCertificateWithCSR(csr), meant to let Zeabur sign a certificate for your hostname while the private key stays on your server. It does not yet recognize zeabur.app bindings (it only checks project-bound domains) and returns NOT_FOUND: domain not found for any binding hostname. Until the backend fix lands, use the cert-manager/certbot paths above. When it is fixed, this becomes the option for machines where port 80 cannot be exposed — renewal is manual (re-run the exchange; check expiry with openssl x509 -enddate -noout -in <cert>).
Manage bindings
zapi '{"query":"{ zeaburAppBindings { edges { node { _id hostname ipv4 ipv6 dnsStatus } } } }"}' \
| jq -r '.data.zeaburAppBindings.edges[].node | "\(._id)\t\(.hostname)\t→ \(.ipv4)\t\(.dnsStatus)"'
zapi '{"query":"mutation($id: ObjectID!, $t: ZeaburAppBindingTargetInput!){ updateZeaburAppBindingTarget(id: $id, target: $t) { hostname dnsStatus } }","variables":{"id":"<binding-id>","t":{"ipv4":"203.0.113.99"}}}'
zapi '{"query":"mutation($id: ObjectID!){ deleteZeaburAppBinding(id: $id) { hostname } }","variables":{"id":"<binding-id>"}}'
After updating a target, dnsStatus cycles back through PENDING — poll it to ACTIVE again. The certificate is tied to the hostname, not the IP, so repointing does not require a new certificate.