API
Everything the console and the CLI do goes through one GraphQL API, and it is yours to script against. There is no separate "public" surface with fewer capabilities: what the dashboard can do, a service token can do, with the same permission checks.
Endpoint
POST https://server.anyport.dev/query
Content-Type: application/json
Authorization: Bearer apt_…
Self-hosted installations serve the same path on their own console address.
Authentication
Create a service token with the role the job needs, and send it as a
bearer credential. A token belongs to one organization, so no organization header is required;
if you send X-Org-Id, it has to name that organization.
anyport token create ci-reporter --role viewer
A token is refused from the moment an admin revokes it. Tokens do not count as seats, and every change they make appears in the audit log under the token's name.
A first request
curl -s https://server.anyport.dev/query \
-H "Authorization: Bearer $ANYPORT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ projects { metadata { name } spec { clusterRef } phase { phase reason } } }"}'
{
"data": {
"projects": [
{ "metadata": { "name": "demo" }, "spec": { "clusterRef": "prod" }, "phase": { "phase": "Healthy", "reason": null } }
]
}
}
Deploy a new image from CI the way the CLI does: change the app's draft, then publish it.
fields names the spec fields you are setting, so nothing else on the app is touched.
curl -s https://server.anyport.dev/query \
-H "Authorization: Bearer $ANYPORT_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"query": "mutation($input: ContainerAppPatchIn!, $fields: [String!]!) { patchContainerAppDraft(input: $input, fields: $fields) { metadata { name } } }",
"variables": {
"input": { "metadata": { "name": "api" }, "spec": { "clusterRef": "prod", "projectRef": "demo", "image": "ghcr.io/acme/api:2.4.0" } },
"fields": ["image"]
}
}
JSON
curl -s https://server.anyport.dev/query \
-H "Authorization: Bearer $ANYPORT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"mutation { publishContainerApp(name: \"api\", projectName: \"demo\", clusterName: \"prod\") { metadata { name } phase { phase reason } } }"}'
anyport app deploy api -p demo --image ghcr.io/acme/api:2.4.0 --wait does both and waits
for the rollout. The CLI is a client of this API and nothing more.
The schema
The full schema is published at /api/schema.graphql and refreshed with
every release. Point a GraphQL IDE, a code generator or an editor plugin at it.
Introspection (__schema, __type) is answered to authenticated requests, so a client that
introspects the live endpoint with a token gets the schema the deployed console actually serves.
Anonymous requests are not introspected.
Names follow the console's own vocabulary. A resource's identity is its metadata.name inside a
projectName on a clusterName; most list fields take the project and cluster as arguments, and
projects tells you both. Status is the derived phase { phase reason }, one of Healthy,
Queued, Applying, ActionNeeded, Degraded, Failed, Deleting, Unknown, with a plain
reason when it is not healthy.
Errors
A request that fails validation or a permission check returns HTTP 200 with an errors array,
as GraphQL does; the message is written to be read. Two things arrive as HTTP status codes
instead, because they happen before the query runs: an invalid or revoked token is a 401, and
a token used with an X-Org-Id for a different organization is a 403. Rate limiting is a
429.
Some errors carry a machine-readable extensions.code; EMAIL_NOT_VERIFIED is the one a script
can meet, on an account that has never confirmed its address.
Limits
Requests are rate limited per token and per client address. Lists that can grow take
pagination: { page, limit } and cap limit. The notification delivery log keeps 30 days; the
audit log keeps 90 days, and a year for privileged actions. A single query is limited in
complexity, so a deeply nested document is refused rather than run.
Stability
The published schema is a contract:
- Additions can happen in any release: new fields, arguments, types and enum values. Write clients that ignore what they do not know.
- Nothing is removed without notice. A field or argument that is going away is marked
@deprecated(reason: …)first and stays for at least 90 days after the release that deprecated it. Generators and IDEs surface the deprecation; the reason names the replacement. - A change that would break an existing document is refused by our own release process unless it is a decided removal after its notice period. The check compares the schema a release would serve against the one published here.
- Nullability only ever moves in the safe direction: a result may become non-null, an input may become optional.
The one thing a client must not assume is that a value it never saw cannot appear: enums and
phases grow, and a switch without a default is the usual way a client breaks on a release that
broke nothing.