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

# Iniciar upload: obtener signed URL

> Paso 1 del patrón 2-step. Valida mime_type + size_bytes + cuota del business, reserva una fila pending en business_files y devuelve un signed URL con TTL de 5 minutos.

## Flujo completo

### Paso 1 — Este endpoint

Declara la metadata del archivo y recibe el signed URL.

### Paso 2 — PUT directo a Supabase (NO es un endpoint nuestro)

El cliente sube el binario directo a Storage. El server nunca ve el binario. El `Content-Type` del PUT debe estar en el catálogo permitido del bucket (si no, Storage responde 400). En `/confirm` el server lee los magic bytes del blob ya subido y los compara contra el `mime_type` declarado en el paso 1 (persistido en DB): si los magic bytes no corresponden a ese `mime_type`, la confirmación falla con `MIME_MISMATCH`.

**curl:**
```bash
curl -X PUT "<upload_url del paso 1>" \
  -H "Content-Type: application/pdf" \
  --data-binary "@./invoice.pdf"
```

**JavaScript (Node 20+ / browser):**
```javascript
await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/pdf' },
  body: file, // File object del input o ReadStream
})
```

**n8n:** nodo HTTP Request con method PUT, URL del paso 1, Body Type "Binary", field del nodo "Read Binary File" anterior.

### Paso 3 — POST /files/confirm

Tras completar el PUT, llama a `/files/confirm` con el `file_uuid` para validar el binario y recibir un signed URL de descarga usable en mensajes.

## Cómo obtener mime_type y size_bytes

La metadata debe declararse antes de subir. Si la herramienta no la expone:

- **JS File API:** `file.type` y `file.size` vienen gratis en un objeto `File`.
- **Node fs:** `fs.statSync(path).size` para bytes; `mime-types` package para el MIME.
- **curl:** `stat -c%s file` (Linux) / `stat -f%z file` (macOS) / `(Get-Item file).Length` (PowerShell) para bytes. `file --mime-type -b file` para MIME.
- **n8n:** el nodo "Read Binary File" expone `binary.data.mimeType` y `binary.data.fileSize`.



## OpenAPI

````yaml /openapi.json post /{slug}/files/request-upload
openapi: 3.1.0
info:
  title: 1TO1 AI Public API
  version: 0.2.0
  description: >-
    API pública de 1TO1 AI para integraciones externas (n8n, CRMs, scripts).
    Auth via Bearer token. Rate limit 60 req/min por API key. Contrato REST
    puro: el body de éxito es el payload directo (status HTTP como señal),
    errores retornan `{code, message}` con ambos campos garantizados. Ver schema
    `PublicError`.
servers:
  - url: https://app.1to1.ai/api/v1/public
    description: Producción
security:
  - bearerAuth: []
