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

# Documents

> Upload, set audience, and remove documents on a teardown.

The generic document endpoints for a teardown your org owns. For
slot-specific documents (harvest list / OCCM / aircraft-details /
non-incident-statement), see [the slot endpoints](/api-reference/uploads/teardown-slot).

## Upload

```http theme={null}
POST /public/v1/teardown/documents/{teardown_id}
```

Attach a document to one of your teardowns. The URL is also appended
to the teardown's `documents[]` array so it shows up in detail
responses.

### Headers

<ParamField header="Authorization" type="string" required>
  `Bearer tdao_live_…`
</ParamField>

<ParamField header="X-Organization-Id" type="string" required />

<ParamField header="Content-Type" type="string" required>
  `multipart/form-data` (curl's `-F` does this automatically).
</ParamField>

### Form fields

<ParamField body="file" type="file" required>
  The file to upload.
</ParamField>

<ParamField body="audience" type="string (CSV)">
  Optional. Comma-separated list of company types to restrict who
  can see documents on this teardown. Allowed:
  `Airline / Lessor / OEM / MRO / Distributor / Others`.
  Case-insensitive. See [audience](/concepts/audience).
</ParamField>

### File rules

* **Max size:** 50 MB per file.
* **Allowed types:** PDF, XLSX, XLS, DOCX, DOC, CSV, JPEG, PNG,
  WEBP, GIF.
* **One file per call.** Loop client-side for batch uploads.

### Storage path

Files land at:

```
{SUPABASE_URL}/storage/v1/object/public/teardowns-media/teardowns/<teardown_id>/documents/<8-hex>_<filename>
```

The 8-character prefix randomises the path so naming collisions are
impossible.

### Response

`200 OK`:

```jsonc theme={null}
{
  "url": "https://.../teardowns-media/teardowns/<id>/documents/abcd1234_specs.pdf",
  "filename": "specs.pdf",
  "size": 123456,
  "content_type": "application/pdf"
}
```

The URL is also appended to `teardown.documents[]` automatically no
separate PATCH needed.

<RequestExample>
  ```bash upload only theme={null}
  curl -X POST "$base_url/public/v1/teardown/documents/$teardown_id" \
    -H "Authorization: Bearer $api_key" \
    -H "X-Organization-Id: $org_id" \
    -F "file=@/absolute/path/to/specs.pdf"
  ```

  ```bash upload + set audience (one round trip) theme={null}
  curl -X POST "$base_url/public/v1/teardown/documents/$teardown_id" \
    -H "Authorization: Bearer $api_key" \
    -H "X-Organization-Id: $org_id" \
    -F "file=@/absolute/path/to/specs.pdf" \
    -F "audience=Airline, MRO, Others"
  ```

  ```python python theme={null}
  import requests
  with open("specs.pdf", "rb") as f:
      resp = requests.post(
          f"{base_url}/public/v1/teardown/documents/{teardown_id}",
          headers={"Authorization": f"Bearer {api_key}", "X-Organization-Id": org_id},
          files={"file": f},
          data={"audience": "Airline, MRO, Others"},
      )
  resp.raise_for_status()
  print(resp.json()["url"])
  ```

  ```typescript node theme={null}
  import { readFile } from "node:fs/promises";
  const form = new FormData();
  form.append("file", new Blob([await readFile("specs.pdf")]), "specs.pdf");
  form.append("audience", "Airline, MRO, Others");

  const resp = await fetch(
    `${baseUrl}/public/v1/teardown/documents/${teardownId}`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "X-Organization-Id": orgId,
      },
      body: form,
    },
  );
  const { url } = await resp.json();
  ```
</RequestExample>

### Audience write happens BEFORE the upload

If you send an invalid `audience` value, you get `422 invalid_audience`
**before** the file is uploaded. No orphan storage paths.

```jsonc theme={null}
{
  "detail": {
    "error_code": "invalid_audience",
    "message": "Unknown audience value(s): ['BogusCo']. Allowed (case-insensitive): ['Airline','Lessor','OEM','MRO','Distributor','Others']",
    "got": ["BogusCo"],
    "allowed": ["Airline","Lessor","OEM","MRO","Distributor","Others"]
  }
}
```

Sending the same `audience` the teardown already has is a no-op no
audit row written.

## Delete by URL

```http theme={null}
DELETE /public/v1/teardown/documents/{teardown_id}/by-url?url=<encoded-url>
```

Removes a single document URL from the teardown's `documents[]`.
Best-effort deletes the underlying storage file.

### Query parameters

<ParamField query="url" type="string" required>
  The full document URL (URL-encoded).
</ParamField>

### Response

`200 OK`:

```jsonc theme={null}
{ "status": "ok" }
```

Idempotent deleting a URL that isn't present returns 200 too. The
storage delete is best-effort; failures are logged but never fail the
call.

<RequestExample>
  ```bash curl theme={null}
  encoded_url=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$DOC_URL")

  curl -X DELETE "$base_url/public/v1/teardown/documents/$teardown_id/by-url?url=$encoded_url" \
    -H "Authorization: Bearer $api_key" \
    -H "X-Organization-Id: $org_id"
  ```

  ```python python theme={null}
  import requests
  resp = requests.delete(
      f"{base_url}/public/v1/teardown/documents/{teardown_id}/by-url",
      headers={"Authorization": f"Bearer {api_key}", "X-Organization-Id": org_id},
      params={"url": doc_url},
  )
  resp.raise_for_status()
  ```
</RequestExample>

## See also

* [Teardown images](/api-reference/uploads/teardown-images) for image
  upload + by-url delete.
* [Teardown slot documents](/api-reference/uploads/teardown-slot) for
  the named slots (harvest list, OCCM, etc.).
* [Audience](/concepts/audience) for the visibility model.
