---
title: "Errors & request IDs"
description: "Every Metalhost API error carries a stable reason code and a request ID. Branch on the reason, and give support the request ID."
url: "https://metalhost.com/docs/developers/errors"
---

# Errors & request IDs

Every API error carries a stable **reason** code and the **request ID** of the call that failed. Branch on the reason in code. Quote the request ID when you contact support.

## What an error looks like

Errors use the Connect protocol: an HTTP status, a JSON body with a `code` and a human-readable `message`, and a `google.rpc.ErrorInfo` detail. The detail's `domain` is always `metalhost.net`, a fixed identifier, not a URL.

```
HTTP/2 401
x-request-id: 8ae81966-ffe7-4533-a5e4-ae5414427193
content-type: application/json

{
  "code": "unauthenticated",
  "message": "Authentication is required. Sign in, or send an API key as a Bearer token.",
  "details": [{
    "type": "google.rpc.ErrorInfo",
    "value": "Cg9VTkFVVEhF…",
    "debug": {
      "reason": "UNAUTHENTICATED",
      "domain": "metalhost.net",
      "metadata": { "request_id": "8ae81966-ffe7-4533-a5e4-ae5414427193" }
    }
  }]
}
```

- **`reason`**: one of the codes below. Stable across releases.
- **`metadata.request_id`**: the same value as the `X-Request-ID` response header, which is on every response, successful or not.
- **`message`**: written for people. It may change at any time, so never match on it.

> **send your own request ID**
>
> Set an `X-Request-ID` header (up to 128 characters of `A-Z a-z 0-9 . _ : -`) and the API uses it instead of generating one, so your logs and ours share an identifier. Other values are replaced with a fresh UUID.

## Reason codes

The `aes.errors.v1.ErrorReason` enum in the [API reference](https://metalhost.com/docs/developers/api.md) lists these. ErrorInfo carries them without the `ERROR_REASON_` prefix.

| Reason | Meaning |
| --- | --- |
| `INVALID_ARGUMENT` | The request is not valid. ErrorInfo metadata may name the field. |
| `UNAUTHENTICATED` | Missing, expired or revoked credentials. |
| `PERMISSION_DENIED` | The caller's role or key scope does not allow this action. |
| `RESOURCE_NOT_FOUND` | The resource does not exist, or was deleted. |
| `ALREADY_EXISTS` | A resource with that name already exists. |
| `PRECONDITION_FAILED` | Not possible in the resource's current state. |
| `CONFLICT` | The resource changed while the request ran. Re-read and retry with the new version. |
| `RESOURCE_BUSY` | Another operation is running on this resource. Retry when it finishes. |
| `RESOURCE_EXHAUSTED` | A limit was reached. Retry later. |
| `QUOTA_EXCEEDED` | A project quota limit. Request an increase. |
| `TRUST_TIER_LIMIT` | New-account limit until the first wallet top-up settles. |
| `CAPACITY_UNAVAILABLE` | Not enough capacity for this size right now. |
| `BILLING_INSUFFICIENT_BALANCE` | Add funds to the wallet. Resources are prepaid. |
| `BILLING_SUSPENDED` | The billing account is suspended. |
| `BILLING_NOT_CONFIGURED` | Billing is not set up for this project. |
| `ACCOUNT_SUSPENDED` | The account is suspended. Contact support. |
| `RATE_LIMITED` | Too many requests. Back off and retry. |
| `UNAVAILABLE` | Temporarily unavailable. Retry with backoff. |
| `DATACENTER_UNREACHABLE` | The datacenter could not be reached. Retry with backoff. |
| `DEADLINE_EXCEEDED` | The request took too long. Retry. |
| `CANCELED` | The request was canceled. |
| `NOT_IMPLEMENTED` | The action is not available yet. |
| `INTERNAL` | A fault on our side. Retry, or open a ticket with the request ID. |

Treat an unknown reason as you would its HTTP status: new codes may be added in later releases.

## Failed operations

Long-running mutations return an `operations/…` resource (see [Operations & idempotency](https://metalhost.com/docs/developers/guides/operations-idempotency.md)). When one ends `FAILED` or `CANCELLED`, its `failure_reason` holds a code from the same list, for example `CAPACITY_UNAVAILABLE`. The original request succeeded, so it has no API error; read the operation instead.

## Go SDK

SDK v1.3.0 errors are `*connect.Error` values. Read the ErrorInfo detail like this:

```
import (
    "errors"

    "connectrpc.com/connect"
    "google.golang.org/genproto/googleapis/rpc/errdetails"
)

// errorInfo returns the Metalhost reason and request ID of an API error.
func errorInfo(err error) (reason, requestID string) {
    var ce *connect.Error
    if !errors.As(err, &ce) {
        return "", ""
    }
    for _, d := range ce.Details() {
        v, derr := d.Value()
        if derr != nil {
            continue
        }
        if info, ok := v.(*errdetails.ErrorInfo); ok && info.GetDomain() == "metalhost.net" {
            return info.GetReason(), info.GetMetadata()["request_id"]
        }
    }
    return "", ""
}

_, err := client.CreateVirtualMachine(ctx, req)
switch reason, requestID := errorInfo(err); reason {
case "":
    // transport failure, or no Metalhost detail
case "QUOTA_EXCEEDED", "TRUST_TIER_LIMIT":
    // raise the limit, then retry
case "CAPACITY_UNAVAILABLE":
    // try a smaller size
default:
    log.Printf("%s (request %s): %v", reason, requestID, err)
}
```

## curl

```
curl -sS -X POST "$METALHOST_ENDPOINT/aes.compute.v1.ComputeService/GetVirtualMachine" \
  -H "Authorization: Bearer $METALHOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"projects/my-app/virtual-machines/web-01"}' |
  jq -r '.details[]? | select(.type == "google.rpc.ErrorInfo") | .debug | "\(.reason) \(.metadata.request_id)"'
```

## CLI

CLI v1.3.0 prints the reason, a hint for common reasons, and the request ID on stderr. Its [exit codes](https://github.com/AES-Services/metalhost-cli/blob/v1.3.0/docs/SCRIPTING.md) follow the error class.

```
$ metalhost vm create …
Error: You've reached a quota limit. Request an increase from the Quotas page. (QUOTA_EXCEEDED)
  A quota limit was reached — check `metalhost quota list` or request an increase with `metalhost quota request`.
  Request ID: 7f6c2a1e-… (include it if you contact support)
```

## Asking support about an error

Include the request ID in your ticket. When you open a ticket from an error in the dashboard, the error is attached to the ticket automatically. API callers can pass the same details in `CreateTicketRequest.error_reference`: the `error_request_id`, `reason`, the failed `procedure` and, for operations, the `operation_name`.

## What's next

- [Operations & idempotency](https://metalhost.com/docs/developers/guides/operations-idempotency.md)
- [Concepts](https://metalhost.com/docs/developers/concepts.md)
- [API reference](https://metalhost.com/docs/developers/api.md)
