> ## 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.

# Pushover-compatible send shim

> Accepts Pushover's exact field set so existing senders can migrate by changing one URL — point your Pushover client at `https://api.reflecto.dev/v1/messages.json` and the same body works.

Field mapping:

| Pushover    | Reflecto                                   |
|-------------|--------------------------------------------|
| `token`     | Sender token (must be a `rfk_live_…`)      |
| `user`      | Ignored (token already identifies the user)|
| `message`   | `message`                                  |
| `title`     | `title`                                    |
| `priority`  | `-2`→min, `-1`→low, `0`→default, `1`→high  |
| `url`       | `url`                                      |
| `url_title` | `url_title`                                |
| `device`    | `device` (comma-separated allowed)         |
| `ttl`       | `ttl`                                      |
| `sound`     | Ignored in MVP (no custom sounds)          |
| `timestamp` | Ignored (server uses current time)         |

Pushover `priority=2` (Emergency) is **rejected** with `400` — Reflecto does not implement Pushover's receipt-polling contract, so silently downgrading would break migrations that depend on it.



## OpenAPI

````yaml /openapi.yaml post /v1/messages.json
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/messages.json:
    post:
      tags:
        - Pushover compatibility
      summary: Pushover-compatible send shim
      description: >-
        Accepts Pushover's exact field set so existing senders can migrate by
        changing one URL — point your Pushover client at
        `https://api.reflecto.dev/v1/messages.json` and the same body works.


        Field mapping:


        | Pushover    | Reflecto                                   |

        |-------------|--------------------------------------------|

        | `token`     | Sender token (must be a `rfk_live_…`)      |

        | `user`      | Ignored (token already identifies the user)|

        | `message`   | `message`                                  |

        | `title`     | `title`                                    |

        | `priority`  | `-2`→min, `-1`→low, `0`→default, `1`→high  |

        | `url`       | `url`                                      |

        | `url_title` | `url_title`                                |

        | `device`    | `device` (comma-separated allowed)         |

        | `ttl`       | `ttl`                                      |

        | `sound`     | Ignored in MVP (no custom sounds)          |

        | `timestamp` | Ignored (server uses current time)         |


        Pushover `priority=2` (Emergency) is **rejected** with `400` — Reflecto
        does not implement Pushover's receipt-polling contract, so silently
        downgrading would break migrations that depend on it.
      operationId: pushoverCompat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PushoverRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PushoverRequest'
      responses:
        '200':
          description: Accepted, wrapped in Pushover's success shape.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
            X-RateLimit-Resource:
              $ref: '#/components/headers/XRateLimitResource'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushoverSuccess'
        '400':
          description: >-
            Malformed payload or invalid field value, wrapped in Pushover's
            failure shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushoverFailure'
              examples:
                emergencyUnsupported:
                  value:
                    status: 0
                    errors:
                      - priority_emergency_unsupported
        '401':
          description: Missing or invalid token, wrapped in Pushover's failure shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushoverFailure'
              examples:
                invalidToken:
                  value:
                    status: 0
                    errors:
                      - invalid_token
        '403':
          description: >-
            Token exists but lacks scope for the requested priority, wrapped in
            Pushover's failure shape. Surfaces when a Pushover client sends
            `priority=1` (high) against a token whose `priority_cap` is
            `default` or below.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushoverFailure'
              examples:
                scopeExceeded:
                  value:
                    status: 0
                    errors:
                      - Token's priority_cap is 'default'
        '413':
          description: Envelope > 2048 bytes, wrapped in Pushover's failure shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushoverFailure'
              examples:
                tooLarge:
                  value:
                    status: 0
                    errors:
                      - Payload exceeds 2048 byte limit
        '429':
          description: Rate limit exceeded, wrapped in Pushover's failure shape.
          headers:
            Retry-After:
              description: >-
                Seconds to wait before retrying. Respect — immediate retries
                trip the same gate.
              schema:
                type: integer
                example: 30
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
            X-RateLimit-Resource:
              $ref: '#/components/headers/XRateLimitResource'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushoverFailure'
              examples:
                rateLimited:
                  value:
                    status: 0
                    errors:
                      - Rate limit hit on token_burst
        '500':
          description: Unhandled server error, wrapped in Pushover's failure shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushoverFailure'
      security: []
