# Errors

Every error is a problem document with a stable code. Branch on the code, show the detail, and retry only what is worth retrying.

Source: https://docs.pantechdynamics.com/api/errors

## The shape of an error

Every error is `application/problem+json` (RFC 9457):

```json
{
  "type": "https://api.pantechdynamics.com/problems/insufficient-credit",
  "title": "Insufficient credit",
  "status": 402,
  "code": "INSUFFICIENT_CREDIT",
  "detail": "This instance needs NGN 15,040.00 of credit upfront and NGN 2,000.00 is available. Top up your credit or add a card, then try again.",
  "request_id": "req_6Yq8mB2x0dKpL4vS"
}
```

- `code` is the stable, machine-readable reason. **Branch on it.** It never changes within v1.
- `detail` explains this occurrence for a person. Show it, log it, but do not parse it, because its wording can change.
- `request_id` identifies the request. Quote it to support.

`request_id` is also in every response's `X-Request-Id` header, errors or not, so log it with each call you make.

A validation error lists every bad field in `errors`, so you can fix them all at once:

```json
{
  "status": 422,
  "code": "VALIDATION_FAILED",
  "title": "Validation failed",
  "detail": "plan_slug: must identify an active plan.",
  "request_id": "req_6Yq8mB2x0dKpL4vS",
  "errors": [
    { "field": "plan_slug", "code": "PLAN_NOT_FOUND", "message": "must identify an active plan" }
  ]
}
```

Its `detail` names each bad field and what it must be (`field: message`, separated by `;`), so it is useful on its own. Show each entry of `errors` beside its field, and branch on its `code`.

## By status

| Status | Means | Retry? |
| --- | --- | --- |
| `400` | The request could not be read | No, fix it |
| `401` | No valid API key | No, fix the key |
| `402` | Not enough credit to pay | After topping up |
| `403` | The key may not do this | No |
| `404` | Nothing with that id in your organization | No |
| `409` | Not possible in the resource's current state | Sometimes; see the code |
| `413` | Body over 64 KiB | No |
| `422` | A field is invalid; see `errors` | No, fix it |
| `429` | Rate limited | Yes, after `Retry-After` |
| `500`, `503` | Our side failed | Yes, with backoff and the same `Idempotency-Key` |

## Request and authentication codes

| `code` | Status | What to do |
| --- | --- | --- |
| `INVALID_REQUEST_BODY` | 400 | Send valid JSON |
| `IDEMPOTENCY_KEY_REQUIRED` | 400 | Add an `Idempotency-Key` header |
| `INVALID_IDEMPOTENCY_KEY` | 400 | Keep the key to 255 characters |
| `INVALID_FILTER` | 400 | Use a listed filter value |
| `INVALID_CURSOR` | 400 | Start the list again without a cursor |
| `INVALID_PAGE` | 400 | Send a `limit` from 1 to 100, or leave it out for 50 |
| `INVALID_CALLER_ADDRESS` | 400 | Retry from a normal internet connection |
| `UNAUTHENTICATED` | 401 | Send a valid, unexpired key, and check the URL starts with `/public/v1` |
| `INSUFFICIENT_SCOPE` | 403 | Use a key with `write` scope |
| `FORBIDDEN` | 403 | The key's creator lacks the role for this |
| `RESOURCE_NOT_FOUND` | 404 | Check the id and the organization |
| `NOT_FOUND` | 404 | The feature is not offered yet |
| `ROUTE_NOT_FOUND` | 404 | Check the method and path |
| `REQUEST_TOO_LARGE` | 413 | Send a smaller body |
| `VALIDATION_FAILED` | 422 | Fix each field in `errors` |
| `RATE_LIMITED` | 429 | Wait `Retry-After` seconds |
| `INTERNAL` | 500 | Retry with backoff; quote `request_id` if it persists |

## Account and billing codes

| `code` | Status | What to do |
| --- | --- | --- |
| `INSUFFICIENT_CREDIT` | 402 | Top up credit or add a card in the console |
| `ACCOUNT_NOT_ACTIVE` | 409 | Finish activating billing in the console |
| `ACCOUNT_SUSPENDED` | 409 | Pay the overdue invoice; nothing new can be created until then |
| `PROVIDER_ACCOUNT_NOT_READY` | 409 | Your account is still being set up; retry in a minute |
| `STATIC_NAT_LIMIT_EXCEEDED` | 403 | Your organization has as many one-VM public IPs as it may. Release one, or contact support for a higher limit |
| `PUBLIC_IP_LIMIT_EXCEEDED` | 403 | Your organization has as many public IPs, of any purpose, as it may (20 by default). Release one, or contact support for a higher limit |
| `CREDIT_PURCHASE_UNAVAILABLE` | 409 | Finish billing setup in the console |
| `EMAIL_VERIFICATION_REQUIRED` | 409 | The key's creator must verify their email first |
| `AMOUNT_OUT_OF_RANGE` | 422 | Buy an amount within the per-purchase limits |
| `BILLING_UNAVAILABLE` | 503 | Retry in a few seconds |

