> ## Documentation Index
> Fetch the complete documentation index at: https://formhug.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Prepare a form-element attachment upload

> Prepare a 2-step upload for a form-element image. `purpose` selects the kind:
`header_image` / `wallpaper_image` (theme images), `rich_text_image`, or a
field-element image (`choice_image`, `product_image`, `booking_image`).
Returns a one-time `upload_id` and an `upload_url`.

Send the file to `upload_url` with an HTTP `PUT`: the raw file bytes as the
request body and the `Content-Type` header set to the value you declared in
`content_type` (it must match). Use the `upload_url` exactly as returned.

Then consume the `upload_id`: theme images go into a theme update
(`header.images[].upload_id` / `wallpaper.image.upload_id`); `rich_text_image`
is finalized via POST /api/v1/attachment_commitments; field-element images go
into the form create / update `fields` payload (`choices[].image.upload_id`,
`goods_items[].images[].upload_id`, `reservation_items[].images[].upload_id`).
No PAT scope required.




## OpenAPI

````yaml /api-reference/openapi.yaml post /api/v1/attachments
openapi: 3.0.1
info:
  title: FormHug API V1
  version: v1
  description: |
    FormHug public REST API. All request/response keys are snake_case.

    Successful response:

    ```json
    {
      "data": { ... }
    }
    ```

    Paginated response:

    ```json
    {
      "data": [ ... ],
      "pagination": {
        "total": 123,
        "next_cursor": "NQ=="
      }
    }
    ```

    Error response:

    ```json
    {
      "error": "Human-readable message",
      "error_details": [
        { "attribute": "field_name", "message": "specific error" }
      ]
    }
    ```

    `error_details` only appears for model validation failures.
servers:
  - url: https://formhug.ai
security: []
tags:
  - name: Forms
    description: Form CRUD
  - name: Attachments
    description: >-
      2-step upload for form-element images (theme, rich-text, or field-element
      images). Prepare returns an `upload_id`; consume it in a theme update,
      attachment_commitments, or the form `fields` payload depending on purpose.
  - name: Entries
    description: Entries collected by a form owned by the current user
  - name: Folders
    description: Folder CRUD
  - name: Participated Forms
    description: Forms the current user has submitted to (does not own)
  - name: Published Form Entries
    description: Submit entries to a published form
  - name: Published Forms
    description: Read a published form's public structure for filling
  - name: Linked Form Options
    description: >-
      Discover candidate entries (and their `entry_token`s) for a `linked_form`
      field while filling a published form
  - name: Entry Attachments
    description: >-
      2-step upload for `attachment` field submissions. Prepare returns an
      `upload_id`; submit it under the attachment field in `field_values` when
      creating the entry.
  - name: Me
    description: Current authenticated user
  - name: Webhooks
    description: Webhook integrations attached to a form
  - name: Form Themes
    description: Read and update a form's visual theme
  - name: Form Settings
    description: >-
      Per-form settings — submission flow, availability, access control,
      presentation
  - name: Field Rules
    description: >-
      Read and replace a form's conditional field-display and post-submission
      redirect rules.
  - name: Form Folder
    description: Per-user folder placement for a shared form
  - name: OAuth
    description: OAuth 2.0 PKCE token issuance and revocation
