# Upload a file

`POST /v1/leads/{lead_id}/files`

Adds one file to a file or image custom field of the lead's pipeline card.

**Scope:** `leads:write`

**Access:** The key's member must also be allowed to see files.

The body is `multipart/form-data` with exactly one part: the file, in a part named `file`, sent as a file (with a filename). Let your HTTP client set the `Content-Type` and its boundary.

The type is recognized from the file's content (its magic bytes): PDF, JPEG, PNG, WebP, HEIC or HEIF. The part's `Content-Type` and the file name's extension are ignored, so a renamed file of another type is rejected with `415`. Image fields take images only, unless your team set the field to also accept files. The stored name is the part's filename without any directory or control characters, shortened to 200 characters (keeping its extension); without a usable name the file is called `upload.<ext>`, for example `upload.pdf`.

The file goes to the lead's main card in its campaign (never a linked card on another team's pipeline). A follow-through card shares its main card's files, as in DialEcho, so a member who sees the lead only through a follow-through card uploads to those same files. A field can hold several files and an upload never replaces one: to replace a file, upload the new one, then delete the old one. The file records the API key (by its prefix) as its uploader.

Retries are safe with the same `Idempotency-Key`: the same file (same lead, field, file name and content) answers `201` with the same `file_id` and stores nothing new. Using the key for anything else is `409 idempotency_conflict`. Create a new key for every new upload.

## Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | Yes | One key per upload, reused on its retries. 16 to 128 printable ASCII characters, no spaces (a UUID works). |

## Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `lead_id` | `UUID` | Yes | The lead. |

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `field` | `string` | Yes | Slug of a file or image custom field of the lead's campaign. Any other slug (unknown, deleted, another type or another campaign's field) is `422 unknown_field`. |

## Body

**Content type:** `multipart/form-data`

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | `file` | Yes | The document or photo, sent as a file part with a filename. Empty files are rejected. Up to 10 MB (10,485,760 bytes). PDF, JPEG, PNG, WebP, HEIC or HEIF. |

## Response

**Status:** `201 Created`

| Name | Type | Description |
| --- | --- | --- |
| `file_id` | `UUID` | The stored file. Keep it to delete the file later. |
| `lead_id` | `UUID` | The lead. |
| `field` | `string` | The custom field slug. |
| `filename` | `string` | The name the file was stored under. |
| `content_type` | `string` | The type DialEcho recognized, for example `application/pdf`. |
| `size_bytes` | `integer` | Size of the stored file. |

## Example request

**curl**

```bash
curl -X POST "https://development.agencydevworks.ai/v1/leads/aaaaaaaa-0000-4000-8000-000000000001/files?field=utility_bill" \
  -H "Authorization: Bearer dei_your_key" \
  -H "Idempotency-Key: 0f0f0f0f-0000-4000-8000-00000000abcd" \
  -F "file=@utility-bill.pdf"
```

**JavaScript**

```js
import { openAsBlob } from "node:fs";

const form = new FormData();
form.append("file", await openAsBlob("utility-bill.pdf"), "utility-bill.pdf");

const response = await fetch("https://development.agencydevworks.ai/v1/leads/aaaaaaaa-0000-4000-8000-000000000001/files?field=utility_bill", {
  method: "POST",
  headers: {
    "Authorization": "Bearer dei_your_key",
    "Idempotency-Key": "0f0f0f0f-0000-4000-8000-00000000abcd",
  },
  body: form,
});
const data = await response.json();
```

## Example response

**Status:** `201 Created`

```json
{
  "file_id": "55555555-0000-4000-8000-000000000001",
  "lead_id": "aaaaaaaa-0000-4000-8000-000000000001",
  "field": "utility_bill",
  "filename": "utility-bill.pdf",
  "content_type": "application/pdf",
  "size_bytes": 482133
}
```

## Errors

| HTTP | Code | When |
| --- | --- | --- |
| 403 | `insufficient_scope` | The key does not have `leads:write`. |
| 403 | `permission_denied` | The key's member may not see files. Only for a lead they can see; any other lead is `404`. |
| 404 | `not_found` | The lead does not exist or the key cannot reach it. |
| 409 | `no_pipeline_card` | The lead has no pipeline card. Nothing was stored. |
| 409 | `idempotency_conflict` | The `Idempotency-Key` was already used for a different upload. Nothing was stored. |
| 413 | `file_too_large` | The file is larger than 10 MB. Nothing was stored. |
| 415 | `unsupported_media_type` | The content is not a PDF, JPEG, PNG, WebP, HEIC or HEIF file, or is a PDF sent to an image-only field. Nothing was stored. |
| 422 | `unknown_field` | `field` is not a file or image custom field of the lead's campaign. |
| 422 | `invalid_request` | `lead_id` is not a UUID, `field` or `Idempotency-Key` is missing or malformed, or the file is empty or the body is not exactly one file part named `file` (these two carry an empty `error.fields`). |
| 503 | `dependency_unavailable` | The file could not be stored right now. Retry with the same `Idempotency-Key`. |

Every endpoint can also answer `401 invalid_api_key`, `429 rate_limited` and `503 dependency_unavailable`. See [Errors](/api/#errors) in the overview.

## Related endpoints

- `DELETE /v1/leads/{lead_id}/files/{file_id}` · [Delete a file](/api/files/delete)
- `POST /v1/leads` · [Create or update a lead](/api/leads/create)
