# Create upload (`create_upload`)

Declares a file the caller intends to upload — its purpose, filename, content type and byte size — and returns a presigned target to PUT the bytes to, the headers that must accompany them, and an upload id. Nothing in the store changes until attach_customiser_asset binds the uploaded object to a slot; an upload that is never attached expires and is pruned. The declared purpose fixes the allowed content types and the maximum size, so a mismatch is caught before the bytes are sent. It requires the customisers:write scope and an idempotency_key, and supports dry_run.

- Source URL: https://www.signcustomiser.com/help/mcp/tools/create_upload/
- Markdown URL: https://www.signcustomiser.com/help/mcp/tools/create_upload.md
- MCP endpoint: https://web.signcustomiser.com/mcp
- Required scope: `customisers:write`
- Behaviour: write

## Purpose

Reserve a staging target for one file, before attaching it to an option image, a font, a backboard shape or the storefront product image.

## Annotations

| Hint | Value |
| --- | --- |
| readOnlyHint | false |
| destructiveHint | false |
| idempotentHint | true |
| openWorldHint | false |

## Prerequisites

- The file purpose, its filename, its content type and its exact byte size.
- A merchant has connected this store and approved the customisers:write scope.

## Side effects

- Creates one staged upload record and mints a presigned target that expires.
- Nothing in the store configuration changes until attach_customiser_asset runs.
- Claims the idempotency_key for at least 24 hours.

## Arguments

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `purpose` | string | yes | What the file is for. The purpose fixes the accepted content types and the byte cap, and the slot it can later be attached to. Expected to grow: tolerate unknown values. One of: `font_ttf`, `option_image`, `option_texture_image`, `backboard_shape_svg`, `label_image`. |
| `filename` | string | yes | The original filename. Its extension is checked against the purpose; the stored object name is chosen by the server. |
| `content_type` | string | yes | The file media type, such as font/ttf, image/png or image/svg+xml. Both this and the filename extension must match the purpose. |
| `byte_size` | integer | yes | The exact size of the bytes that will be sent. The staged object is refused at attach time if what arrives is a different size. |
| `idempotency_key` | string | no | A client-generated key unique to this logical write, such as a UUID. Required: repeating the call with the same key and the same arguments replays the original result instead of writing twice, and the same key with different arguments is an idempotency_key_conflict. A dry run claims no key. |
| `dry_run` | boolean | no | When true, the declaration is checked against the purpose limits is checked and nothing is written: the result is the same verdict the apply would have reached, and a problem the apply would have raised comes back as the same tool error. Defaults to false. |

## Result

The staged upload record and the presigned target to PUT the bytes to, with the headers the PUT must carry. The target is returned once and cannot be re-read. A dry run returns the create verdict and mints nothing.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `request_id` | string | yes |  |
| `store` | object | yes |  |
| `store.id` | integer | yes |  |
| `store.name` | string | yes |  |
| `upload` | object | no | The staged upload. Its id is what attach_customiser_asset takes. Absent on a dry run. |
| `upload.id` | string | yes |  |
| `upload.purpose` | string | no | One of: `font_ttf`, `option_image`, `option_texture_image`, `backboard_shape_svg`, `label_image`. |
| `upload.filename` | string or null | no |  |
| `upload.content_type` | string or null | no |  |
| `upload.byte_size` | integer or null | no |  |
| `upload.status` | string | no | One of: `pending`, `uploaded`, `attached`, `rejected`, `expired`. |
| `upload.verification_failure_code` | string or null | no |  |
| `upload.expires_at` | string or null | no |  |
| `upload.attached_at` | string or null | no |  |
| `upload.created_at` | string or null | no |  |
| `upload_target` | object | no | Where to PUT the bytes, and the headers the PUT must carry. Returned only by this tool, only once, and never by get_upload. |
| `upload_target.method` | string | yes |  |
| `upload_target.url` | string | yes |  |
| `upload_target.headers` | object | no |  |
| `upload_target.expires_at` | string or null | no |  |
| `idempotent_replay` | boolean | no | True when this call replayed an earlier declaration carrying the same idempotency_key rather than minting a second upload. |
| `dry_run` | boolean | no | Present and true only when dry_run was requested. |
| `valid` | boolean | no | Present only on a dry run, and always true: a failing dry run returns a tool error instead. |
| `verdict` | object | no | Present only on a dry run: the create verdict. |

## Error cases

| Code | Recovery |
| --- | --- |
| `insufficient_scope` | The connection was approved without customisers:write. |
| `missing_idempotency_key` | Send idempotency_key. It is required for this write. |
| `unsupported_value` | The content type or the filename extension is not one this purpose accepts; allowed_values lists the ones it does. |
| `upload_size_exceeded` | byte_size is above the purpose maximum, which the problem reports as max_byte_size. |

## Example: Stage a TrueType font file

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create_upload",
    "arguments": {
      "purpose": "font_ttf",
      "filename": "Signature.ttf",
      "content_type": "font/ttf",
      "byte_size": 184320,
      "idempotency_key": "c3d4e5f6-7081-4b9c-8d0e-1f2a3b4c5d6e"
    }
  }
}
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "structuredContent": {
      "request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t",
      "store": {
        "id": 12,
        "name": "Demo Signs"
      },
      "upload": {
        "id": "upl_9f2c4b7ad13e6058ba41cd",
        "purpose": "font_ttf",
        "filename": "Signature.ttf",
        "content_type": "font/ttf",
        "byte_size": 184320,
        "status": "pending",
        "verification_failure_code": null,
        "expires_at": "2026-07-23T00:00:00Z",
        "attached_at": null,
        "created_at": "2026-07-22T00:00:00Z"
      },
      "upload_target": {
        "method": "PUT",
        "url": "https://uploads.example.invalid/staged-upload-target",
        "headers": [],
        "expires_at": "2026-07-22T00:10:00Z"
      },
      "idempotent_replay": false
    }
  }
}
```

## Example: A content type the purpose does not accept (error)

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create_upload",
    "arguments": {
      "purpose": "font_ttf",
      "filename": "Signature.otf",
      "content_type": "font/otf",
      "byte_size": 184320,
      "idempotency_key": "c3d4e5f6-7081-4b9c-8d0e-1f2a3b4c5d6e"
    }
  }
}
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "isError": true,
    "structuredContent": {
      "error": {
        "code": "unsupported_value",
        "title": "Unsupported value",
        "detail": "The font_ttf purpose does not accept the content type font/otf.",
        "recovery": "Send one of the values in allowed_values for the field named by pointer or parameter.",
        "pointer": "/content_type",
        "allowed_values": [
          "font/ttf"
        ]
      }
    }
  }
}
```
