---
title: Uploads
description: The Uploads API moves CAD and drawing files into ARCNM.
---

# Uploads

The Uploads API moves CAD and drawing files into ARCNM: presign a direct upload, confirm it, then list, retry, cancel, or fetch a download URL.

> **Auto-generated** from the public OpenAPI spec — this page never
> drifts from the running API. Base URL `https://api.arcnm.io`. Authenticate with
> the `X-API-Key` header (see [Authentication](../authentication.md)).

## List Uploads

`GET /api/v1/uploads/`

List the org's uploads, hiding cancelled rows by default.

Cancelled uploads are useless — the file may have been written to
storage but the download endpoint refuses to serve it (status gate
is ``confirmed`` only). They just clutter the picker, so the UI gets
them filtered out unless an admin explicitly opts in via
``?include_cancelled=true`` (e.g. for an audit view).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `include_cancelled` | query | boolean | no | Include cancelled uploads in the list (hidden by default). |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X GET https://api.arcnm.io/api/v1/uploads/ \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/uploads/",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/uploads/", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

| Status | Code | When |
| --- | --- | --- |
| `401` | `invalid_api_key` | Missing, malformed, or revoked API key. |
| `403` | `insufficient_scope` | The key is valid but lacks a scope this endpoint requires. |
| `429` | `rate_limited` | Per-key or per-org rate limit exceeded — back off with jitter and retry. |


## Cancel Upload

`POST /api/v1/uploads/{data_source_id}/cancel`

Mark a pending upload as ``cancelled``.

Idempotent for already-cancelled rows; rejects rows that have moved
past ``pending`` since cancelling a confirmed file would silently
delete a valid artefact.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `data_source_id` | path | string | yes | Identifier of the data source. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/uploads/{data_source_id}/cancel \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/uploads/{data_source_id}/cancel",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/uploads/{data_source_id}/cancel", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

| Status | Code | When |
| --- | --- | --- |
| `401` | `invalid_api_key` | Missing, malformed, or revoked API key. |
| `403` | `insufficient_scope` | The key is valid but lacks a scope this endpoint requires. |
| `404` | `not_found` | A referenced resource doesn't exist or isn't visible to your organisation. |
| `409` | `conflict` | A conflicting change, or an `Idempotency-Key` reused with a different body. |
| `429` | `rate_limited` | Per-key or per-org rate limit exceeded — back off with jitter and retry. |


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `content_type` | string | MIME type of the file (e.g. 'application/pdf'); null if unknown. |
| `created_at` | string | ISO 8601 timestamp when the upload record was created. |
| `id` | string | Unique identifier of the uploaded data source. |
| `name` | string | Original filename of the uploaded file. |
| `sha256` | string | Hex-encoded SHA-256 checksum of the file; null until confirmed. |
| `size_bytes` | integer | Size of the file in bytes; null until the upload is confirmed. |
| `status` | string | Upload lifecycle state: pending, confirmed, failed, or cancelled. |

**Example response**

```json
{
  "content_type": "string",
  "created_at": "2026-06-01T12:00:00Z",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "string",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015…",
  "size_bytes": 204800,
  "status": "string"
}
```

## Download Url

`GET /api/v1/uploads/{data_source_id}/download-url`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `data_source_id` | path | string | yes | Identifier of the data source. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X GET https://api.arcnm.io/api/v1/uploads/{data_source_id}/download-url \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/uploads/{data_source_id}/download-url",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/uploads/{data_source_id}/download-url", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

| Status | Code | When |
| --- | --- | --- |
| `401` | `invalid_api_key` | Missing, malformed, or revoked API key. |
| `403` | `insufficient_scope` | The key is valid but lacks a scope this endpoint requires. |
| `404` | `not_found` | A referenced resource doesn't exist or isn't visible to your organisation. |
| `429` | `rate_limited` | Per-key or per-org rate limit exceeded — back off with jitter and retry. |


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `url` | string | Short-lived presigned URL to download the confirmed file. |

**Example response**

```json
{
  "url": "string"
}
```

## Retry Upload

`POST /api/v1/uploads/{data_source_id}/retry`

Re-queue the confirm worker for a non-confirmed upload.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `data_source_id` | path | string | yes | Identifier of the data source. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/uploads/{data_source_id}/retry \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/uploads/{data_source_id}/retry",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/uploads/{data_source_id}/retry", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

| Status | Code | When |
| --- | --- | --- |
| `401` | `invalid_api_key` | Missing, malformed, or revoked API key. |
| `403` | `insufficient_scope` | The key is valid but lacks a scope this endpoint requires. |
| `404` | `not_found` | A referenced resource doesn't exist or isn't visible to your organisation. |
| `409` | `conflict` | A conflicting change, or an `Idempotency-Key` reused with a different body. |
| `429` | `rate_limited` | Per-key or per-org rate limit exceeded — back off with jitter and retry. |


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `content_type` | string | MIME type of the file (e.g. 'application/pdf'); null if unknown. |
| `created_at` | string | ISO 8601 timestamp when the upload record was created. |
| `id` | string | Unique identifier of the uploaded data source. |
| `name` | string | Original filename of the uploaded file. |
| `sha256` | string | Hex-encoded SHA-256 checksum of the file; null until confirmed. |
| `size_bytes` | integer | Size of the file in bytes; null until the upload is confirmed. |
| `status` | string | Upload lifecycle state: pending, confirmed, failed, or cancelled. |

