> This page is for version v2026-02-09 (default).
> For other versions, use one of these documentation indexes:
> - v2026-02-09 (default): https://docs.extend.ai/2026-02-09/llms.txt
> - v2025-04-21: https://docs.extend.ai/2025-04-21/llms.txt
> - v2024-12-23: https://docs.extend.ai/2024-12-23/llms.txt

> ## Documentation Index
> Fetch the complete documentation index at: https://docs.extend.ai/llms.txt
> Use this file to discover all available pages before exploring further.
>
> ## API version
> The current API version is `2026-02-09`, served at the site root (no version prefix in URLs).
> If this page URL contains `/2025-04-21/` or `/2024-12-23/`, you are reading an older API version.
> Prefer the current docs at https://docs.extend.ai/llms.txt unless the user explicitly needs that older version.
> Do not treat older-version pages as the source of truth for new integrations.

# Editing Error Handling

> Handle Edit API errors: structured error fields, failure reasons, HTTP status codes, and sync vs async patterns.

When an error occurs, the Edit API returns a structured error with the following fields:

| Field       | Type    | Description                                                                              |
| ----------- | ------- | ---------------------------------------------------------------------------------------- |
| `code`      | string  | A specific error code for programmatic handling.                                         |
| `message`   | string  | A human-readable description of the error. Don't rely on its exact text — it may change. |
| `retryable` | boolean | Whether retrying the request might succeed. When `true`, retry with exponential backoff. |
| `requestId` | string  | A unique identifier for the request, useful when contacting support.                     |

## How failures surface: sync vs. async

Where an error shows up depends on which endpoint you use.

