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

# Send a text document to your paired phone's Downloads folder (send_file)

> **Not yet enabled.** This endpoint is rolling out alongside the Android release that can receive documents — until then, calls receive a plain `404 Not Found` with no response body, indistinguishable from a route that doesn't exist.

**Requires the connector-only `files:send` OAuth scope, which a bearer (`rfk_live_…`) token can never hold.** Once this endpoint is enabled, an `rfk_live_…` token still cannot use it — it authenticates fine but is refused at the scope gate with `403 insufficient_scope`. The only live caller is Reflecto's own remote MCP `send_file` tool, which calls this same logic in-process as an OAuth connector holding `files:send`. This route is documented (rather than hidden behind a 404 once enabled) so a per-token-scopes future has somewhere to point.

Sends a text document to the paired phone on the account behind the bearer token. The server holds `content` in plaintext RAM only for the duration of this call (disclosed server-as-sender tier, same as `POST /v1/send`) — it is encrypted once to the phone's public key, parked as an opaque ciphertext blob for up to 1 hour, and the phone pulls it via an FCM-nudged `GET /v1/documents/{documentId}`, then writes it into Downloads. Nothing is retained server-side beyond that 1 hour window.

**Filenames are restricted to plain-text/code extensions** — never `.html`/`.svg`, which render and execute if tapped from Downloads (`400 invalid_filename`). `content` must be non-empty and at most 131072 bytes (`400 invalid_content` / `413 content_too_large`).

Requires a paired phone device on the account behind the bearer token (`409 no_phone_device`).

**Phone-capability gate — fail-closed, unlike the other agent-actuated tools.** If the paired phone's reported app build is too old to receive documents, the request is refused outright (`426 phone_app_outdated`). Unlike `remind_me`/`request_approval`/`find_my_phone`, a phone that has **never** reported its version is ALSO refused (`428 phone_app_unknown`, not a warning-and-proceed) — document support shipped strictly after version reporting did, so "unknown" is proof the installed build predates it, not genuine ambiguity.



## OpenAPI

````yaml /openapi.yaml post /v1/documents
openapi: 3.1.0
info:
  title: Reflecto API
  version: 1.1.0
  description: >-
    The Reflecto public HTTP API lets you push notifications to your own paired

    devices — your Android phone and any browser extensions you've paired with
    it.

    Designed for the same job as Pushover or the old Pushbullet API: a single

    HTTPS call from a script, cron job, or webhook puts a notification on every

    screen you own.


    Authentication uses opaque prefixed bearer tokens (`rfk_live_…`) created and

    managed on your paired Android device — there are no email accounts, no

    passwords, no server-side user records beyond device pairing state. See the

    Authentication guide for the token lifecycle.


    The public-API surface is small on purpose: `POST /v1/send`, its

    capability-URL alias `POST /v1/send/{token}`, and a Pushover-compatible shim

    at `POST /v1/messages.json`. Mirroring traffic from your phone to your

    extension never reaches this surface — it stays end-to-end encrypted via the

    private `/v1/sync` SSE stream.


    See the Guides tab for the quickstart, authentication model, rate-limit

    contract, and encryption posture.
  contact:
    name: Reflecto
    url: https://github.com/reflectoapp/reflecto
  license:
    name: MIT
    url: https://github.com/reflectoapp/reflecto/blob/main/LICENSE
servers:
  - url: https://api.reflecto.dev
    description: Production
security:
  - BearerAuth: []
tags:
  - name: Send
    description: Push notifications to one or all of your paired devices.
  - name: Pushover compatibility
    description: >-
      Drop-in replacement for `POST https://api.pushover.net/1/messages.json`.
      Field names, request shape, and response shape mirror Pushover's so
      existing senders can migrate by changing a single URL. See [Pushover
      compatibility guide](/guides/pushover-compatibility) for the full mapping.
paths:
  /v1/documents:
    post:
      tags:
        - Documents
      summary: Send a text document to your paired phone's Downloads folder (send_file)
      description: >-
        **Not yet enabled.** This endpoint is rolling out alongside the Android
        release that can receive documents — until then, calls receive a plain
        `404 Not Found` with no response body, indistinguishable from a route
        that doesn't exist.


        **Requires the connector-only `files:send` OAuth scope, which a bearer
        (`rfk_live_…`) token can never hold.** Once this endpoint is enabled, an
        `rfk_live_…` token still cannot use it — it authenticates fine but is
        refused at the scope gate with `403 insufficient_scope`. The only live
        caller is Reflecto's own remote MCP `send_file` tool, which calls this
        same logic in-process as an OAuth connector holding `files:send`. This
        route is documented (rather than hidden behind a 404 once enabled) so a
        per-token-scopes future has somewhere to point.


        Sends a text document to the paired phone on the account behind the
        bearer token. The server holds `content` in plaintext RAM only for the
        duration of this call (disclosed server-as-sender tier, same as `POST
        /v1/send`) — it is encrypted once to the phone's public key, parked as
        an opaque ciphertext blob for up to 1 hour, and the phone pulls it via
        an FCM-nudged `GET /v1/documents/{documentId}`, then writes it into
        Downloads. Nothing is retained server-side beyond that 1 hour window.


        **Filenames are restricted to plain-text/code extensions** — never
        `.html`/`.svg`, which render and execute if tapped from Downloads (`400
        invalid_filename`). `content` must be non-empty and at most 131072 bytes
        (`400 invalid_content` / `413 content_too_large`).


        Requires a paired phone device on the account behind the bearer token
        (`409 no_phone_device`).


        **Phone-capability gate — fail-closed, unlike the other agent-actuated
        tools.** If the paired phone's reported app build is too old to receive
        documents, the request is refused outright (`426 phone_app_outdated`).
        Unlike `remind_me`/`request_approval`/`find_my_phone`, a phone that has
        **never** reported its version is ALSO refused (`428 phone_app_unknown`,
        not a warning-and-proceed) — document support shipped strictly after
        version reporting did, so "unknown" is proof the installed build
        predates it, not genuine ambiguity.
      operationId: sendDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDocumentRequest'
            examples:
              minimal:
                summary: A short runbook
                value:
                  filename: runbook.md
                  content: |
                    # Runbook

                    1. Check the dashboard
                    2. Page on-call if red
      responses:
        '201':
          description: Created — the document was encrypted and dispatched to the phone.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateDocumentResponse'
              examples:
                created:
                  value:
                    document_id: doc_3f6e2a7c9b1d4f8e8a5b6c1d2e3f4a5b
                    byte_length: 128
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/DocumentTooLarge'
        '426':
          $ref: '#/components/responses/UpgradeRequired'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/RateLimitedInteractions'
      security:
        - BearerAuth: []
