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

# Save workspace

> Save the app's mutable draft (workspace). Invalid content can still be saved; validation results come back in the response.

**`PUT`** `https://api-datahub.octoparse.com/v1/data-apps/{app_id}/workspace`

Authentication: API key required (`Authorization: Bearer <API Key>`). App author only; others get `404`.

Save the app's mutable draft. **Saves are never rejected for validation failure**: full validation still runs, and the result (`valid` / `issues`) is returned with the response. "Save a half-finished draft and come back later" is a model guarantee.

App identity is created implicitly on the first save — there is no separate "create app" endpoint. Only three hard gates return `400`: the account has a username, the payload is within size limits, and attached filenames are valid. `identity.app_name` inside the manifest is no longer compared to the path: the route name follows the reference, and the name declared in the file is consumed only at creation time.

`expected_revision` provides optimistic concurrency (`409 revision-conflict`) so multiple tabs cannot silently overwrite each other. Attached secrets are stored only when this save is valid; names that were not stored are reported in `secrets_deferred`. Deprecated apps can still be edited; only new Releases are rejected.

## Request

### Path parameters

<ParamField path="app_id" type="string" required>
  App reference: `app_<hex>` or `<namespace>/<app_name>`. Use the latter on first save.
</ParamField>

### Request body

<ParamField body="manifest" type="string" required>
  Raw manifest text.
</ParamField>

<ParamField body="files" type="object">
  Attached files: keys are relative paths, values are text content.
</ParamField>

<ParamField body="secrets" type="object">
  Map of secret names to plaintext values. Stored only when this save is valid.
</ParamField>

<ParamField body="expected_revision" type="integer">
  Expected current draft revision. Mismatch returns `409`.
</ParamField>

### Example request

```bash theme={null}
curl -X PUT \
  -H "Authorization: Bearer $OCTOPARSE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"manifest": "spec_version: \"0.2\"\n…", "files": {}, "expected_revision": 3}' \
  "https://api-datahub.octoparse.com/v1/data-apps/carol/reviews-query/workspace"
```

## Response

### 200 success

```json theme={null}
{
  "data": {
    "app_name": "string",
    "app_id": "string",
    "namespace": "string",
    "revision": 0,
    "valid": false,
    "issues": [
      {
        "path": "string",
        "message": "string"
      }
    ],
    "missing_secrets": [
      "string"
    ],
    "secrets_deferred": [
      "string"
    ],
    "name_reused_from": "string"
  }
}
```

The payload is wrapped in `data`. Fields:

<ResponseField name="app_name" type="string" required>
  —
</ResponseField>

<ResponseField name="app_id" type="string" required>
  Stable app id.
</ResponseField>

<ResponseField name="namespace" type="string">
  —
</ResponseField>

<ResponseField name="revision" type="integer" required>
  Draft revision after the save.
</ResponseField>

<ResponseField name="valid" type="boolean" required>
  Whether the current content passed validation.
</ResponseField>

<ResponseField name="issues" type="object[]">
  Issue list.

  <Expandable title="fields">
    <ResponseField name="path" type="string" required>
      —
    </ResponseField>

    <ResponseField name="message" type="string" required>
      —
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="missing_secrets" type="string[]">
  —
</ResponseField>

<ResponseField name="secrets_deferred" type="string[]">
  Secret names not stored because this save was invalid.
</ResponseField>

<ResponseField name="name_reused_from" type="string">
  —
</ResponseField>

### Errors

| HTTP | `code`              | `category`      | Description                                                                                  |
| ---- | ------------------- | --------------- | -------------------------------------------------------------------------------------------- |
| 401  | `unauthorized`      | `forbidden`     | Missing or invalid API key.                                                                  |
| 400  | `username-required` | `invalid_input` | Account has no username yet, so a `<username>/<app_name>` reference cannot be formed.        |
| 409  | `revision-conflict` | `invalid_input` | `expected_revision` does not match the current draft revision — concurrent edit.             |
| 400  | `invalid-app-name`  | `invalid_input` | Name fails naming rules or is a reserved word.                                               |
| 409  | `app-name-reserved` | `forbidden`     | Name is held by someone else's historical binding (30-day freeze after a username transfer). |
| 413  | `payload-too-large` | `invalid_input` | Request body exceeds the size limit.                                                         |

Error responses use `{"error": {code, category, message, retryable}}`. See <a href="/docs/en/datahub/api/reference/introduction#errors">Errors</a>.

## Client libraries

The Python and JavaScript SDKs do not wrap this endpoint yet. Call REST directly.
