# Pantech Dynamics public API: agent brief

You can manage Pantech Dynamics cloud infrastructure (virtual machines, VPC networks, firewalls, public IPs, volumes, snapshots, managed databases and SSH keys) through its public REST API. This file is the whole contract in brief. Read it before your first call.

Full reference: https://docs.pantechdynamics.com/api
OpenAPI 3.1 contract: https://docs.pantechdynamics.com/openapi/public-api.yaml
Postman collection: https://docs.pantechdynamics.com/postman/pantech-public-api.postman_collection.json (also for Insomnia: https://docs.pantechdynamics.com/insomnia/pantech-public-api.insomnia.json, and Bruno: https://docs.pantechdynamics.com/bruno/pantech-public-api.bruno.zip)
Every page as one file: https://docs.pantechdynamics.com/llms-full.txt

If your client speaks MCP, connect it to the hosted MCP server instead of calling the API by hand: `https://mcp.pantechdynamics.com/mcp` (OAuth sign-in; guide: https://docs.pantechdynamics.com/ai/connect-your-ai). Its tools that cost money, open access from the internet, or delete or overwrite are two-step: the first call returns a preview and a `confirmation_token`; show the preview to the human and call again with the same arguments and the token only if they agree. If you are writing code, use an SDK: Go `github.com/Pantech-Dynamics/pantech-go`, Python `pantech` (PyPI), JavaScript `@pantechdynamics/sdk` (npm). They add Idempotency-Key, retry safely, follow pages and wait for operations (https://docs.pantechdynamics.com/sdks).

## Connect

- Base URL: `https://api.pantechdynamics.com/public/v1`. Every path below is relative to it. API keys work only here. Never call any other path on the host: keys are not accepted there. If you get `401 UNAUTHENTICATED`, check the key and that the URL starts with `/public/v1`.
- Authenticate every request: `Authorization: Bearer $PANTECH_API_KEY`. Keys start with `PAN_`.
- Call `GET /me` first. `api_key.scopes` says whether you may write: a `read` key can only call GET endpoints, plus `DELETE /me` and `POST /pricing/estimate`.
- `api_key.kind` is `standard` or `ai_app` (a key an AI app got when a member connected it; `client_name` is the app's name). An `ai_app` key needs the `delete` scope as well as `write` for a destructive call: every DELETE except `DELETE /me`, rebuilding or restoring an instance, and lowering a snapshot schedule's `retention_count`. Without it the call returns `403 INSUFFICIENT_SCOPE`: tell the human to do it in the console or reconnect the app with "Manage and delete" access. `403 AI_APPS_DISABLED` means an owner turned AI app access off: stop and tell the human.
- When you are done with a key you were given for one job, `DELETE /me` revokes it (`204`; any scope, no `Idempotency-Key`).
- Send JSON with `Content-Type: application/json`. A field that is not documented is rejected with `422 VALIDATION_FAILED` (its entry in `errors` has code `UNKNOWN_FIELD`), so never add fields that are not documented.

## Rules

1. Every POST, PUT, DELETE and PATCH that needs the `write` scope needs an `Idempotency-Key` header (`DELETE /me` and `POST /pricing/estimate` take none). Make a new UUID for each new change. If a request fails or times out, retry it with the SAME key, so the change cannot happen twice. These do not replay a retry yet; each one's entry under Endpoints says what to do instead: `POST /instances/{instance_id}/console`, `POST /instances/{instance_id}/volume-key-credential`, `POST /ssh-keys`, `DELETE /ssh-keys/{ssh_key_id}`, `POST /webhooks`, `PATCH /webhooks/{webhook_id}`, `DELETE /webhooks/{webhook_id}`, `POST /webhooks/{webhook_id}/test`, `POST /webhooks/{webhook_id}/rotate-secret` and `POST /webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver`.
2. Writes are asynchronous. They return `202` with an `operation_id`. Poll `GET /operations/{operation_id}` every 3 to 5 seconds until `status` is `succeeded` or `failed` before you rely on the result.
3. `POST /instances`, `POST /databases` and `GET /database-orders/{order_id}` return an `order_id` instead. Poll `GET /instance-orders/{order_id}` (for an instance) or `GET /database-orders/{order_id}` (for a database) until `status` is `provisioned` (or `failed`, `payment_failed`), then read the instance or database.
4. Never guess an id or a slug. Read plans, images and disk offerings from the catalogue, and ids from list endpoints.
5. Lists return `{"data": [...], "next_cursor": ...}`. Follow `next_cursor` (pass it as `cursor`) until it is null before deciding something does not exist.
6. Errors are JSON problem documents with a stable `code`. Act on `code`, not on the wording. A `422` lists every bad field in `errors`.
7. On `429`, wait `Retry-After` seconds. On `500` or `503`, retry with backoff and the same `Idempotency-Key`. On any other 4xx, fix the request; do not repeat it unchanged.
8. Money is an integer in minor units with a currency: `amount_minor: 150000` in NGN is NGN 1,500.00.
9. Ask a human before anything destructive or costly: deleting an instance, database, network, volume or snapshot; rebuild or restore (they erase data); resize; creating more than one instance; buying credit; opening the console. Say what it costs first: `POST /pricing/estimate` prices it without creating anything.

## Common jobs

- **A server with a public IP:** `GET /plans` and `GET /images` for slugs, `GET /ssh-keys` (or `POST /ssh-keys`), then `POST /instances` with `name`, `plan_slug`, `image_slug`, `ssh_key_id`. Poll the order, then `GET /instances/{instance_id}` for `public_ipv4`.
- **A server in a private network:** `POST /networks`, wait; `POST /networks/{network_id}/subnets`, wait; `POST /subnets/{subnet_id}/firewall-rules` to let traffic in, wait; `POST /instances` with `subnet_id`; `POST /public-ips` with `network_id` and `instance_id`, wait.
- **Back up before a risky change:** `POST /instances/{instance_id}/snapshots`, wait. To roll back: `POST /instances/{instance_id}/restore` with `snapshot_id` and `confirm_name` (the instance's name).
- **A managed database:** `GET /database-engines` for `engine` and `version`, `GET /plans` for `plan_slug`, then `POST /databases` with `name`, `engine`, `version`, `plan_slug`, `zone_id` (`af-abj-2`) and `subnet_id`. If you did not send `password`, the response's `password` is the only copy that will ever exist: hand it to the human or their secret store at once, and never log it. Poll the order, then `GET /databases/{database_id}` for `hostname` and `port`. A database has no public address: only machines on its network and inside its `access_rules` can connect.
- **What costs the most:** `GET /usage/by-resource?period=month` and sort by `cost_minor`.
- **Will the credit cover it:** `GET /billing/credit`. If `standing` is `low` or `empty`, tell a human before creating anything that costs money. A new instance needs enough credit or a card on file for its first payment, or `POST /instances` returns `402 INSUFFICIENT_CREDIT`.
- **Add credit:** ask a human how much first. `POST /billing/credit/purchases` with `amount_minor` returns a `checkout_url`; give that link to the human to pay. Poll `GET /billing/credit/purchases/{purchase_id}` until `status` is `settled`. Never try to pay it yourself.
- **Be told when something happens:** `GET /webhook-event-types`, then `POST /webhooks` with an `https` `url` and `events` (exact types, `<resource>.*` or `*`). The response's `secret` (`whsec_…`) is shown only once: hand it to the human or their secret store at once, and never log it. Every delivery's `Pantech-Signature` (`t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">`) must be verified on the raw body; deduplicate on the event `id`. Guide: https://docs.pantechdynamics.com/webhooks/verify-signatures.
- **Tear down:** delete in reverse order of creation: public IPs, instances, subnets, then the network. Wait for each operation.

## Endpoints

All 157 operations, grouped as in the reference. `(read)` works with any key; `(write)` needs a write key and an `Idempotency-Key`.

### Account

- `GET /me` (read): Describe this API key. Returns 200.
- `DELETE /me` (read): Revoke this API key. Returns 204, no body.

### Catalogue

- `GET /regions` (read): List regions. Returns 200.
- `GET /plans` (read): List plans. Query: placement. Returns 200.
- `GET /images` (read): List images. Returns 200.
- `GET /disk-offerings` (read): List disk offerings. Returns 200.
- `GET /disk-offerings/{slug}/quote` (read): Price a volume. Query: size_gb. Returns 200.
- `POST /pricing/estimate` (read): Estimate a monthly bill. Body: currency (required), machines, extra_storage_gb, network_count, snapshot_gb. Returns 200.

### Instances

- `GET /instances` (read): List instances. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /instances` (write): Create an instance. Body: name (required), plan_slug (required), image_slug (required), ssh_key_id (required), encrypted, region, subnet_id, network_id, security_group_id, tags. Returns 202 with an order to poll.
- `GET /instances/{instance_id}` (read): Get an instance. Returns 200.
- `POST /instances/{instance_id}/start` (write): Start an instance. Returns 202 with an operation to poll.
- `POST /instances/{instance_id}/stop` (write): Stop an instance. Returns 202 with an operation to poll.
- `POST /instances/{instance_id}/reboot` (write): Reboot an instance. Returns 202 with an operation to poll.
- `POST /instances/{instance_id}/rename` (write): Rename an instance. Body: name (required). Returns 202 with an operation to poll.
- `POST /instances/{instance_id}/resize` (write): Resize an instance. Body: plan_slug (required). Returns 202 with an operation to poll.
- `POST /instances/{instance_id}/rebuild` (write): Rebuild an instance. Body: image_slug. Returns 202 with an operation to poll.
- `POST /instances/{instance_id}/restore` (write): Restore an instance from a snapshot. Body: snapshot_id (required), confirm_name (required). Returns 202 with an operation to poll.
- `POST /instances/{instance_id}/change-ssh-key` (write): Change an instance's SSH key. Body: ssh_key_id (required). Returns 202 with an operation to poll.
- `PUT /instances/{instance_id}/security-group` (write): Change an instance's security group. Body: security_group_id (required). Returns 202 with an operation to poll.
- `POST /instances/{instance_id}/console` (write): Open the browser console. Returns 201. Idempotency: each request opens a new console URL; a retry is not replayed. Opening one changes nothing else, so retrying is safe.
- `POST /instances/{instance_id}/volume-key-credential` (write): Issue a volume key credential. Returns 201. Idempotency: each request issues a new credential; a retry is not replayed.
- `POST /instances/{instance_id}/ssh-access` (write): Grant temporary SSH access. Returns 202 with an operation to poll.
- `GET /instances/{instance_id}/ssh-access/{grant_id}` (read): Get an SSH access grant. Returns 200.
- `DELETE /instances/{instance_id}/ssh-access/{grant_id}` (write): Revoke SSH access. Returns 202 with an operation to poll.
- `DELETE /instances/{instance_id}` (write): Delete an instance. Returns 202 with an operation to poll.
- `GET /instances/{instance_id}/metrics` (read): Get instance metrics. Query: interval. Returns 200.
- `POST /instances/{instance_id}/private-network` (write): Add a private database network interface to an instance. Returns 202 with an operation to poll.
- `DELETE /instances/{instance_id}/private-network` (write): Remove an instance's private database network interface. Returns 202 with an operation to poll.

### Instance orders

- `GET /instance-orders` (read): List instance orders. Query: limit, cursor. Returns 200.
- `GET /instance-orders/{order_id}` (read): Get an instance order. Returns 200.

### VPC networks

- `GET /networks` (read): List VPC networks. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /networks` (write): Create a VPC network. Body: name (required), cidr (required), region. Returns 202 with an operation to poll.
- `GET /networks/quote` (read): Price a VPC network. Returns 200.
- `GET /networks/{network_id}` (read): Get a VPC network. Returns 200.
- `DELETE /networks/{network_id}` (write): Delete a VPC network. Returns 202 with an operation to poll.
- `GET /networks/{network_id}/subnets` (read): List subnets. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /networks/{network_id}/subnets` (write): Create a subnet. Body: name (required), cidr (required). Returns 202 with an operation to poll.
- `GET /subnets/{subnet_id}` (read): Get a subnet. Returns 200.
- `DELETE /subnets/{subnet_id}` (write): Delete a subnet. Returns 202 with an operation to poll.
- `GET /subnets/{subnet_id}/firewall-rules` (read): List a subnet's firewall rules. Returns 200.
- `POST /subnets/{subnet_id}/firewall-rules` (write): Add a firewall rule. Body: number (required), protocol (required), cidr (required), direction, port_start, port_end, icmp_type, icmp_code, action. Returns 202 with an operation to poll.
- `GET /subnets/{subnet_id}/firewall-rules/{rule_id}` (read): Get a firewall rule. Returns 200.
- `DELETE /firewall-rules/{rule_id}` (write): Delete a firewall rule. Returns 202 with an operation to poll.

### Public IPs

- `GET /public-ips` (read): List public IPs. Query: network_id. Returns 200.
- `POST /public-ips` (write): Allocate a public IP. Body: network_id (required), purpose, instance_id. Returns 202 with an operation to poll.
- `GET /public-ips/quote` (read): Price a public IP. Returns 200.
- `GET /public-ips/{public_ip_id}` (read): Get a public IP. Returns 200.
- `DELETE /public-ips/{public_ip_id}` (write): Release a public IP. Returns 202 with an operation to poll.
- `POST /public-ips/{public_ip_id}/attach` (write): Attach a public IP to an instance. Body: instance_id (required). Returns 202 with an operation to poll.
- `GET /public-ips/{public_ip_id}/port-forwarding-rules` (read): List port forwarding rules. Returns 200.
- `POST /public-ips/{public_ip_id}/detach` (write): Detach a public IP from its instance. Returns 202 with an operation to poll.
- `POST /public-ips/{public_ip_id}/port-forwarding-rules` (write): Add a port forwarding rule. Body: instance_id (required), protocol (required), public_port_start (required), public_port_end, private_port_start, private_port_end. Returns 202 with an operation to poll.
- `GET /public-ips/{public_ip_id}/port-forwarding-rules/{rule_id}` (read): Get a port forwarding rule. Returns 200.
- `DELETE /port-forwarding-rules/{rule_id}` (write): Delete a port forwarding rule. Returns 202 with an operation to poll.

### Load balancers

- `GET /load-balancers` (read): List load balancers. Query: network_id, public_ip_id. Returns 200.
- `POST /load-balancers` (write): Create a load balancer. Body: name (required), public_port (required), zone_id, kind, public_ip_id, subnet_id, algorithm, private_port, cidr_list, instance_ids. Returns 202 with an operation to poll.
- `GET /load-balancers/{load_balancer_id}` (read): Get a load balancer. Returns 200.
- `PATCH /load-balancers/{load_balancer_id}` (write): Change a load balancer. Body: name, algorithm, instance_ids. Returns 202 with an operation to poll.
- `DELETE /load-balancers/{load_balancer_id}` (write): Delete a load balancer. Returns 202 with an operation to poll.

### Security groups

- `GET /security-groups` (read): List security groups. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /security-groups` (write): Create a security group. Body: name (required), rules. Returns 202 with an operation to poll.
- `GET /security-groups/{security_group_id}` (read): Get a security group. Returns 200.
- `PUT /security-groups/{security_group_id}/rules` (write): Replace a security group's rules. Body: rules (required). Returns 202 with an operation to poll.
- `DELETE /security-groups/{security_group_id}` (write): Delete a security group. Returns 202 with an operation to poll.

### Volumes

- `GET /instances/{instance_id}/volumes` (read): List an instance's volumes. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `GET /volumes` (read): List volumes. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /volumes` (write): Create a volume. Body: name (required), disk_offering_slug (required), size_gb, region, instance_id, mount_point, encrypted. Returns 202 with an operation to poll.
- `GET /volumes/{volume_id}` (read): Get a volume. Returns 200.
- `DELETE /volumes/{volume_id}` (write): Delete a volume. Returns 202 with an operation to poll.
- `POST /volumes/{volume_id}/attach` (write): Attach a volume. Body: instance_id (required). Returns 202 with an operation to poll.
- `POST /volumes/{volume_id}/detach` (write): Detach a volume. Returns 202 with an operation to poll.
- `POST /volumes/{volume_id}/resize` (write): Grow a volume. Body: disk_offering_slug (required), size_gb. Returns 202 with an operation to poll.

### Databases

- `GET /database-engines` (read): List database engines. Returns 200.
- `GET /databases` (read): List databases. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /databases` (write): Create a database. Body: name (required), engine (required), version (required), plan_slug (required), zone_id (required), subnet_id, access_rules, admin_username, password, storage_gb. Returns 202 with an order to poll.
- `GET /databases/{database_id}` (read): Get a database. Returns 200.
- `DELETE /databases/{database_id}` (write): Delete a database. Returns 202 with an operation to poll.
- `POST /databases/{database_id}/start` (write): Start a database. Returns 202 with an operation to poll.
- `POST /databases/{database_id}/stop` (write): Stop a database. Returns 202 with an operation to poll.
- `PUT /databases/{database_id}/access-rules` (write): Replace access rules. Body: rules (required). Returns 202 with an operation to poll.
- `PUT /databases/{database_id}/password` (write): Set the password. Body: password (required). Returns 202 with an operation to poll.
- `POST /databases/{database_id}/reset-password` (write): Reset the password. Returns 202 with an operation to poll.
- `PUT /databases/{database_id}/security-groups` (write): Attach security groups. Body: security_group_ids (required). Returns 202 with an operation to poll.
- `POST /databases/{database_id}/resize-storage` (write): Grow a database's storage. Body: storage_gb (required). Returns 202 with an operation to poll.
- `GET /databases/{database_id}/snapshots` (read): List a database's snapshots. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /databases/{database_id}/snapshots` (write): Snapshot a database. Body: name (required). Returns 202 with an operation to poll.
- `GET /databases/{database_id}/snapshots/{snapshot_id}` (read): Get a database's snapshot. Returns 200.
- `DELETE /databases/{database_id}/snapshots/{snapshot_id}` (write): Delete a database's snapshot. Returns 202 with an operation to poll.

### Database orders

- `GET /database-orders` (read): List database orders. Query: limit, cursor. Returns 200.
- `GET /database-orders/{order_id}` (read): Get a database order. Returns 200.

### Kubernetes

- `GET /kubernetes-versions` (read): List Kubernetes versions. Query: zone_id. Returns 200.
- `GET /kubernetes-clusters` (read): List Kubernetes clusters. Returns 200.
- `POST /kubernetes-clusters` (write): Create a Kubernetes cluster. Body: name (required), zone_id (required), kubernetes_version_id (required), node_plan (required), subnet_id, control_nodes, workers, autoscaling, api_allowed_cidrs, kind, cni_plugin, pod_cidr, service_cidr, endpoint_access. Returns 202 with an operation to poll.
- `GET /kubernetes-clusters/{cluster_id}` (read): Get a Kubernetes cluster. Returns 200.
- `PATCH /kubernetes-clusters/{cluster_id}` (write): Change a Kubernetes cluster. Body: workers, autoscaling, api_allowed_cidrs. Returns 202 with an operation to poll.
- `DELETE /kubernetes-clusters/{cluster_id}` (write): Delete a Kubernetes cluster. Returns 202 with an operation to poll.
- `POST /kubernetes-clusters/{cluster_id}/upgrade` (write): Upgrade a Kubernetes cluster. Body: kubernetes_version_id (required). Returns 202 with an operation to poll.
- `POST /kubernetes-clusters/{cluster_id}/resize-plan` (write): Resize worker node compute plan. Body: node_plan (required). Returns 202 with an operation to poll.
- `POST /kubernetes-clusters/{cluster_id}/start` (write): Start a Kubernetes cluster. Returns 202 with an operation to poll.
- `POST /kubernetes-clusters/{cluster_id}/upgrade-network` (write): Upgrade Kubernetes cluster networking. Body: cni_plugin, pod_cidr, service_cidr, api_allowed_cidrs. Returns 202 with an operation to poll.
- `POST /kubernetes-clusters/{cluster_id}/stop` (write): Stop a Kubernetes cluster. Returns 202 with an operation to poll.
- `POST /kubernetes-clusters/{cluster_id}/kubeconfig` (write): Download a kubeconfig. Returns 200.
- `GET /kubernetes-clusters/{cluster_id}/nodes` (read): List a cluster's nodes. Returns 200.
- `GET /kubernetes-clusters/{cluster_id}/workloads` (read): Get a cluster's services and pods. Returns 200.
- `POST /kubernetes-clusters/{cluster_id}/apply-manifest` (write): Apply a manifest to a cluster. Body: manifest (required), namespace. Returns 200.
- `GET /kubernetes-clusters/{cluster_id}/live-manifests` (read): Export a cluster's workloads as YAML. Returns 200.
- `POST /kubernetes-clusters/{cluster_id}/expose-service` (write): Expose a Kubernetes service. Body: service_name (required), public_port (required), namespace, node_port, protocol. Returns 200.
- `POST /kubernetes-clusters/{cluster_id}/sync-routing` (write): Sync a cluster's public routing. Returns 200.
- `GET /kubernetes-clusters/{cluster_id}/node-pools` (read): List node pools. Returns 200.
- `POST /kubernetes-clusters/{cluster_id}/node-pools` (write): Create a node pool. Body: name (required), plan_id (required), workers, autoscaling_enabled, min_workers, max_workers, labels, taints. Returns 202 with an operation to poll.
- `PATCH /kubernetes-clusters/{cluster_id}/node-pools/{pool_id}` (write): Scale a node pool. Body: workers, autoscaling_enabled, min_workers, max_workers. Returns 202 with an operation to poll.
- `DELETE /kubernetes-clusters/{cluster_id}/node-pools/{pool_id}` (write): Delete a node pool. Returns 202 with an operation to poll.
- `GET /kubernetes-clusters/{cluster_id}/addons` (read): List cluster add-ons. Returns 200.
- `POST /kubernetes-clusters/{cluster_id}/addons/{addon_name}/toggle` (write): Toggle a cluster add-on. Body: enabled (required). Returns 202 with an operation to poll.
- `POST /kubernetes-clusters/{cluster_id}/endpoint-access` (write): Configure API server endpoint access. Body: endpoint_access (required), api_allowed_cidrs. Returns 202 with an operation to poll.

### Snapshots

- `GET /instances/{instance_id}/snapshots` (read): List an instance's snapshots. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /instances/{instance_id}/snapshots` (write): Snapshot an instance. Body: name (required). Returns 202 with an operation to poll.
- `GET /instances/{instance_id}/snapshot-schedule` (read): Get an instance's snapshot schedule. Returns 200.
- `PUT /instances/{instance_id}/snapshot-schedule` (write): Set an instance's snapshot schedule. Body: frequency (required), retention_count, enabled. Returns 200.
- `GET /volumes/{volume_id}/snapshots` (read): List a volume's snapshots. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `POST /volumes/{volume_id}/snapshots` (write): Snapshot a volume. Body: name (required). Returns 202 with an operation to poll.
- `GET /volumes/{volume_id}/snapshot-schedule` (read): Get a volume's snapshot schedule. Returns 200.
- `PUT /volumes/{volume_id}/snapshot-schedule` (write): Set a volume's snapshot schedule. Body: frequency (required), retention_count, enabled. Returns 200.
- `GET /snapshots` (read): List snapshots. Query: limit, cursor, q, desired_state, observed_state. Returns 200.
- `GET /snapshots/quote` (read): Price a snapshot. Query: size_gb (required). Returns 200.
- `GET /snapshots/{snapshot_id}` (read): Get a snapshot. Returns 200.
- `DELETE /snapshots/{snapshot_id}` (write): Delete a snapshot. Returns 202 with an operation to poll.
- `POST /snapshots/{snapshot_id}/restore` (write): Restore a snapshot to a new volume. Body: name (required), disk_offering_slug (required), size_gb. Returns 202 with an operation to poll.

### SSH keys

- `GET /ssh-keys` (read): List SSH keys. Returns 200.
- `POST /ssh-keys` (write): Add or generate an SSH key. Body: name (required), public_key. Returns 201. Idempotency: a retry with the same `Idempotency-Key` is not replayed yet. If a request may have gone through, list your SSH keys before retrying: a second attempt with the same name returns `409 SSH_KEY_NAME_TAKEN`, and a generated private key is never shown again.
- `GET /ssh-keys/{ssh_key_id}` (read): Get an SSH key. Returns 200.
- `DELETE /ssh-keys/{ssh_key_id}` (write): Delete an SSH key. Returns 204, no body. Idempotency: a retry is not replayed. Once the key is deleted, a repeat returns `404`; treat that as done.

### Webhooks

- `GET /webhook-event-types` (read): List webhook event types. Returns 200.
- `GET /webhooks` (read): List webhooks. Returns 200.
- `POST /webhooks` (write): Create a webhook. Body: url (required), events (required), description. Returns 201. Idempotency: a retry with the same `Idempotency-Key` returns the webhook the first request made, with `secret: null`: the secret is never shown again. The same key with a different request returns `409 IDEMPOTENCY_CONFLICT`.
- `GET /webhooks/{webhook_id}` (read): Get a webhook. Returns 200.
- `PATCH /webhooks/{webhook_id}` (write): Change a webhook. Body: url, description, events, enabled. Returns 200. Idempotency: the same request applied twice leaves the webhook the same.
- `DELETE /webhooks/{webhook_id}` (write): Delete a webhook. Returns 204, no body. Idempotency: a retry is not replayed. Once the webhook is deleted, a repeat returns `404`; treat that as done.
- `POST /webhooks/{webhook_id}/test` (write): Send a test event. Returns 202 with an operation to poll. Idempotency: a retry is not replayed: each request sends another test event.
- `POST /webhooks/{webhook_id}/rotate-secret` (write): Rotate a webhook's secret. Returns 200. Idempotency: a retry is not replayed: each request rotates again, and only the latest secret and the one before it sign.
- `GET /webhooks/{webhook_id}/deliveries` (read): List a webhook's deliveries. Query: limit, cursor. Returns 200.
- `GET /webhooks/{webhook_id}/deliveries/{delivery_id}` (read): Get a delivery. Returns 200.
- `POST /webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver` (write): Send a delivery again. Returns 202 with an operation to poll. Idempotency: a retry while it is being sent returns `409 WEBHOOK_DELIVERY_IN_PROGRESS`; treat that as done.

### Operations

- `GET /operations/{operation_id}` (read): Get an operation. Returns 200.
- `GET /operations/{operation_id}/events` (read): Stream an operation. Returns 200, an event stream of the operation.

### Usage

- `GET /instances/{instance_id}/usage` (read): Get an instance's usage. Returns 200.
- `GET /volumes/{volume_id}/usage` (read): Get a volume's usage. Returns 200.
- `GET /snapshots/{snapshot_id}/usage` (read): Get a snapshot's usage. Returns 200.
- `GET /usage` (read): Get usage. Returns 200.
- `GET /usage/by-resource` (read): Get cost by resource. Query: period. Returns 200.

### Billing

- `GET /billing/credit` (read): Get credit. Returns 200.
- `POST /billing/credit/purchases` (write): Buy credit. Body: amount_minor (required). Returns 201.
- `GET /billing/credit/purchases/{purchase_id}` (read): Get a credit purchase. Returns 200.
- `GET /billing/receipts` (read): List receipts. Query: limit, cursor. Returns 200.
- `GET /billing/receipts/{receipt_id}` (read): Get a receipt. Returns 200.
- `GET /billing/receipts/{receipt_id}/pdf` (read): Download a receipt. Returns 200, no body.