paths:
  /api/v1/attachments:
    post:
      tags:
        - Attachments
      summary: Prepare a form-element attachment upload
      description: >
        Prepare a 2-step upload for a form-element image. `purpose` selects the
        kind:

        `header_image` / `wallpaper_image` (theme images), `rich_text_image`, or
        a

        field-element image (`choice_image`, `product_image`, `booking_image`).

        Returns a one-time `upload_id` and an `upload_url`.


        Send the file to `upload_url` with an HTTP `PUT`: the raw file bytes as
        the

        request body and the `Content-Type` header set to the value you declared
        in

        `content_type` (it must match). Use the `upload_url` exactly as
        returned.


        Then consume the `upload_id`: theme images go into a theme update

        (`header.images[].upload_id` / `wallpaper.image.upload_id`);
        `rich_text_image`

        is finalized via POST /api/v1/attachment_commitments; field-element
        images go

        into the form create / update `fields` payload
        (`choices[].image.upload_id`,

        `goods_items[].images[].upload_id`,
        `reservation_items[].images[].upload_id`).

        No PAT scope required.
      parameters: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AttachmentUploadInitRequest'
      responses:
        '200':
          description: Upload credentials issued
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/AttachmentUploadInit'
        '400':
          description: Disallowed content type
          content:
            application/json:
              examples:
                invalid_content_type:
                  value:
                    error: Content type "text/html" is not allowed
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '413':
          description: File exceeds 5 MB
          content:
            application/json:
              examples:
                too_large:
                  value:
                    error: File too large (max 5 MB)
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: Invalid purpose (not one of the allowed values)
          content:
            application/json:
              examples:
                invalid_purpose:
                  value:
                    error: Invalid field input
                    error_details:
                      - attribute: purpose
                        message: >-
                          must be one of: header_image, wallpaper_image,
                          rich_text_image, choice_image, product_image,
                          booking_image
              schema:
                $ref: '#/components/schemas/ValidationErrorEnvelope'
      security:
        - PersonalAccessToken: []
components:
  schemas:
    AttachmentUploadInitRequest:
      type: object
      required:
        - purpose
        - file_name
        - content_type
        - file_size
      additionalProperties: false
      properties:
        purpose:
          type: string
          enum:
            - header_image
            - wallpaper_image
            - rich_text_image
            - choice_image
            - product_image
            - booking_image
          description: >-
            What this attachment is for. `header_image` / `wallpaper_image` are
            theme images consumed by a theme update; `rich_text_image` is
            finalized via POST /api/v1/attachment_commitments; `choice_image` /
            `product_image` / `booking_image` are field-element images
            referenced from the form create / update `fields` payload.
        file_name:
          type: string
          description: Original file name (with extension).
          example: cover.png
        content_type:
          type: string
          description: >-
            MIME type. Must be one of image/jpeg, image/png, image/webp,
            image/gif. Pinned into the upload URL — the PUT must send this exact
            Content-Type.
          example: image/png
        file_size:
          type: integer
          description: File size in bytes. Must be ≤ 5 MB.
          example: 123456
    AttachmentUploadInit:
      type: object
      required:
        - upload_id
        - upload_url
        - expires_at
        - purpose
      description: >-
        Prepare-step response for the 2-step form-element attachment upload.
        Send the file to `upload_url` with an HTTP PUT: the raw file bytes as
        the request body and the `Content-Type` header set to the value you
        declared in `content_type` (it must match). Use `upload_url` exactly as
        returned. Then consume the `upload_id` per its purpose: theme images in
        a theme update; `rich_text_image` via POST
        /api/v1/attachment_commitments; field-element images (`choice_image` /
        `product_image` / `booking_image`) inside the form create / update
        `fields` payload.
      properties:
        upload_id:
          type: string
          description: >-
            One-time id. Reference it as `header.images[].upload_id` or
            `wallpaper.image.upload_id`.
        upload_url:
          type: string
          format: uri
          description: >-
            URL to PUT the file bytes to (Content-Type matching the declared
            value). Use it exactly as returned.
        expires_at:
          type: string
          format: date-time
          description: When the upload credential expires (UTC, ISO 8601).
        purpose:
          type: string
          enum:
            - header_image
            - wallpaper_image
            - rich_text_image
            - choice_image
            - product_image
            - booking_image
          description: Echo of the requested purpose.
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Error message
    ValidationErrorEnvelope:
      type: object
      required:
        - error
        - error_details
      description: >-
        Returned on model validation failures. error_details lists per-field
        errors.
      properties:
        error:
          type: string
          example: Name can't be blank
        error_details:
          type: array
          items:
            type: object
            required:
              - attribute
              - message
            properties:
              attribute:
                type: string
                example: name
              message:
                type: string
                example: can't be blank
  securitySchemes:
    PersonalAccessToken:
      type: http
      scheme: bearer
      bearerFormat: PAT
      description: >
        Personal Access Token prefixed with `fh_`. Sent as `Authorization:
        Bearer fh_xxx`.

        The scope required by each endpoint is listed in that endpoint's
        description.

````