---
title: "Sandbox API failures · intentic"
description: "A refusal is a result, not a crash: every failure comes back as JSON with a message. What each status means on an intentic sandbox, and which ones are worth retrying."
url: "https://intentic.dev/api/errors/"
updated: "2026-08-24"
---

Start here

# When a call fails

Every failure comes back as JSON with a message, never as an empty body and never as a stack trace. There are five statuses to know, and one distinction that matters more than all of them.

**On this page (4 sections)**

- [A refusal is a result](#a-refusal-is-a-result)
- [Every status, and what it means](#statuses)
- [Reported, not raised](#reported-not-raised)
- [What is worth retrying](#retrying)

## A refusal is a result

The daemon does not crash at you. A call it will not perform answers with a status and an object carrying a `message`, and where the reason is structured (a body that did not match its shape), the object carries the specific fields it could not accept.

A commit with no message

```bash
curl -X POST "$SANDBOX/git/root/commit" \
 -H "x-intentic-control: $INTENTIC_TOKEN" \
 -H "content-type: application/json" \
 -d '{}'

HTTP/1.1 400 Bad Request
content-type: application/json

{ "message": "Input validation failed",
 "issues": [ { "path": ["message"], "message": "Required" } ] }
```

The playground on every group page produces exactly this when you clear a required field, which is the cheapest way to see the shape without a sandbox in front of you.

## Every status, and what it means

| Status | Means |
| --- | --- |
| `400` | The input did not match the declared shape, or a path escaped the workspace. |
| `401` | No credential, or one that does not verify. The two are indistinguishable on purpose: telling them apart would confirm which half of a guess was right. |
| `403` | A real credential that may not go there: an identity that is not a member, a member below the route's role floor (the answer names the tier), or a [control token outside its scope](https://intentic.dev/api/auth/). |
| `404` | No such route on this daemon, or no such thing. Also the answer for the daemon's own credential files, which are not readable through the file routes at all. |
| `409` | The state will not allow it right now, and waiting might. Landing a conversation's work while its turn is running, restoring files an agent is editing, starting a second loop on one conversation. |
| `413` | Too large. The file routes are bounded so that one enormous log cannot stall the daemon for everyone. |

## Reported, not raised

A whole class of things that look like failures are answers instead, and this is the single most useful thing to know about reading this API. A push with no remote configured, a pull that cannot fast-forward, a merge that conflicts, a country switch that did not take: none of those raise. They come back `200` with an object that says what happened.

The rule behind it is that these are ordinary outcomes a screen has to render, not breakages. If a person would look at the result and decide something, it is a value. If the request itself was wrong, it is a status.

## What is worth retrying

`409` is the only one where the same request may succeed later without anything else changing, and even then only once the turn or operation that is holding the state has finished. `400`, `403` and `413` will answer the same way for ever. `401` is worth exactly one retry, after renewing the session.

`404` deserves a second look before you retry it: on an older daemon it may mean the route does not exist yet rather than that the thing does not. [Old daemon, new client](https://intentic.dev/api/calls/) is how to tell those apart.

More in Start here

[Previous ← The shape of a call](https://intentic.dev/api/calls/)[Next Streams →](https://intentic.dev/api/streams/)