paths:
  /{slug}/files/request-upload:
    post:
      tags:
        - Files
      summary: 'Iniciar upload: obtener signed URL'
      description: >-
        Paso 1 del patrón 2-step. Valida mime_type + size_bytes + cuota del
        business, reserva una fila pending en business_files y devuelve un
        signed URL con TTL de 5 minutos.


        ## Flujo completo


        ### Paso 1 — Este endpoint


        Declara la metadata del archivo y recibe el signed URL.


        ### Paso 2 — PUT directo a Supabase (NO es un endpoint nuestro)


        El cliente sube el binario directo a Storage. El server nunca ve el
        binario. El `Content-Type` del PUT debe estar en el catálogo permitido
        del bucket (si no, Storage responde 400). En `/confirm` el server lee
        los magic bytes del blob ya subido y los compara contra el `mime_type`
        declarado en el paso 1 (persistido en DB): si los magic bytes no
        corresponden a ese `mime_type`, la confirmación falla con
        `MIME_MISMATCH`.


        **curl:**

        ```bash

        curl -X PUT "<upload_url del paso 1>" \
          -H "Content-Type: application/pdf" \
          --data-binary "@./invoice.pdf"
        ```


        **JavaScript (Node 20+ / browser):**

        ```javascript

        await fetch(uploadUrl, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/pdf' },
          body: file, // File object del input o ReadStream
        })

        ```


        **n8n:** nodo HTTP Request con method PUT, URL del paso 1, Body Type
        "Binary", field del nodo "Read Binary File" anterior.


        ### Paso 3 — POST /files/confirm


        Tras completar el PUT, llama a `/files/confirm` con el `file_uuid` para
        validar el binario y recibir un signed URL de descarga usable en
        mensajes.


        ## Cómo obtener mime_type y size_bytes


        La metadata debe declararse antes de subir. Si la herramienta no la
        expone:


        - **JS File API:** `file.type` y `file.size` vienen gratis en un objeto
        `File`.

        - **Node fs:** `fs.statSync(path).size` para bytes; `mime-types` package
        para el MIME.

        - **curl:** `stat -c%s file` (Linux) / `stat -f%z file` (macOS) /
        `(Get-Item file).Length` (PowerShell) para bytes. `file --mime-type -b
        file` para MIME.

        - **n8n:** el nodo "Read Binary File" expone `binary.data.mimeType` y
        `binary.data.fileSize`.
      parameters:
        - $ref: '#/components/parameters/SlugPathParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestUploadBody'
      responses:
        '201':
          description: >-
            Upload URL emitido. Continúa con PUT a `upload_url` y luego POST
            /files/confirm.
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
                description: >-
                  ID único para correlacionar con logs server-side. Si el
                  cliente envía uno válido (UUID), se preserva; si no, se genera
                  uno nuevo.
              required: true
              description: >-
                ID único para correlacionar con logs server-side. Si el cliente
                envía uno válido (UUID), se preserva; si no, se genera uno
                nuevo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RequestUploadResponse'
        '400':
          description: >-
            Body inválido (INVALID_REQUEST), MIME type no permitido
            (INVALID_FILE_TYPE) o tamaño excede el límite de la categoría
            (FILE_TOO_LARGE).
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
                description: >-
                  ID único para correlacionar con logs server-side. Si el
                  cliente envía uno válido (UUID), se preserva; si no, se genera
                  uno nuevo.
              required: true
              description: >-
                ID único para correlacionar con logs server-side. Si el cliente
                envía uno válido (UUID), se preserva; si no, se genera uno
                nuevo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Token ausente, mal formado o no válido (INVALID_API_TOKEN)
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
                description: >-
                  ID único para correlacionar con logs server-side. Si el
                  cliente envía uno válido (UUID), se preserva; si no, se genera
                  uno nuevo.
              required: true
              description: >-
                ID único para correlacionar con logs server-side. Si el cliente
                envía uno válido (UUID), se preserva; si no, se genera uno
                nuevo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: >-
            El slug del path no coincide con el business del token
            (TOKEN_BUSINESS_MISMATCH)
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
                description: >-
                  ID único para correlacionar con logs server-side. Si el
                  cliente envía uno válido (UUID), se preserva; si no, se genera
                  uno nuevo.
              required: true
              description: >-
                ID único para correlacionar con logs server-side. Si el cliente
                envía uno válido (UUID), se preserva; si no, se genera uno
                nuevo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '413':
          description: Cuota de storage del business excedida (STORAGE_QUOTA_EXCEEDED).
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
                description: >-
                  ID único para correlacionar con logs server-side. Si el
                  cliente envía uno válido (UUID), se preserva; si no, se genera
                  uno nuevo.
              required: true
              description: >-
                ID único para correlacionar con logs server-side. Si el cliente
                envía uno válido (UUID), se preserva; si no, se genera uno
                nuevo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: >-
            Rate limit excedido (RATE_LIMIT_EXCEEDED). Headers `Retry-After`,
            `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
            indican cuándo reintentar.
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
                description: >-
                  ID único para correlacionar con logs server-side. Si el
                  cliente envía uno válido (UUID), se preserva; si no, se genera
                  uno nuevo.
              required: true
              description: >-
                ID único para correlacionar con logs server-side. Si el cliente
                envía uno válido (UUID), se preserva; si no, se genera uno
                nuevo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '500':
          description: Error inesperado del servidor (UNKNOWN_ERROR)
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
                description: >-
                  ID único para correlacionar con logs server-side. Si el
                  cliente envía uno válido (UUID), se preserva; si no, se genera
                  uno nuevo.
              required: true
              description: >-
                ID único para correlacionar con logs server-side. Si el cliente
                envía uno válido (UUID), se preserva; si no, se genera uno
                nuevo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '507':
          description: >-
            Storage lleno: la limpieza automática no pudo liberar suficiente
            espacio para este upload (STORAGE_QUOTA_CLEANUP_INSUFFICIENT).
            Libera espacio o sube de plan.
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
                description: >-
                  ID único para correlacionar con logs server-side. Si el
                  cliente envía uno válido (UUID), se preserva; si no, se genera
                  uno nuevo.
              required: true
              description: >-
                ID único para correlacionar con logs server-side. Si el cliente
                envía uno válido (UUID), se preserva; si no, se genera uno
                nuevo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
components:
  parameters:
    SlugPathParam:
      schema:
        $ref: '#/components/schemas/SlugPathParam'
      required: true
      description: >-
        Slug del business, case-insensitive. Debe coincidir con el business del
        token.
      name: slug
      in: path
  schemas:
    RequestUploadBody:
      type: object
      properties:
        filename:
          type: string
          minLength: 1
          maxLength: 255
          example: invoice.pdf
          description: >-
            Nombre original del archivo. Se preserva para mostrar al receptor
            final.
        mime_type:
          type: string
          enum:
            - image/jpeg
            - image/png
            - video/mp4
            - video/3gpp
            - audio/ogg
            - application/pdf
            - application/msword
            - >-
              application/vnd.openxmlformats-officedocument.wordprocessingml.document
            - application/vnd.ms-excel
            - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
            - application/vnd.ms-powerpoint
            - >-
              application/vnd.openxmlformats-officedocument.presentationml.presentation
            - text/plain
          example: application/pdf
          description: >-
            MIME type del archivo. Stickers (image/webp) no se soportan en la
            API v1 — WhatsApp los maneja como tipo de mensaje aparte.
        size_bytes:
          type: integer
          minimum: 1
          maximum: 104857600
          example: 524288
          description: Tamaño declarado en bytes. Se re-valida en /confirm.
      required:
        - filename
        - mime_type
        - size_bytes
    RequestUploadResponse:
      type: object
      properties:
        file_uuid:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
          description: >-
            UUID generado por el server. Se usa en /confirm y al adjuntar en
            mensajes.
        upload_url:
          type: string
          format: uri
          description: >-
            Signed URL para hacer PUT con el binario directo a Supabase Storage.
            Expira en 5 minutos.
        upload_token:
          type: string
          description: >-
            Token firmado opcional si el cliente usa el SDK de Supabase en vez
            de un PUT raw.
        storage_path:
          type: string
          description: >-
            Path interno en el bucket. Informativo — el cliente solo debe usar
            upload_url.
        expires_at:
          type: string
          format: date-time
          example: '2026-04-19T12:05:00.000Z'
          description: Instante (UTC) en que upload_url deja de ser válido.
      required:
        - file_uuid
        - upload_url
        - upload_token
        - storage_path
        - expires_at
    PublicError:
      type: object
      properties:
        code:
          type: string
          example: INVALID_API_TOKEN
        message:
          type: string
          example: >-
            The provided API token is invalid or has been revoked. Generate a
            new one in Settings → API Token.
      required:
        - code
        - message
      description: >-
        Public API error body. The client should match against `code` (stable
        canonical identifier) and NOT against `message` (human text, may be
        refined between API versions without notice). The HTTP status code is
        the error-type signal (4xx client, 5xx server).
    SlugPathParam:
      type: string
      example: acme
      description: >-
        Slug del business, case-insensitive. Debe coincidir con el business del
        token.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_1to1_<hex>
      description: >-
        API token emitido desde Settings → API Token del dashboard 1TO1 AI.
        Enviar en header Authorization: Bearer sk_1to1_...

````