# Prepare a direct attachment upload

PUT the bytes to uploadUrl without Hoplite authentication and with the exact returned contentType, which may be normalized from the filename. Then confirm the signed ticket for an existing thread. Use id=initial and initialClientOperationId for a new thread; pass the ticket in initialAttachmentTickets when creating that thread.

`POST /api/threads/{id}/attachments/presign`

Required permission: `thread:update`.

## Parameters

| Name | In | Type | Required | Description |

| --- | --- | --- | --- | --- |

| `id` | path | `string` | Yes | — |

| `projectId` | query | `string` | No | — |

| `Idempotency-Key` | header | `string` | No | Required for writes authenticated by a service credential and for service-account lifecycle writes. Other session writes may opt in. Reuse the key and payload for retries. Settled keys remain reserved for at least seven days; a transient 4xx releases its key. Changed payloads return 409. Match any legacy clientOperationId/clientMessageId in the body. |

## Request body

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `filename` | `string` | Yes | min length 1 | — |
| `contentType` | `string` | Yes | min length 1 | — |
| `byteSize` | `integer` | Yes | max 9007199254740991 | — |
| `width` | `integer` | No | max 9007199254740991 | — |
| `height` | `integer` | No | max 9007199254740991 | — |
| `initialClientOperationId` | `string` | No | — | — |

```json
{
  "filename": "requirements.txt",
  "contentType": "text/plain",
  "byteSize": 1024
}
```

## Example request

```bash
curl --request POST \
  --url https://api.hoplite.sh/api/threads/id/attachments/presign \
  --header "X-Api-Key: $HOPLITE_API_KEY" \
  --header "Idempotency-Key: $HOPLITE_OPERATION_ID" \
  --header 'Content-Type: application/json' \
  --data '{
  "filename": "requirements.txt",
  "contentType": "text/plain",
  "byteSize": 1024
}'
```

## 201 response

Successful response

Content type: `application/json`. Response type: `object`.

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `ok` | `boolean` | Yes | constant true | — |
| `upload` | `object` | Yes | — | — |
| `upload.artifactId` | `string` | Yes | — | — |
| `upload.ticket` | `string` | Yes | — | — |
| `upload.uploadUrl` | `string<uri>` | Yes | — | — |
| `upload.contentType` | `string` | Yes | — | — |

```json

```

## Response headers

| Header | Type | Statuses | Description |

| --- | --- | --- | --- |

| `RateLimit` | `string` | `201`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Current ingress policy, remaining requests, and seconds until reset, for example `"ingress";r=599;t=60`. |

| `RateLimit-Policy` | `string` | `201`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Ingress quota and window, for example `"ingress";q=600;qu="requests";w=60`. |

| `X-RateLimit-Limit` | `integer` | `201`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Maximum requests allowed in the current rolling window. |

| `X-RateLimit-Remaining` | `integer` | `201`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Requests remaining in the current rolling window. |

| `X-RateLimit-Reset` | `integer` | `201`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Unix timestamp when the current rolling window resets. |

| `x-request-id` | `string` | `201`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Request correlation ID. |

| `Retry-After` | `integer` | `429`, `503` | Seconds until another request should be attempted. |

| `X-Retry-After` | `integer` | `429`, `503` | Legacy retry delay in seconds. |

## Status codes

- `201` — Successful response

- `400` — Invalid request. Validation responses include field paths.

- `401` — The API key is missing, invalid, or lacks the operation's permission.

- `402` — The organization does not have access to the requested product capability.

- `403` — The key's current user no longer has the required organization membership or role.

- `404` — The resource is not available in the authenticated scope.

- `409` — The request conflicts with a retry receipt or current resource state.

- `410` — The event cursor has expired; capture a fresh head and resource snapshot.

- `429` — The API-key rate limit has been exceeded.

- `500` — The API encountered an unexpected failure.

- `501` — The deployment has not configured the requested capability.

- `503` — A required API dependency, including the rate limiter, is unavailable.