> ## Documentation Index
> Fetch the complete documentation index at: https://docs.declaw.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Error codes

> Why a 403 is usually not an authentication problem, and how to tell the kinds apart.

Every Declaw error response carries a human-readable `message`. Responses with
status `403` also carry a machine-readable `code`.

**Branch on `code`, not on `message`.** Messages are prose written for a person
reading a stack trace, and they get reworded. `code` is API surface and is
treated as such.

```json theme={null}
{
  "message": "command blocked (IMDS access): curl http://169.254.169.254/",
  "code": "policy_denied"
}
```

## A 403 is not an authentication failure

This trips people up, so it is worth stating plainly: **a bad, missing, expired,
or revoked API key returns `401`.** If you got a `403`, your credentials were
accepted. Something else stopped the request.

`403` covers several unrelated situations:

| `code`               | What happened                                                                             | Who can fix it                                  |
| -------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `policy_denied`      | A security policy blocked this action — an OPA gate, the command scanner, an egress rule. | The operator who set the policy                 |
| `forbidden`          | The resource belongs to another account, team, or owner.                                  | Nobody — you are asking for someone else's data |
| `tier_limit`         | Your plan's ceiling was reached (vCPU, memory, disk, session length, snapshot storage).   | You, by upgrading or asking for less            |
| `not_approved`       | The account has not cleared the waitlist.                                                 | Declaw, by approving you                        |
| `email_not_verified` | The email address is unverified.                                                          | You, by clicking the link                       |
| `admin_forbidden`    | The admin surface rejected the request.                                                   | The operator holding the admin secret           |

## `policy_denied` is Declaw working

The other codes are problems. `policy_denied` is the product doing its job — an
agent attempted something the operator forbade, and it was stopped.

That distinction matters for how you handle it. A `policy_denied` is usually not
an error in *your* integration; it is an event your users may want to see:

```python theme={null}
from declaw import Sandbox

sbx = Sandbox.create(template="python")
result = sbx.commands.run("curl http://169.254.169.254/latest/meta-data/")
```

```
declaw.exceptions.AuthenticationError:
  HTTP 403: command blocked (IMDS access): curl http://169.254.169.254/
```

<Warning>
  The SDKs currently raise an **authentication** error for every `403`, including
  policy denials — the type is misleading even though the message is correct.
  Until a dedicated error type ships, read `code` off the response body rather
  than relying on the exception class, and do not treat these as credential
  problems.
</Warning>

Command, PTY, and stdio denials are decided by a gate running **inside** the
sandbox. Those responses carry `code` too — it is applied as the response leaves
the host, so you get the same envelope regardless of which layer made the
decision.

## Denials are audited

Most policy denials are recorded in the audit trail, so you can reconstruct what
was blocked without parsing error strings. Command, PTY, and stdio denials also
carry the compliance control IDs that fired — both in the audit entry and on the
`X-Declaw-Policy-Controls` response header — so you can see which governance
control each denial satisfied.

Not every denial produces an audit entry today; a request refused because the
sandbox has public traffic disabled, for example, does not.

See [Audit logging](/security/audit-logging) for retrieval.

## Other statuses

| Status        | Meaning                                                                                                                                                                                                                                   |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` / `422` | The request was malformed or failed validation.                                                                                                                                                                                           |
| `401`         | Credentials missing, invalid, expired, or revoked.                                                                                                                                                                                        |
| `402`         | Insufficient balance. The body carries `wallet_type`.                                                                                                                                                                                     |
| `404`         | Not found. Some resources — template build logs among them — also return `404` when the resource belongs to another tenant, so the response cannot confirm it exists. Sandboxes instead return `403` with `code: forbidden` in that case. |
| `409`         | Conflict, such as a duplicate alias or a version mismatch.                                                                                                                                                                                |
| `413`         | Payload too large.                                                                                                                                                                                                                        |
| `429`         | Rate limited. A `Retry-After` **header** carries the backoff.                                                                                                                                                                             |

<Note>
  Revoking an API key takes effect within about 60 seconds — credentials are
  cached briefly. Plan for that window if key revocation is part of your incident
  response.
</Note>
