Skip to content

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.