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

# Errors

> The error shape every Yungle API failure shares, every error code with its status, and which errors are worth retrying.

Every failed request returns the same JSON shape with a stable `code`. Use this page to decide what your code does with each one.

## The error shape

```json theme={null}
{
  "error": {
    "code": "quota_exceeded",
    "message": "This upload would exceed your storage quota.",
    "details": { "usedBytes": 268435456000, "quotaBytes": 268435456000 },
    "docs": "https://docs.yungle.co/errors#quota_exceeded"
  }
}
```

| Field | What it is |
| - | - |
| `code` | Stable. Branch on this. |
| `message` | Written for a person reading a log. It can change, so never parse it. |
| `details` | Present only when there is something you can act on. Its fields depend on the code. |
| `docs` | A link to this code's row in the table below. |

A path under `/api` that is not an endpoint returns the same shape with `404 not_found`, so your client never has to parse an HTML error page.

The SDKs raise this as an exception: `YungleApiError` in JavaScript and `YungleError` in Python, each with `status`, `code`, `details` and `retryable`.

## Error codes

<table>
  <thead>
    <tr>
      <th>Code</th>
      <th>Status</th>
      <th>What to do</th>
    </tr>
  </thead>

  <tbody>
    <tr id="unauthorized">
      <td style={{ whiteSpace: 'nowrap' }}><code>unauthorized</code></td>
      <td>401</td>
      <td>Check the key. Missing, malformed, unknown, revoked and expired all look the same on purpose.</td>
    </tr>

    <tr id="upgrade_required">
      <td style={{ whiteSpace: 'nowrap' }}><code>upgrade\_required</code></td>
      <td>402</td>
      <td>The workspace is on the free plan. Nothing to retry.</td>
    </tr>

    <tr id="out_of_credit">
      <td style={{ whiteSpace: 'nowrap' }}><code>out\_of\_credit</code></td>
      <td>402</td>
      <td>API uploads past the monthly allowance are paid from prepaid credit, and there is none left. Top up in the dashboard, then retry.</td>
    </tr>

    <tr id="insufficient_scope">
      <td style={{ whiteSpace: 'nowrap' }}><code>insufficient\_scope</code></td>
      <td>403</td>
      <td>The key lacks a scope. <code>details.required</code> names it; create a key that has it.</td>
    </tr>

    <tr id="not_found">
      <td style={{ whiteSpace: 'nowrap' }}><code>not\_found</code></td>
      <td>404</td>
      <td>No such resource, or it belongs to another workspace — deliberately the same answer. Also returned for a path that is not an endpoint.</td>
    </tr>

    <tr id="invalid_request">
      <td style={{ whiteSpace: 'nowrap' }}><code>invalid\_request</code></td>
      <td>400</td>
      <td>Bad body. <code>details.issues</code> lists field paths and reasons.</td>
    </tr>

    <tr id="folder_error">
      <td style={{ whiteSpace: 'nowrap' }}><code>folder\_error</code></td>
      <td>400</td>
      <td>Duplicate or invalid folder name, or too deep. <code>details.code</code> says which.</td>
    </tr>

    <tr id="conflict">
      <td style={{ whiteSpace: 'nowrap' }}><code>conflict</code></td>
      <td>409</td>
      <td>Already exists — a contact with that email, typically. Usually safe to skip. With <code>details.reason</code> set, it is about your Idempotency-Key instead; see below.</td>
    </tr>

    <tr id="not_editable">
      <td style={{ whiteSpace: 'nowrap' }}><code>not\_editable</code></td>
      <td>410</td>
      <td>The transfer has been sent, revoked or expired. Composing is over.</td>
    </tr>

    <tr id="quota_exceeded">
      <td style={{ whiteSpace: 'nowrap' }}><code>quota\_exceeded</code></td>
      <td>413</td>
      <td>Storage is full. <code>details</code> carries used and total bytes. Delete something.</td>
    </tr>

    <tr id="transfer_too_large">
      <td style={{ whiteSpace: 'nowrap' }}><code>transfer\_too\_large</code></td>
      <td>413</td>
      <td>One transfer exceeded the plan ceiling. <code>details.limitBytes</code> is the cap.</td>
    </tr>

    <tr id="rate_limited">
      <td style={{ whiteSpace: 'nowrap' }}><code>rate\_limited</code></td>
      <td>429</td>
      <td>Back off. Honour the <code>Retry-After</code> header, or throttle on the <code>RateLimit</code> header before you get here.</td>
    </tr>

    <tr id="email_budget_exhausted">
      <td style={{ whiteSpace: 'nowrap' }}><code>email\_budget\_exhausted</code></td>
      <td>429</td>
      <td>Daily outbound email or invite budget spent. Nothing was sent and nothing changed. For a transfer, finalize again without <code>recipients</code> to get the link now, or retry tomorrow.</td>
    </tr>

    <tr id="password_required">
      <td style={{ whiteSpace: 'nowrap' }}><code>password\_required</code></td>
      <td>403</td>
      <td>The shared link is password protected. Send <code>password</code> with the request.</td>
    </tr>

    <tr id="wrong_password">
      <td style={{ whiteSpace: 'nowrap' }}><code>wrong\_password</code></td>
      <td>403</td>
      <td>Wrong password for the shared link. Attempts are budgeted per workspace and per link; after too many you get <code>rate\_limited</code>.</td>
    </tr>

    <tr id="guests_only">
      <td style={{ whiteSpace: 'nowrap' }}><code>guests\_only</code></td>
      <td>403</td>
      <td>The collection is open to invited guests only. A guest opens it in a browser, signed in; the link alone is not enough.</td>
    </tr>

    <tr id="link_unavailable">
      <td style={{ whiteSpace: 'nowrap' }}><code>link\_unavailable</code></td>
      <td>410</td>
      <td>The shared link has expired, was revoked, or is under a hold. Nothing to retry.</td>
    </tr>

    <tr id="e2ee_unsupported">
      <td style={{ whiteSpace: 'nowrap' }}><code>e2ee\_unsupported</code></td>
      <td>409</td>
      <td>End-to-end encrypted. Yungle holds only ciphertext and never the key, so it cannot give you usable bytes — open the full link, <code>#</code> part included, in a browser.</td>
    </tr>

    <tr id="internal_error">
      <td style={{ whiteSpace: 'nowrap' }}><code>internal\_error</code></td>
      <td>500</td>
      <td>Ours. <code>details.requestId</code> ties it to our logs — quote it if you get in touch.</td>
    </tr>
  </tbody>