## Resource state codes

These mean the request was fine but the resource cannot do it now.

| `code` | Status | What to do |
| --- | --- | --- |
| `INVALID_RESOURCE_STATE` | 409 | Wait for the resource to settle, then retry |
| `CONCURRENT_UPDATE` | 409 | Retry the same request with the same key |
| `IDEMPOTENCY_CONFLICT` | 409 | Use a new `Idempotency-Key` for a different request |
| `INSTANCE_NAME_TAKEN` | 409 | Choose another name |
| `INSTANCE_MUST_BE_STOPPED` | 409 | Stop the instance first |
| `INSTANCE_HAS_PUBLIC_IP` | 409 | A public IP still points at it. [Detach](https://docs.pantechdynamics.com/networking/ip-addresses#move-an-address-to-another-vm) the address to keep it, or release it, first |
| `INSTANCE_HAS_PORT_FORWARDS` | 409 | Delete its port forwards first |
| `INSTANCE_ALREADY_HAS_PUBLIC_IP` | 409 | It has a static NAT address already; detach or release that one first |
| `CONSOLE_INSTANCE_NOT_RUNNING` | 409 | Start the instance first |
| `SSH_ACCESS_INSTANCE_NOT_RUNNING` | 409 | Start the instance first |
| `SSH_ACCESS_SECURITY_GROUP_SHARED` | 409 | Give the instance its own security group |
| `SSH_ACCESS_NOT_SUPPORTED_IN_ZONE` | 409 | Not available for VPC instances yet |
| `SECURITY_GROUPS_NOT_SUPPORTED_IN_ZONE` | 409 | VPC instances use firewall rules |
| `DEFAULT_SECURITY_GROUP_UNDELETABLE` | 409 | Edit its rules instead |
| `FIREWALL_RULES_NOT_SUPPORTED` | 409 | Firewall rules are for VPC subnets |
| `NETWORK_NAME_TAKEN` | 409 | Choose another name |
| `NETWORK_NOT_READY` | 409 | Wait for the VPC to be active |
| `NETWORK_HAS_INSTANCES` | 409 | Delete its instances first |
| `NETWORK_HAS_PUBLIC_IPS` | 409 | Release its public IPs first |
| `SUBNET_HAS_KUBERNETES_CLUSTERS` | 409 | Delete the [Kubernetes clusters](https://docs.pantechdynamics.com/kubernetes/overview) in the subnet first |
| `PUBLIC_IP_NOT_FOR_PORT_FORWARDING` | 409 | Use a `port_forwarding` address |
| `PUBLIC_IP_NOT_STATIC_NAT` | 409 | Only a `static_nat` address can be attached or detached; change a port forwarding address's rules, or a load balancer's targets, instead |
| `PUBLIC_IP_HAS_PORT_FORWARDS` | 409 | Delete its rules first |
| `PUBLIC_IP_HAS_LOAD_BALANCERS` | 409 | Delete the address's [load balancers](https://docs.pantechdynamics.com/networking/load-balancers) first |
| `INSTANCE_BEHIND_LOAD_BALANCER` | 409 | Remove the instance from its load balancers' targets first |
| `PORTS_IN_USE` | 409 | Another rule forwards these ports |
| `SNAPSHOT_NAME_TAKEN` | 409 | Choose another name |
| `SECURITY_GROUP_NAME_TAKEN` | 409 | Choose another name |
| `SSH_KEY_NAME_TAKEN` | 409 | Choose another name |
| `SSH_KEY_ALREADY_EXISTS` | 409 | The key is already registered |
| `SNAPSHOT_SCHEDULE_NOT_FOUND` | 404 | No schedule is set; `PUT` one |
| `PROVISIONING_UNAVAILABLE` | 503 | Retry with backoff |
| `CONSOLE_UNAVAILABLE` | 503 | Retry shortly |
| `OPERATION_STREAM_UNAVAILABLE`, `OPERATION_STREAM_TIMEOUT` | event | The operation's event stream ended; reconnect, or poll the operation |
| `SECURITY_GROUP_ATTACHED_TO_DATABASE` | 409 | Detach the group from the database named in `detail`, then delete it |
| `INSTANCE_NOT_SETTLED` | 409 | Wait until the instance is running or stopped with nothing else in progress |

## Database codes

The database endpoints have codes of their own, with a page that explains each one: [database error codes](https://docs.pantechdynamics.com/databases/errors). In short:

| `code` | Status | What to do |
| --- | --- | --- |
| `DATABASE_NOT_FOUND`, `DATABASE_ORDER_NOT_FOUND`, `DATABASE_SNAPSHOT_NOT_FOUND` | 404 | Check the ids; a snapshot is found only under its own database |
| `DATABASE_NAME_TAKEN`, `DATABASE_SNAPSHOT_NAME_TAKEN` | 409 | Choose another name |
| `DATABASE_NOT_RUNNING` | 409 | Start the database first (password changes and storage resizes need it running) |
| `DATABASE_NOT_STOPPED` | 409 | Stop the database first |
| `DATABASE_SNAPSHOT_NOT_READY` | 409 | Wait until the database is running or stopped |
| `DATABASE_STORAGE_RESIZE_IN_PROGRESS` | 409 | Wait for the resize in flight to finish |
| `DATABASE_ACCESS_RANGE_LIMIT_EXCEEDED` | 422 | Keep rules plus security-group ranges to 1,024 |
| `DATABASE_HOSTNAME_UNAVAILABLE` | 503 | Retry with a new `Idempotency-Key` |
| `DATABASE_STORAGE_UNAVAILABLE` | 503 | Storage is not offered in that zone yet; try later |
| `DATABASES_UNAVAILABLE` | 503 | Databases are unavailable right now; try later |

## Kubernetes codes

The [managed Kubernetes](https://docs.pantechdynamics.com/kubernetes/overview) endpoints have codes of their own; the guide's [troubleshooting](https://docs.pantechdynamics.com/kubernetes/overview#troubleshooting) explains each, field codes included. In short:

| `code` | Status | What to do |
| --- | --- | --- |
| `KUBERNETES_CLUSTER_NOT_FOUND` | 404 | Check the cluster id (`k8s_…`). A deleted cluster answers this at once, although its nodes are destroyed 12 hours later |
| `KUBERNETES_CLUSTER_NAME_TAKEN` | 409 | Choose another name |
| `KUBERNETES_CLUSTER_LIMIT` | 409 | Delete a cluster, or contact support to raise the limit |
| `KUBERNETES_CLUSTER_NOT_RUNNING` | 409 | Start the cluster first; scaling, autoscaling, API server access and upgrades need it running |
| `KUBERNETES_CLUSTER_BUSY` | 409 | Wait until the cluster is running or stopped |
| `KUBERNETES_CLUSTER_NOT_CHANGEABLE` | 409 | It is stopped, being deleted, or failed |
| `KUBECONFIG_UNAVAILABLE` | 409 | Wait until the cluster is running |
| `KUBERNETES_UNAVAILABLE` | 503 | Managed Kubernetes is not open on the platform yet |

## Private network codes

From [adding a private database network interface](https://docs.pantechdynamics.com/databases/standard-vps) to a standard instance:

| `code` | Status | What to do |
| --- | --- | --- |
| `SECURITY_GROUP_ALLOWS_PRIVATE_NETWORK` | 409 | Narrow the rules named in `detail` so none admits `10.250.0.0/20`, ICMP included |
| `PRIVATE_NETWORK_NOT_AVAILABLE` | 409 | Only standard instances in `af-abj-1` can have one |
| `PRIVATE_NETWORK_ALREADY_ATTACHED` | 409 | It already has one; read `private_network_ip` |
| `PRIVATE_NETWORK_NOT_ATTACHED` | 409 | It has none to remove |
| `PRIVATE_NETWORK_CHANGE_IN_PROGRESS` | 409 | Follow the attach or detach already running |

## Field codes

These appear inside `errors` on a `422`, each with the `field` it is about.

| `code` | Typical field | Means |
| --- | --- | --- |
| `REQUIRED` | any | The field is missing |
| `UNKNOWN_FIELD` | any | Not a field of this request |
| `PLAN_NOT_FOUND` | `plan_slug` | Not an active plan |
| `PLAN_NOT_BIGGER` | `plan_slug` | Resize needs a bigger plan |
| `IMAGE_NOT_FOUND` | `image_slug` | Not an active image |
| `IMAGE_NOT_AVAILABLE_IN_ZONE` | `image_slug` | Not offered where this instance runs |
| `ENCRYPTION_NOT_SUPPORTED` | `encrypted` | `encrypted` is `true`, but this image has no encrypted version available to your organization |
| `ENCRYPTION_CONFLICT` | `encrypted` | `encrypted` is `false`, but `image_slug` is already an encrypted image |
| `SSH_KEY_NOT_FOUND` | `ssh_key_id` | Not a key on this account |
| `REGION_NOT_AVAILABLE` | `region` | Not available for this resource |
| `SUBNET_REQUIRED` | `subnet_id` | Needed when `network_id` is set |
| `SUBNET_NOT_FOUND` | `subnet_id` | Not a subnet on this account |
| `SUBNET_NOT_IN_NETWORK` | `subnet_id` | Not in `network_id` |
| `SUBNET_NOT_USABLE` | `subnet_id` | Being deleted or failed |
| `SECURITY_GROUP_NOT_FOUND` | `security_group_id` | Not a group on this account |
| `CONFIRM_NAME_MISMATCH` | `confirm_name` | Not the instance's current name |
| `DISK_OFFERING_NOT_FOUND` | `disk_offering_slug` | Not an active offering |
| `SIZE_REQUIRED` | `size_gb` | Needed for a custom-size offering |
| `INVALID_SIZE` | `size_gb` | Must be a whole number of GB |
| `INSTANCE_NOT_FOUND` | `instance_id` | Not an instance on this account |
| `INSTANCE_NOT_RUNNING` | `instance_id` | Must be running |
| `INSTANCE_WRONG_ZONE` | `instance_id` | Must be in the volume's zone |
| `INSTANCE_NOT_IN_NETWORK` | `instance_id` | Must be in the address's VPC |
| `NOT_ALLOWED` | `instance_id` | Not taken here |
| `INVALID_MOUNT_POINT` | `mount_point` | Must be an absolute path |
| `INVALID_RULE_NUMBER` | `number` | Must be 100–9999 |
| `RULE_NUMBER_TAKEN` | `number` | Used by another rule |
| `INVALID_PORTS` | `port_start` | Ports invalid for the protocol |
| `INVALID_FIREWALL_RULE` | `protocol` | Direction, protocol or action invalid |
| `PURPOSE_NOT_AVAILABLE` | `purpose` | Not available yet |
| `INVALID_SSH_PUBLIC_KEY` | `public_key` | Not an OpenSSH public key |
| `INVALID_NAME`, `NAME_TOO_LONG` | `name` | 1–255 characters |
| `INVALID_METRIC_INTERVAL` | `interval` | One of `1h`, `24h`, `7d`, `30d` |
| `INVALID_PERIOD` | `period` | One of `30d`, `7d`, `month`, `last_month` |
| `INVALID_PLACEMENT` | `placement` | `standard` or `vpc` |
| `UNSUPPORTED_CURRENCY` | `currency` | `NGN` or `USD` |
| `INVALID_MACHINE` | `machines` | A line in the estimate is invalid |
| `TOO_MANY_MACHINES` | `machines` | At most 50 lines |
| `INVALID_DATABASE_NAME` | `name` | 1–255 characters |
| `INVALID_DATABASE_ENGINE` | `engine` | `postgresql`, `mysql` or `mariadb` |
| `INVALID_DATABASE_VERSION` | `version` | Not an offered version line |
| `INVALID_DATABASE_ZONE` | `zone_id` | Not a zone you can use |
| `DATABASE_VERSION_NOT_AVAILABLE_IN_ZONE` | `zone_id` | That version is not offered in that zone |
| `DATABASE_SUBNET_REQUIRED`, `DATABASE_SUBNET_NOT_ALLOWED` | `subnet_id` | Required in `af-abj-2`, refused in `af-abj-1` |
| `DATABASE_SUBNET_NOT_FOUND`, `DATABASE_SUBNET_WRONG_ZONE`, `DATABASE_SUBNET_NOT_USABLE` | `subnet_id` | Not a usable subnet of yours in `zone_id` |
| `INVALID_DATABASE_ADMIN_USERNAME` | `admin_username` | 3–32 lowercase letters, digits, underscores; not reserved |
| `INVALID_DATABASE_PASSWORD` | `password` | 16–128 printable characters, no spaces, quotes or backslashes |
| `INVALID_DATABASE_STORAGE` | `storage_gb` | Plan's size to 2,000 GB, in 10 GB steps; a resize must grow |
| `INVALID_DATABASE_SNAPSHOT_NAME` | `name` | 1–255 characters |
| `INVALID_ACCESS_RULE` | `access_rules` | An IPv4 CIDR no wider than `/8`, never `0.0.0.0/0` |
| `TOO_MANY_ACCESS_RULES` | `access_rules` | At most 50 |
| `DUPLICATE_ACCESS_RULE` | `access_rules` | Each CIDR once |
| `INVALID_DATABASE_SECURITY_GROUPS` | `security_group_ids` | At most 5, each once |
| `DATABASE_SECURITY_GROUP_NOT_FOUND`, `DATABASE_SECURITY_GROUP_WRONG_ZONE`, `DATABASE_SECURITY_GROUP_NOT_USABLE` | `security_group_ids` | Your own usable groups in the database's zone |
| `RULE_ALLOWS_PRIVATE_NETWORK` | `rules` | An instance on this group has a private network interface; leave `10.250.0.0/20` out |
| `SECURITY_GROUP_ALLOWS_PRIVATE_NETWORK` | `security_group_id` | The instance has a private network interface; pick a group that admits nothing from `10.250.0.0/20` |
| `DATABASE_ACCESS_RANGE_LIMIT_EXCEEDED` | `rules` | An attached database would get more than 1,024 ranges |
| `INVALID_KUBERNETES_WORKER_COUNT` | `workers` | 1 to 10 |
| `INVALID_KUBERNETES_AUTOSCALING` | `autoscaling` | `min_workers` and `max_workers` 1 to 10, minimum no more than maximum, `workers` within them; no `workers` while autoscaling is on |
| `INVALID_API_ALLOWED_CIDRS` | `api_allowed_cidrs` | At most 20 IPv4 ranges, like `203.0.113.0/24` |
| `API_ALLOWED_CIDRS_VPC_ONLY` | `api_allowed_cidrs` | Only clusters in a VPC zone can limit API server access |

## When an operation fails

An accepted change (`202`) can still fail later. Its [operation](https://docs.pantechdynamics.com/api/operations) then ends `failed`, and its `failure` says why:

```json
{
  "id": "op_…",
  "kind": "attach_private_network",
  "status": "failed",
  "failure": {
    "code": "SECURITY_GROUP_ALLOWS_PRIVATE_NETWORK",
    "reason": "The instance's security group was changed to allow 10.250.0.0/20 before the interface was added. Narrow it, then try again."
  }
}
```

Branch on `failure.code`, such as `DATABASE_STORAGE_GROW_FAILED` or `INSTANCE_PRIVATE_NETWORK_ATTACH_FAILED`; show `failure.reason`, whose wording can change. The resource is left as it was before. These are listed with the guide for each feature, for example the [database error codes](https://docs.pantechdynamics.com/databases/errors). Retry a failed operation with a **new** `Idempotency-Key`: the old one replays the failure.

A change the platform itself could not carry out (creating, starting or resizing a machine, for example) fails with one of these codes, whatever the resource:

| `failure.code` | Means | What to do |
| --- | --- | --- |
| `PROVISIONING_CAPACITY_UNAVAILABLE` | No capacity for it in the zone right now | Try a different plan or zone, or try again later |
| `PROVISIONING_LIMIT_EXCEEDED` | Your account reached its limit for this kind of resource. A Kubernetes cluster or scale counts every node it could have, up to its autoscaling maximum, and fails before anything is created | Delete what you no longer use, ask for less, or contact support to raise the limit |
| `PROVISIONING_ADDRESSES_EXHAUSTED` | No free addresses in the zone right now | Try again later, or choose another zone |
| `PROVISIONING_IMAGE_UNAVAILABLE` | The image is not available in this zone right now | Choose another image or zone, or try again later |
| `PROVISIONING_RESOURCE_BUSY` | The resource was busy with another change | Wait for it to settle, then retry |
| `PROVISIONING_RESOURCE_CONFLICT` | A resource with these settings already exists | Use a different name or settings |
| `PROVISIONING_RESOURCE_NOT_FOUND` | The resource no longer exists on the platform | Refresh, then retry |
| `PROVISIONING_INVALID_REQUEST` | A setting is not valid here | Check the request, then retry |
| `PROVISIONING_UNAVAILABLE` | The platform is temporarily unavailable | Retry in a few minutes |
| `PROVISIONING_INTERNAL_ERROR` | Something failed on our side; we have been alerted | Retrying will not help; contact support with the operation id |
| `PROVISIONING_FAILED` | The change could not be completed | Retry; contact support with the operation id if it keeps failing |

New codes may be added within v1. Treat an unknown code by its status.