components:
  schemas:
    PushoverRequest:
      type: object
      properties:
        token:
          type: string
          pattern: ^rfk_(live|test)_[A-Za-z0-9]{32}$
          description: >-
            Reflecto sender token (`rfk_live_…` or `rfk_test_…`). Stands in for
            Pushover's app token.
          example: rfk_live_aB3xQ7mN9pK2vR5tY8uW4sZ1cE6dF0gH
        user:
          type: string
          description: >-
            Accepted for Pushover-client compatibility; the value is ignored
            (the token already identifies the user).
          example: u_pushover_legacy
        message:
          type: string
          description: >-
            Notification body. Required by Pushover; same semantics as
            `/v1/send`.
          example: Backup done
        title:
          type: string
          description: Optional title. Same byte cap as `/v1/send` (100 bytes UTF-8).
          example: Backup
        priority:
          anyOf:
            - type: string
              enum:
                - '0'
                - '1'
                - '2'
                - '-2'
                - '-1'
            - type: integer
              minimum: -2
              maximum: 2
          description: >-
            Pushover priority. `-2` → min, `-1` → low, `0` → default, `1` →
            high. `2` (Emergency) is accepted by the wire schema but rejected
            server-side with `400 priority_emergency_unsupported` — Reflecto
            does not implement Pushover's receipt-polling contract, so silently
            downgrading would break migrations that depend on it.
          example: 0
        url:
          type: string
          format: uri
          example: https://example.com
        url_title:
          type: string
          example: View
        device:
          type: string
          description: Optional target device(s); same semantics as `/v1/send`.
          example: phone
        ttl:
          anyOf:
            - type: integer
              minimum: 0
              maximum: 259200
            - type: string
          description: >-
            Seconds the message stays in the per-device queue if not delivered
            live. Capped at 72 hours (259200 s) to match the Redis queue TTL.
            Same semantics as `SendRequest.ttl` — both endpoints feed through
            the shared `processSend` clamp. The string branch is only meaningful
            for form-encoded callers (Pushover senders post `ttl=600`); JSON
            callers should use the integer branch.
          example: 259200
        sound:
          type: string
          description: >-
            Accepted for Pushover-client compatibility; ignored in MVP (no
            custom sounds).
          example: default
        timestamp:
          anyOf:
            - type: integer
            - type: string
          description: >-
            Accepted for Pushover-client compatibility; ignored (server uses
            current time).
      required:
        - token
        - message
      description: >-
        Pushover-compatible send payload. Accepts the exact field set so
        existing Pushover clients can migrate by changing one URL. Unknown
        fields are ignored.
    PushoverSuccess:
      type: object
      properties:
        status:
          type: number
          enum:
            - 1
          format: int32
        request:
          type: string
          description: >-
            Opaque request id. Reflecto returns the internal `msg_*` id for log
            correlation.
          example: msg_3f6e2a7c9b1d4f8e8a5b6c1d2e3f4a5b
      required:
        - status
        - request
    PushoverFailure:
      type: object
      properties:
        status:
          type: number
          enum:
            - 0
          format: int32
        errors:
          type: array
          items:
            type: string
          description: >-
            Human-readable failure reasons. Reflecto extracts `err.message` from
            the internal error (not the stable code) to match Pushover's
            contract.
          example:
            - message is required
      required:
        - status
        - errors
  headers:
    XRateLimitLimit:
      description: Cap for the binding rate-limit layer on this request.
      schema:
        type: integer
        example: 60
    XRateLimitRemaining:
      description: Remaining sends in the binding layer's current window.
      schema:
        type: integer
        example: 47
    XRateLimitReset:
      description: Unix epoch (seconds) when the binding layer's window resets.
      schema:
        type: integer
        example: 1234567890
    XRateLimitResource:
      description: >-
        Names which layer was tightest on this request. The enumerated values
        match the resources tracked in
        `apps/server/src/middleware/publicApiLimits.ts`.
      schema:
        type: string
        enum:
          - token_burst
          - token_monthly
          - receiver_daily
          - ip_minute
          - ip_hour
        example: token_burst
  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.

````