**Example response**

```json
{
  "content_type": "string",
  "created_at": "2026-06-01T12:00:00Z",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "string",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015…",
  "size_bytes": 204800,
  "status": "string"
}
```

## Bulk Cancel Uploads

`POST /api/v1/uploads/bulk-cancel`

Cancel many pending uploads at once.

Per-row failures are reported in ``skipped`` so the UI can render a
summary toast — the whole batch isn't rolled back if a single id is
already confirmed. We cap the batch size to keep a single request
bounded.

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | string[] | yes | Upload (data source) IDs to cancel or retry in bulk. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/uploads/bulk-cancel \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/uploads/bulk-cancel",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/uploads/bulk-cancel", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }),
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

| Status | Code | When |
| --- | --- | --- |
| `401` | `invalid_api_key` | Missing, malformed, or revoked API key. |
| `403` | `insufficient_scope` | The key is valid but lacks a scope this endpoint requires. |
| `409` | `conflict` | A conflicting change, or an `Idempotency-Key` reused with a different body. |
| `429` | `rate_limited` | Per-key or per-org rate limit exceeded — back off with jitter and retry. |


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `cancelled` | string[] | Upload IDs that were successfully cancelled. |
| `retried` | string[] | Upload IDs that were successfully re-queued for confirmation. |
| `skipped` | object[] | Uploads that were skipped, each as ``{id, reason}``. |

**Example response**

```json
{
  "cancelled": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "retried": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    {}
  ]
}
```

## Bulk Retry Uploads

`POST /api/v1/uploads/bulk-retry`

Re-queue many uploads at once.

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | string[] | yes | Upload (data source) IDs to cancel or retry in bulk. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/uploads/bulk-retry \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/uploads/bulk-retry",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/uploads/bulk-retry", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }),
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

| Status | Code | When |
| --- | --- | --- |
| `401` | `invalid_api_key` | Missing, malformed, or revoked API key. |
| `403` | `insufficient_scope` | The key is valid but lacks a scope this endpoint requires. |
| `409` | `conflict` | A conflicting change, or an `Idempotency-Key` reused with a different body. |
| `429` | `rate_limited` | Per-key or per-org rate limit exceeded — back off with jitter and retry. |


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `cancelled` | string[] | Upload IDs that were successfully cancelled. |
| `retried` | string[] | Upload IDs that were successfully re-queued for confirmation. |
| `skipped` | object[] | Uploads that were skipped, each as ``{id, reason}``. |

**Example response**

```json
{
  "cancelled": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "retried": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    {}
  ]
}
```

## Confirm

`POST /api/v1/uploads/confirm`

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data_source_id` | string | yes | UUID of the presigned upload to validate and ingest. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/uploads/confirm \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data_source_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/uploads/confirm",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "data_source_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/uploads/confirm", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "data_source_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }),
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

| Status | Code | When |
| --- | --- | --- |
| `401` | `invalid_api_key` | Missing, malformed, or revoked API key. |
| `403` | `insufficient_scope` | The key is valid but lacks a scope this endpoint requires. |
| `409` | `conflict` | A conflicting change, or an `Idempotency-Key` reused with a different body. |
| `429` | `rate_limited` | Per-key or per-org rate limit exceeded — back off with jitter and retry. |


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Identifier of the background job validating and ingesting the file. |

**Example response**

```json
{
  "job_id": "string"
}
```

## Presign

`POST /api/v1/uploads/presign`

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `content_type` | string | no | MIME type of the file (e.g. application/pdf). |
| `name` | string | yes | Original filename of the file to upload. |
| `size_bytes` | integer | no | Size of the file in bytes, if known. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/uploads/presign \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "string"
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/uploads/presign",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "name": "string"
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/uploads/presign", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "string"
  }),
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

| Status | Code | When |
| --- | --- | --- |
| `401` | `invalid_api_key` | Missing, malformed, or revoked API key. |
| `403` | `insufficient_scope` | The key is valid but lacks a scope this endpoint requires. |
| `409` | `conflict` | A conflicting change, or an `Idempotency-Key` reused with a different body. |
| `429` | `rate_limited` | Per-key or per-org rate limit exceeded — back off with jitter and retry. |


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `data_source_id` | string | Identifier of the data source record; pass it to /uploads/confirm. |
| `expires_in` | integer | Seconds until the presigned URL expires. |
| `fields` | object | Form fields to include for a multipart POST upload; null if none. |
| `headers` | object | Headers that must be sent with the upload request; null if none. |
| `method` | string | HTTP method to use against ``url`` (typically PUT or POST). |
| `url` | string | Presigned URL the client uploads the file bytes to. |

**Example response**

```json
{
  "data_source_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "expires_in": 0,
  "fields": {},
  "headers": {},
  "method": "string",
  "url": "string"
}
```
