> ## 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.

# 워크스페이스 저장

> 가변 드래프트(워크스페이스) 저장. 무효 내용도 저장 가능. 검증 결과 포함.

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

인증: API 키 필요(`Authorization: Bearer <API Key>`). App 작성자만. 그 외 `404`.

App 가변 드래프트를 저장. **검증 실패여도 저장은 거부되지 않음**: 전체 검증은 돌고 결과(`valid`와 `issues`)가 반환. 무효 드래프트도 유지되어 고친 뒤 재저장 가능.

App 식별자는 첫 저장 시 암묵 생성 — 별도 「App 생성」 엔드포인트 없음. 하드 게이트 `400`은 3가지만: 계정에 사용자 이름 있음, 페이로드 크기 내, `app_name` 미사용.

`expected_revision`은 낙관적 잠금(`409 revision-conflict`)으로 여러 탭의 조용한 덮어쓰기를 막음. 첨부 시크릿은 이 저장이 유효할 때만 저장. 무효 저장에 이름만 남기지 않음.

## 요청

### 경로 파라미터

<ParamField path="app_id" type="string" required>
  App 참조: `app_<hex>` 또는 `<namespace>/<app_name>`. 첫 저장에는 후자를 사용.
</ParamField>

### 요청 body

<ParamField body="manifest" type="string" required>
  원시 매니페스트 텍스트.
</ParamField>

<ParamField body="files" type="object">
  첨부 파일: 키는 상대 경로, 값은 텍스트.
</ParamField>

<ParamField body="secrets" type="object">
  시크릿 이름→평문. 이 저장이 유효할 때만 저장.
</ParamField>

<ParamField body="expected_revision" type="integer">
  기대하는 현재 드래프트 리비전. 불일치는 `409`.
</ParamField>

### 요청 예시

```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"
```

## 응답

### 200 성공

```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"
  }
}
```

페이로드는 `data`로 감쌉니다. 필드:

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

<ResponseField name="app_id" type="string" required>
  안정적인 App ID.
</ResponseField>

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

<ResponseField name="revision" type="integer" required>
  저장 후 드래프트 리비전.
</ResponseField>

<ResponseField name="valid" type="boolean" required>
  현재 내용이 검증을 통과했는지.
</ResponseField>

<ResponseField name="issues" type="object[]">
  문제 목록.

  <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[]">
  이 저장이 무효라 저장되지 않은 시크릿 이름.
</ResponseField>

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

### 오류

| HTTP | `code`              | `category`      | 설명                                                  |
| ---- | ------------------- | --------------- | --------------------------------------------------- |
| 401  | `unauthorized`      | `forbidden`     | API 키 누락 또는 무효.                                     |
| 400  | `username-required` | `invalid_input` | 계정에 아직 사용자 이름이 없어 `<username>/<app_name>`을 만들 수 없음. |
| 409  | `revision-conflict` | `invalid_input` | `expected_revision`이 현재 드래프트 리비전과 불일치 — 동시 편집.      |
| 400  | `invalid-app-name`  | `invalid_input` | 이름이 명명 규칙을 충족하지 않거나 예약어입니다.                         |
| 409  | `app-name-reserved` | `forbidden`     | 타인 이력 바인딩이 이름을 보유(사용자 이름 이전 후 30일 동결).              |
| 413  | `payload-too-large` | `invalid_input` | 요청 body가 크기 상한 초과.                                  |

오류 응답은 `{"error": {code, category, message, retryable}}`입니다. <a href="/docs/ko/datahub/api/reference/introduction#errors">오류</a> 참고.

## 클라이언트 라이브러리

Python / JavaScript SDK는 아직 이 엔드포인트를 감싸지 않습니다. REST를 직접 호출하세요.