components:
  schemas:
    CreateDocumentRequest:
      type: object
      properties:
        filename:
          type: string
          description: >-
            Destination filename, written into the phone's Downloads folder.
            Must end in an allowed plain-text/code extension (never
            `.html`/`.svg` — those render and execute if tapped) and be at most
            120 characters after sanitization (rejected outright above 4096 raw
            characters). Path separators, control characters, and leading dots
            are stripped server-side and again on the phone before writing.
          example: runbook.md
        content:
          type: string
          description: UTF-8 text content of the file. Non-empty, at most 131072 bytes.
          example: |
            # Runbook

            1. Check the dashboard
            2. Page on-call if red
      required:
        - filename
        - content
    CreateDocumentResponse:
      type: object
      properties:
        document_id:
          type: string
          description: >-
            Document id (`doc_` prefix). The paired phone pulls the encrypted
            content with it via the FCM-nudged `GET /v1/documents/{documentId}`
            pull, never carried inline.
          example: doc_3f6e2a7c9b1d4f8e8a5b6c1d2e3f4a5b
        byte_length:
          type: integer
          description: UTF-8 byte length of the content that was sent.
          example: 128
      required:
        - document_id
        - byte_length
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: message_too_long
            message:
              type: string
              example: message must be ≤ 1500 bytes
            details:
              type: object
              additionalProperties: {}
          required:
            - code
            - message
      required:
        - error
    SimpleErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: invalid_token
        message:
          type: string
      required:
        - error
      description: >-
        Flat error shape used for auth failures — `invalid_token` carries only
        the stable code; `missing_token` adds a `message` hint pointing at the
        `Authorization: Bearer …` header. The `message` field is therefore
        optional.
  responses:
    BadRequest:
      description: Malformed payload or invalid field value.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalidPriority:
              value:
                error:
                  code: invalid_priority
                  message: priority must be one of min, low, default, high, urgent
            messageTooLong:
              value:
                error:
                  code: message_too_long
                  message: message must be ≤ 1500 bytes
                  details:
                    bytes: 1620
                    max: 1500
            invalidFilename:
              summary: POST /v1/documents — blocked extension or unsafe name
              value:
                error:
                  code: invalid_filename
                  message: >-
                    filename must end in an allowed extension and be a safe name
                    (max 120 characters)
            invalidContent:
              summary: POST /v1/documents — empty content
              value:
                error:
                  code: invalid_content
                  message: content must not be empty
    Unauthorized:
      description: Missing, invalid, or revoked bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SimpleErrorResponse'
          examples:
            missingToken:
              value:
                error: missing_token
                message: 'Authorization: Bearer rfk_live_… required'
            invalidToken:
              value:
                error: invalid_token
    Forbidden:
      description: Token exists but lacks scope for the requested action.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            priorityCapped:
              value:
                error:
                  code: priority_capped
                  message: Token's priority_cap is 'default'; requested 'high'
    Conflict:
      description: The request conflicts with the current state of a resource.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            noPhoneDevice:
              summary: Owner has no paired phone to ask
              value:
                error:
                  code: no_phone_device
                  message: no_phone_device
    DocumentTooLarge:
      description: Document content exceeds the 131072-byte cap.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            contentTooLarge:
              value:
                error:
                  code: content_too_large
                  message: content must be at most 131072 bytes
    UpgradeRequired:
      description: >-
        The target phone's Reflecto app is too old to receive this — update
        Reflecto on the phone from the Google Play Store before this can be
        delivered. Nothing was sent.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            phoneAppOutdated:
              value:
                error:
                  code: phone_app_outdated
                  message: >-
                    the paired phone's Reflecto app is too old to receive this —
                    update Reflecto on the phone from the Google Play Store
                    before this can be delivered; nothing was sent.
    PreconditionRequired:
      description: >-
        The paired phone has never reported an app version, so the server can't
        confirm it supports this. Nothing was sent.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            phoneAppUnknown:
              value:
                error:
                  code: phone_app_unknown
                  message: >-
                    the paired phone hasn't reported its app version yet — open
                    Reflecto once on the phone, then try again; nothing was
                    sent.
    RateLimitedInteractions:
      description: >-
        Rate limit or resource cap exceeded. For interaction creation this
        includes the 10-pending-per-owner cap; for ring and reminders, IP/token
        rate limits.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            rateLimitExceeded:
              value:
                error:
                  code: rate_limit_exceeded
                  message: Rate limit hit
            tooManyPending:
              summary: Owner already has 10 pending interactions
              value:
                error:
                  code: too_many_pending
                  message: too_many_pending
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: rfk_live_…
      description: >-
        Opaque bearer token created on your paired Android phone. The format is
        `rfk_live_` followed by 32 URL-safe alphanumeric characters (≥160 bits
        of entropy). Test tokens use the `rfk_test_` prefix.

````