---
title: Errors
path: concepts/errors
status: published
---

# Errors

Standard conventions for ScaiDNS error responses. This page covers the shape, the status codes, and how to interpret the most common cases. See [Error Codes](../reference/error-codes.md) for the exhaustive reference.

## Error response shape

All error responses use this shape:

```json
{
  "detail": "A human-readable message describing what went wrong"
}
```

For some structured errors (validation failures, conflicts), `detail` is an object instead of a string:

```json
{
  "detail": {
    "message": "Record conflicts with existing entries",
    "field": "content",
    "conflicts": ["a1b2c3d4-..."]
  }
}
```

Pydantic validation errors from FastAPI follow the standard pattern:

```json
{
  "detail": [
    {
      "loc": ["body", "name"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}
```

## HTTP status codes

ScaiDNS uses standard HTTP status codes.

### 2xx — Success

| Code | Meaning |
|------|---------|
| `200 OK` | GET, PATCH, POST with a response body |
| `201 Created` | POST creating a resource (sometimes — usually just `200` with body) |
| `204 No Content` | DELETE or write that returns no body |

### 4xx — Caller error

| Code | Meaning | Typical cause |
|------|---------|---------------|
| `400 Bad Request` | Malformed request or invalid input | Missing field, bad enum value, invalid record content |
| `401 Unauthorized` | Missing or invalid credentials | No `X-API-Key`/`Authorization` header; expired token; invalid signature |
| `403 Forbidden` | Authenticated but not allowed | Insufficient permissions, IP whitelist mismatch, cross-tenant attempt |
| `404 Not Found` | Resource doesn't exist (or you can't see it) | Wrong UUID, soft-deleted without `?include_deleted`, tenant scoping |
| `409 Conflict` | Write conflicts with current state | Duplicate domain name, record collision (CNAME + A), role already assigned |
| `422 Unprocessable Entity` | Schema validation failed | Pydantic validation errors; usually a typed `detail` array |
| `429 Too Many Requests` | Rate limit exceeded | API key or global rate limit hit; check `Retry-After` header |

### 5xx — Server error

| Code | Meaning | What to do |
|------|---------|------------|
| `500 Internal Server Error` | Unhandled exception | Retry with backoff; check platform health; if it persists, contact support |
| `502 Bad Gateway` | PowerDNS or ScaiKey unreachable | Usually transient; retry |
| `503 Service Unavailable` | Maintenance or overload | Retry with backoff |
| `504 Gateway Timeout` | PowerDNS or ScaiKey slow | Retry; check dashboards |

## Common patterns

### Validation failures

On `POST` and `PATCH` with invalid input, expect `422`:

```json
{
  "detail": [
    {"loc": ["body", "content"], "msg": "invalid IP address", "type": "value_error"}
  ]
}
```

Fix the input and retry.

### Tenant mismatches

Trying to read a resource from another tenant returns `404`, not `403`. This is intentional — leaking existence-by-status would let a caller enumerate resources across tenant boundaries.

### Permission failures

Trying to take an action your role doesn't permit returns `403`:

```json
{"detail": "records:write permission required on this domain"}
```

Check effective permissions: `GET /api/v1/roles/users/{your_id}/permissions?domain_id={domain}`.

### Soft delete / not-found

A soft-deleted domain returns `404` on normal reads. To restore:

```bash
curl -X POST https://scaidns.scailabs.ai/api/v1/domains/$DOMAIN_ID/restore \
  -H "X-API-Key: $SCAIDNS_API_KEY"
```

### Record conflicts

DNS has its own rules about which records can coexist:

- `CNAME` and `A`/`AAAA` can't share a name.
- `DNAME` and any other record can't share a name.
- `NS` at the zone apex requires at least one to remain present.

Violations return `409` with specifics:

```json
{
  "detail": {
    "message": "Cannot create A record: CNAME exists at www.example.com",
    "field": "type",
    "conflicts": ["record-uuid-of-the-cname"]
  }
}
```

### DNSSEC errors

Particular cases to watch:

- **Enabling when already enabled.** `409` — disable first or use rotate.
- **Confirming DS that doesn't match KSK.** `400` — check the `key_tag` and `digest`.
- **Disabling with DS still published.** Allowed, but resolvers will see broken DNSSEC. Remove DS at registrar first.

## Rate limiting headers

When you approach a rate limit, responses include:

```
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1714567890
```

When exceeded (`429`):

```
Retry-After: 60
```

The `Retry-After` value is either seconds or an HTTP date.

## Idempotency

Most state changes are not idempotent at the HTTP level. Retrying a failed `POST /records` may create a duplicate if the first attempt partially succeeded. The server protects against this with:

- Unique constraints on `(domain_id, name, type, content)` — duplicate records return `409`.
- Bulk operations default to atomic (all-or-nothing) mode; partial success requires opting in with `continue_on_error`.

If you need client-side idempotency, keep a record of the last successful create and check for its existence before retrying.

## Audit trail

Every write goes to the audit log. If an operation succeeds but the response was lost in transit, you can inspect `GET /api/v1/admin/audit-logs` to confirm the mutation happened before retrying.

## What's next

- [Error Codes reference](../reference/error-codes.md) — exhaustive code list.
- [API Guides](../tutorials/) — task-oriented walkthroughs showing error handling in context.