* **Synchronous (`/edit`)** waits for processing to finish, so every failure — both request validation and document processing — comes back as an **HTTP error response** with the fields above. The [HTTP status codes](#http-status-codes) below apply to this endpoint.
* **Asynchronous (`/edit_runs`)** returns `200` with an edit run as soon as the job is accepted. Request-level problems (invalid request, auth) still return an HTTP error at creation time, but **document processing failures are not HTTP errors** — the run instead reaches `status: "FAILED"` with a `failureReason` and `failureMessage` (one of the [failure reasons](#failure-reasons) below), which you read when you poll or receive the webhook.

> **Note**
>
> Production integrations should use the asynchronous `/edit_runs` endpoint and check `status` on the returned run. See [Move to production with async processing](/editing/best-practices#move-to-production-with-async-processing).

## Failure reasons

These codes describe why an edit failed. On the sync endpoint they appear in the error body's `code` field; on the async endpoint they appear as the run's `failureReason`. Most are client errors related to the file or config and are not retryable. The structured error's `retryable` flag is authoritative at runtime.

| Failure reason             | Description                                                                                                                                               | Retryable |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| `UNABLE_TO_DOWNLOAD_FILE`  | Failed to load the requested file (e.g. an expired or malformed URL).                                                                                     | ❌         |
| `FILE_TYPE_NOT_SUPPORTED`  | File type not supported. Edit runs currently require a PDF.                                                                                               | ❌         |
| `FILE_SIZE_TOO_LARGE`      | The file exceeds the maximum allowed size.                                                                                                                | ❌         |
| `CORRUPT_FILE`             | The file appears to be corrupted and cannot be edited.                                                                                                    | ❌         |
| `FIELD_DETECTION_ERROR`    | An error occurred during field detection.                                                                                                                 | ❌         |
| `OCR_ERROR`                | An error occurred in the OCR system.                                                                                                                      | ✅         |
| `PASSWORD_PROTECTED_FILE`  | The file is password protected. Retry with the file's password using `file.settings.password`. See [Password-Protected Files](/password-protected-files). | ❌         |
| `FAILED_TO_CONVERT_TO_PDF` | The file could not be converted to PDF for processing.                                                                                                    | ❌         |
| `INVALID_OPTIONS`          | The provided configuration options are invalid.                                                                                                           | ❌         |
| `SCHEMA_VALIDATION_FAILED` | The provided schema failed validation.                                                                                                                    | ❌         |
| `SCHEMA_VALIDATION_ERROR`  | The generated output value failed validation against the edit schema, including its conditional validation rules.                                         | ❌         |
| `EMPTY_SCHEMA`             | No schema was provided and no fields could be detected.                                                                                                   | ❌         |
| `OUT_OF_CREDITS`           | Insufficient credits to process the file.                                                                                                                 | ❌         |
| `INTERNAL_ERROR`           | An unexpected internal error occurred.                                                                                                                    | ✅         |

> **Note**
>
> Additional failure reasons may be added in the future. Handle unknown values gracefully.

## HTTP status codes

These HTTP status codes are returned by the synchronous `/edit` endpoint. On the asynchronous `/edit_runs` endpoint, only request-level errors are returned at creation time — processing failures surface on the run as `status: "FAILED"`. We generally recommend relying on the failure reasons above for programmatic handling.

### 400 Bad Request

Returned when required fields are missing (e.g. `file`), the file locator is invalid, or the `config` contains invalid options.

### 401 Unauthorized

Returned when the API token is missing or invalid.

### 402 Payment Required

Returned when the workspace has insufficient credits to process the file.

### 403 Forbidden

Returned when the authenticated workspace or token doesn't have permission to use the edit functionality.

### 404 Not Found

Returned when a referenced file or resource cannot be found.

### 422 Unprocessable Entity

Returned when the file cannot be processed — for example, it is corrupt, password protected, or could not be converted to PDF.

### 429 Too Many Requests

Returned when you exceed the rate limit. Retry with exponential backoff.

### 500 Internal Server Error

Returned when an unexpected error occurs during editing. Retry with exponential backoff.

## Handling errors

Error handling differs between the two endpoints: with sync you catch a thrown HTTP error, while with async you also check the run's terminal `status`. Pick the tab that matches how you call Edit.

#### Synchronous (/edit)

Catch the HTTP error and inspect its status code and body.

#### Python

```python
from extend_ai import Extend
from extend_ai.core.api_error import ApiError

client = Extend(token="your_api_key")

try:
    result = client.edit(
        file={"url": "https://example.com/form.pdf"},
        config={"instructions": "Fill out the form with the provided data."},
    )
except ApiError as e:
    # e.body holds the structured error: { code, message, retryable, requestId }
    body = e.body or {}
    if body.get("retryable"):
        ...  # retry with exponential backoff
    print(f"edit failed ({e.status_code}): {body.get('code')} — {body.get('message')}")
    raise
```

#### TypeScript

```typescript
import { ExtendClient, ExtendError } from "extend-ai";

const client = new ExtendClient({ token: "your_api_key" });

try {
  await client.edit({
    file: { url: "https://example.com/form.pdf" },
    config: { instructions: "Fill out the form with the provided data." },
  });
} catch (error) {
  if (error instanceof ExtendError) {
    // error.body holds the structured error: { code, message, retryable, requestId }
    const body = error.body as { code: string; message: string; retryable: boolean; requestId: string };
    if (body.retryable) {
      // retry with exponential backoff
    }
    console.error(`edit failed (${error.statusCode}): ${body.code} — ${body.message}`);
  }
  throw error;
}
```

#### Java

```java
import ai.extend.core.ExtendClientApiException;
import ai.extend.requests.EditRequest;
import ai.extend.types.EditConfig;
import ai.extend.types.EditRequestFile;
import ai.extend.types.FileFromUrl;

try {
    client.edit(EditRequest.builder()
        .file(EditRequestFile.of(FileFromUrl.builder()
            .url("https://example.com/form.pdf")
            .build()))
        .config(EditConfig.builder()
            .instructions("Fill out the form with the provided data.")
            .build())
        .build());
} catch (ExtendClientApiException e) {
    // e.statusCode() is the HTTP status; e.body() holds { code, message, retryable, requestId }
    System.err.println("edit failed (" + e.statusCode() + "): " + e.body());
    throw e;
}
```

#### Go

```go
import (
	"context"
	"errors"
	"log"

	extend "github.com/extend-hq/extend-go-sdk"
	"github.com/extend-hq/extend-go-sdk/core"
)

_, err := c.Edit(context.TODO(), &extend.EditRequest{
	File: &extend.EditRequestFile{
		FileFromURL: &extend.FileFromURL{URL: "https://example.com/form.pdf"},
	},
	Config: &extend.EditConfig{
		Instructions: extend.String("Fill out the form with the provided data."),
	},
})
if err != nil {
	var apiErr *core.APIError
	if errors.As(err, &apiErr) {
		// apiErr.StatusCode is the HTTP status: 400, 401, 402, 403, 404, 422, 429, 500, ...
		log.Printf("edit failed (status %d): %v", apiErr.StatusCode, apiErr)
	}
	return err
}
```

#### Asynchronous (/edit_runs)

A request-level problem still throws at creation time. A document that fails to process does **not** throw — the run comes back with `status: "FAILED"`, so check the terminal status and read `failureReason` / `failureMessage`.

#### Python

```python
from extend_ai import Extend
from extend_ai.core.api_error import ApiError

client = Extend(token="your_api_key")

try:
    run = client.edit_runs.create_and_poll(
        file={"url": "https://example.com/form.pdf"},
        config={"instructions": "Fill out the form with the provided data."},
    )
except ApiError as e:
    # Request-level errors (invalid request, auth) still raise at creation time
    print(f"could not start edit ({e.status_code}): {e.body}")
    raise

# Processing failures surface on the run, not as a raised error
if run.status == "FAILED":
    print(f"edit failed: {run.failure_reason} — {run.failure_message}")
```

#### TypeScript

```typescript
import { ExtendClient, ExtendError } from "extend-ai";

const client = new ExtendClient({ token: "your_api_key" });

let run;
try {
  run = await client.editRuns.createAndPoll({
    file: { url: "https://example.com/form.pdf" },
    config: { instructions: "Fill out the form with the provided data." },
  });
} catch (error) {
  if (error instanceof ExtendError) {
    // Request-level errors (invalid request, auth) still throw at creation time
    console.error(`could not start edit (${error.statusCode}): ${JSON.stringify(error.body)}`);
  }
  throw error;
}

// Processing failures surface on the run, not as a thrown error
if (run.status === "FAILED") {
  console.error(`edit failed: ${run.failureReason} — ${run.failureMessage}`);
}
```

#### Java

```java
import ai.extend.core.ExtendClientApiException;
import ai.extend.resources.editruns.requests.EditRunsCreateRequest;
import ai.extend.resources.editruns.types.EditRunsCreateRequestFile;
import ai.extend.types.EditConfig;
import ai.extend.types.EditRun;
import ai.extend.types.EditRunStatus;
import ai.extend.types.FileFromUrl;

EditRun run;
try {
    run = client.editRuns().createAndPoll(EditRunsCreateRequest.builder()
        .file(EditRunsCreateRequestFile.of(FileFromUrl.builder()
            .url("https://example.com/form.pdf")
            .build()))
        .config(EditConfig.builder()
            .instructions("Fill out the form with the provided data.")
            .build())
        .build());
} catch (ExtendClientApiException e) {
    // Request-level errors (invalid request, auth) still throw at creation time
    System.err.println("could not start edit (" + e.statusCode() + "): " + e.body());
    throw e;
}

// Processing failures surface on the run, not as a thrown error
if (run.getStatus() == EditRunStatus.FAILED) {
    System.err.println("edit failed: " + run.getFailureReason().orElse("")
        + " — " + run.getFailureMessage().orElse(""));
}
```

#### Go

```go
import (
	"context"
	"errors"
	"log"
	"time"

	extend "github.com/extend-hq/extend-go-sdk"
	"github.com/extend-hq/extend-go-sdk/core"
)

run, err := c.EditRuns.Create(context.TODO(), &extend.EditRunsCreateRequest{
	File: &extend.EditRunsCreateRequestFile{
		FileFromURL: &extend.FileFromURL{URL: "https://example.com/form.pdf"},
	},
	Config: &extend.EditConfig{
		Instructions: extend.String("Fill out the form with the provided data."),
	},
})
if err != nil {
	var apiErr *core.APIError
	if errors.As(err, &apiErr) {
		// Request-level errors (4xx) still come back as HTTP errors
		log.Printf("could not start edit (status %d): %v", apiErr.StatusCode, apiErr)
	}
	return err
}

// Poll until the run reaches a terminal state
for run.Status == extend.EditRunStatusProcessing {
	time.Sleep(2 * time.Second)
	run, err = c.EditRuns.Retrieve(context.TODO(), run.ID, &extend.EditRunsRetrieveRequest{})
	if err != nil {
		return err
	}
}

// Processing failures surface on the run, not as an HTTP error
if run.Status == extend.EditRunStatusFailed {
	log.Printf("edit failed: %s — %s", *run.FailureReason, *run.FailureMessage)
}
```

For retryable errors (`OCR_ERROR`, `INTERNAL_ERROR`, or any `5xx`), retry with exponential backoff. Always log the `requestId` when contacting support.