# POST /api/v2/drive-items/{drive_item}/uploads

Start a file upload on a drive item

- MCP id: drive-items.uploads.store
- Scope: api:write:project_drive
- Scope: api:write:customer_drive
- Scope: api:write:organization_drive
- Scope: api:write:user_drive
- Permission: project_drive::create
- Rate cost: 1 unit, write bucket. A write costs the same whatever `view` you ask for.
- Cache-Control: private, no-store

Step 1 of 3.

Returns an upload ticket whose `upload_url` you PUT the raw bytes to, after which you POST the ticket's id as `upload_id` to `drive-items/{drive_item}/files` to turn those bytes into a file. The bytes never travel through this API: `/api/v2` accepts `application/json` and nothing else, so the upload target is a separate signed route. The file always lands in the document's `attachments` collection (`content` belongs to the editor and `file` is the document's own single payload, so neither is writable here), and the collection, the storage location and the ticket lifetime are all chosen by the server. The declared size is not a bound: the server counts what arrives and refuses anything over 10 MB. The declared permission is `project_drive::create`; `customer_drive`, `organization_drive` and `user_drive` apply by owner type, and the record rule requires edit access: read access to a document is not enough to attach to it, which is deliberately stricter than plnnrly. An archived document is `404`. The ticket is yours alone, is single-use, expires in 30 minutes, and can be deleted with `DELETE uploads/{upload}` while unregistered.

## Parameters
- drive_item (integer, path): The DriveItem's numeric id. Accepts a soft-deleted record.
- view (string, optional): Which field set to render.
- X-Request-Id (header, optional): A correlation id of your own. It is recorded in our logs next to the server's request id but never returned: the response header always carries the server's id. A value that does not match the pattern is discarded silently, never rejected.
- Idempotency-Key (header, optional): A caller-chosen key that makes this POST safe to retry. Replaying it returns the original response with Idempotency-Replayed: true.

## Request body
- file_name (string ≤ 255, required): The sanitized file name the file will carry. Sent when the ticket is created; control characters are stripped and `#`, `/`, `\` and spaces become hyphens, so what you read back can differ from what you sent.
- byte_size (integer 1 to 10485760, required): The size you declared when creating the ticket. A declaration, not a bound: the server counts the bytes it actually receives.
- checksum_sha256 (string or null ≤ 64, optional): The optional SHA-256 you declared when creating the ticket. When present the server refuses bytes that do not match it.

Unknown keys are refused with `422` validation-failed, naming the key.

## Response 201
- id (integer, required): Unique identifier. Send this as `upload_id` when you register the bytes.
- work_item_id (integer or null, required): The work item this ticket attaches to, or `null` when it targets a drive item.
- drive_item_id (integer or null, required): The drive item this ticket attaches to, or `null` when it targets a work item.
- collection (string ≤ 255, required): The media collection the file will land in. Always `attachments`: it is chosen by the route, and no request field can change it.
- file_name (string ≤ 255, required): The sanitized file name the file will carry. Sent when the ticket is created; control characters are stripped and `#`, `/`, `\` and spaces become hyphens, so what you read back can differ from what you sent.
- byte_size (integer, required): The size you declared when creating the ticket. A declaration, not a bound: the server counts the bytes it actually receives.
- checksum_sha256 (string or null, required): The optional SHA-256 you declared when creating the ticket. When present the server refuses bytes that do not match it.
- received_byte_size (integer or null, required): The number of bytes that actually arrived, measured by the server. Null until the upload lands.
- received_checksum_sha256 (string or null, required): The SHA-256 the server computed over the bytes it received. Null until the upload lands.
- mime_type (string or null ≤ 255, required): The media type detected from the received bytes. Detected, never declared: the API accepts no content type when creating the ticket and ignores the `Content-Type` of the upload itself.
- status (string, required): Where the ticket is in its lifecycle.
- upload_url (string or null, required): Where to PUT the file. An opaque absolute URL: send the raw file as the body of a PUT whose only header is your Authorization bearer token. No `Content-Type` is read and the body is stored exactly as sent, so a form envelope would become part of the file. Present only while status is `pending`.
- expires_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): The deadline for the PUT. After it the ticket is dead and a new one must be created.
- register_by (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required): The deadline for the registration, set when the bytes land and measured from that moment, so a slow upload does not eat into the time you have to register. Null until the upload lands.
- uploaded_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required): When the bytes arrived. Null until they do.
- committed_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required): When the bytes became a file. Null until they do.
- media_id (integer or null, required): The id of the file this ticket became. Null until it is registered, and set exactly once.
- created_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): When the ticket was created.
- updated_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): Last-write timestamp; use for incremental sync.
- links (UploadLinks, required)
  - self (url, required): The canonical URI of this record.

## Errors
- 403 forbidden-by-policy | insufficient-permission | insufficient-token-scope: Forbidden.
- 409 idempotency-key-in-flight | idempotency-key-reused | state-conflict: Conflict.
- 415 unsupported-media-type: Unsupported media type.
- 422 unprocessable-relationship | validation-failed: Unprocessable content.
- 400 idempotency-key-required | invalid-cursor | invalid-query-parameter | malformed-request | unsupported-parameter: Bad request.
- 401 token-expired | unauthenticated: Unauthenticated.
- 404 endpoint-not-found | resource-not-found: Not found.
- 406 not-acceptable: Not acceptable.
- 429 rate-limit-exceeded: Too many requests.
- 500 internal-error: Server error.
- 503 tenant-unavailable: Service unavailable.

## Example
```bash
curl -X POST \
  'https://<your-url>/api/v2/drive-items/1/uploads' \
  -H 'Authorization: Bearer <token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <key>' \
  -d '{
    "file_name": "quarterly-report.pdf",
    "byte_size": 184320
  }'
```