</table>

## How to handle each class

| Status | Class | What your code does |
| - | - | - |
| `400` | Your request is wrong | Fix it. `details.issues` lists each bad field as `path` and `message`. Do not retry unchanged. |
| `401` | No valid key | Check the key. See [Authentication](/authentication). |
| `402` | Plan or credit | `upgrade_required`: the endpoint needs a paid plan. `out_of_credit`: top up API credit, then retry. |
| `403` | Not allowed | `insufficient_scope`: use a key with the scope in `details.required`. Password and guest-only links: see the table above. |
| `404` | Not found | The resource does not exist or belongs to another workspace. Do not retry. |
| `409` | Conflict | Usually safe to skip, such as a contact that already exists. With `details.reason`, it is about your `Idempotency-Key`. |
| `410` | Gone | The transfer is sent, revoked or expired, or a shared link is no longer available. Do not retry. |
| `413` | Too large | Storage is full or the transfer exceeds the plan's ceiling. `details` has the numbers. |
| `429` | Slow down | Wait for `Retry-After`, then retry. `email_budget_exhausted` is different: the transfer is live, so share its link yourself. |
| `500` | Our fault | Retry with backoff. If it persists, quote `details.requestId` when you get in touch. |

### Not found is never forbidden

A resource that exists but belongs to another workspace returns `404`, not `403`. A `403` would confirm the id is real and let anyone probe for other workspaces' ids. You cannot tell the two cases apart, and that is intended.

## Retrying

Retry only these:

* **`429`**: after the number of seconds in the `Retry-After` header. Most `429` bodies also carry `details.retryAfterSeconds`.
* **`500`** and dropped connections: with exponential backoff, a few times.
* **`409`** with `details.reason` `idempotency_in_progress`: after `Retry-After`, to get the first request's result.

Every other `4xx` is deterministic: the same request fails the same way.

Before you retry a `POST`, make it safe to repeat by sending an `Idempotency-Key`. [Pagination and idempotency](/pagination-and-idempotency#idempotency) explains how. The SDKs already retry `429` and `5xx` responses, and send an `Idempotency-Key` on every `POST`.

## Next steps

<Columns cols={2}>
  <Card title="Pagination and idempotency" icon="arrows-rotate" href="/pagination-and-idempotency">
    Walk long lists and make retries safe.
  </Card>

  <Card title="Limits" icon="gauge" href="/limits">
    The limits behind `429`, `413` and `email_budget_exhausted`.
  </Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.