openapi: 3.0.0
info:
  title: PostData
  description: |
    The simplest API to get your device's data onto the cloud. No signup, no
    API keys, everything public. Works over https:// and, deliberately, plain
    http:// for constrained firmware.
  version: 2.0.0
components:
  schemas:
    Metric:
      type: object
      minProperties: 1
      maxProperties: 10
      description: |
        A flat object of 1-10 metrics. Values are numbers, or short strings
        (strings keep only their last value - no history). Keys named "ts" or
        "timestamp" are ignored; the server sets the reception timestamp.
      example:
        temperature: 12.52
        humidity: 81
        firmware: "rc2.0.2"
    MetricResponse:
      type: object
      minProperties: 1
      description: The stored metrics plus ts, the server reception time (unix seconds).
      example:
        ts: 1785239565
        temperature: 12.52
        humidity: 81
    Error:
      type: object
      properties:
        error:
          type: string
    Dweet:
      type: object
      description: One reading in dweet.io's classic shape.
      properties:
        thing:
          type: string
        created:
          type: string
          format: date-time
          description: Server reception time, ISO 8601
        content:
          $ref: '#/components/schemas/Metric'
      example:
        thing: my-thing-name
        created: "2026-07-28T17:28:42.556Z"
        content:
          hello: world
    DweetSuccess:
      type: object
      description: dweet.io's success envelope for dweeting.
      properties:
        this:
          type: string
          enum: [succeeded]
        by:
          type: string
          enum: [dweeting]
        the:
          type: string
          enum: [dweet]
        with:
          $ref: '#/components/schemas/Dweet'
    DweetReadSuccess:
      type: object
      description: dweet.io's success envelope for reads (always an array, newest first).
      properties:
        this:
          type: string
          enum: [succeeded]
        by:
          type: string
          enum: [getting]
        the:
          type: string
          enum: [dweets]
        with:
          type: array
          items:
            $ref: '#/components/schemas/Dweet'
    DweetFailure:
      type: object
      description: dweet.io's failure envelope.
      properties:
        this:
          type: string
          enum: [failed]
        with:
          type: integer
          description: The HTTP status code
        because:
          type: string
      example:
        this: failed
        with: 404
        because: we couldn't find this
servers:
  - url: https://api.postdata.cloud
  - url: http://api.postdata.cloud
    description: Plain HTTP, for devices where TLS is impractical
paths:
  /add/{id}:
    post:
      tags:
        - Devices
      summary: Send a new set of metrics for a device
      requestBody:
        description: A flat JSON object with the metrics for this device.
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Metric'
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
          description: |
            A unique name for the device (letters, numbers, "_", ".", "-",
            max 64 chars). First come, first served - use something
            hard to guess (e.g. a UUID) if you want practical obscurity.
      responses:
        '200':
          description: Responds with the stored metrics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Metric'
        '400':
          description: Invalid device name, metric name, or body structure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited (one message per device per 30 seconds). See the Retry-After header.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until the next message is accepted
    get:
      tags:
        - Devices
      summary: Send metrics via query parameters (for constrained firmware)
      description: |
        Same as the POST variant, but metrics travel as query parameters, e.g.
        `/add/my-sensor?temp=21.5&hum=60`. Values that parse as numbers are
        stored as numbers.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
          description: The unique device name
      responses:
        '200':
          description: Responds with the stored metrics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Metric'
        '400':
          description: Invalid device name, metric name, or no metrics provided
        '429':
          description: Rate limited. See the Retry-After header.
  /last/{id}:
    get:
      tags:
        - Devices
      summary: Get the last metrics sent from a device
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
          description: The unique device name
      responses:
        '200':
          description: The last stored metrics, plus the server reception timestamp.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetricResponse'
        '400':
          description: Invalid device name
        '404':
          description: Unknown device
  /past/{id}/{hours}:
    get:
      tags:
        - Devices
      summary: Get historical numeric metrics from a device
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
          description: The unique device name
        - in: path
          name: hours
          schema:
            type: integer
            default: 168
            maximum: 2208
          required: true
          description: |
            How many hours to look back (168 = one week; capped at 2208, about
            92 days, which is also the retention window). May be omitted
            entirely (`/past/{id}`) to get the default week.
      responses:
        '200':
          description: A timestamp-ordered array of readings (numeric metrics only)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/MetricResponse'
        '400':
          description: Invalid device name
        '404':
          description: Unknown device
  /live/{id}:
    get:
      tags:
        - Live
      summary: Stream and publish metrics over a WebSocket
      description: |
        Upgrade to a WebSocket and receive the current reading immediately,
        then every new reading as it arrives. Each message is a JSON object
        shaped like /last. Text frame "ping" is answered with "pong".

        The same socket also publishes: send a flat JSON object of metrics --
        the body /add takes -- and it is stored identically, sharing the one
        rate limit. Success is the resulting broadcast, which carries the
        server timestamp; failures answer `{"error": ..., "retryAfter"?: n}`
        and leave the socket open.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
          description: The unique device name
        - in: header
          name: Upgrade
          required: true
          schema:
            type: string
            enum: [websocket]
      responses:
        '101':
          description: WebSocket established
        '400':
          description: Invalid device name
        '426':
          description: Not a WebSocket upgrade request
  /mqtt/{id}:
    get:
      tags:
        - Live
      summary: Publish and subscribe over MQTT 3.1.1 or 5.0
      description: |
        MQTT over WebSocket. There is no port 1883 or 8883: Workers accept only
        HTTP and HTTPS, so the transport is always ws:// or wss:// on the
        standard ports, and clients must have a WebSocket transport enabled.

        The device is taken from the URL, and one connection serves one device.
        Publish to `postdata/{id}` with a flat JSON object, or to
        `postdata/{id}/{metric}` with a single bare value. Subscribe to
        `postdata/{id}` (wildcards supported) for live readings in the same
        JSON shape /live sends.

        Supported: CONNECT, PUBLISH at QoS 0 and 1, SUBSCRIBE, UNSUBSCRIBE,
        PINGREQ, DISCONNECT. Not supported: QoS 2, retained messages, wills and
        persistent sessions. CONNECT credentials are accepted and ignored --
        every device is public.

        Prefer MQTT 5: its PUBACK reason codes report the 30-second rate limit
        (0x97), a topic naming another device (0x87) and a payload that failed
        validation (0x99). MQTT 3.1.1 has no reason-code field, so a rejected
        publish is indistinguishable from an accepted one. MQTT 5 clients are
        also given a Server Keep Alive of 240s and a Maximum QoS of 1.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
          description: The unique device name
        - in: header
          name: Upgrade
          required: true
          schema:
            type: string
            enum: [websocket]
        - in: header
          name: Sec-WebSocket-Protocol
          required: true
          description: Must offer "mqtt"; the server echoes it on the 101.
          schema:
            type: string
            enum: [mqtt]
      responses:
        '101':
          description: WebSocket established, MQTT session ready for CONNECT
        '400':
          description: Invalid device name, or the "mqtt" subprotocol was not offered
        '426':
          description: Not a WebSocket upgrade request
  /dweet/for/{thing}:
    post:
      tags:
        - dweet.io compatibility
      summary: Dweet like it's 2014 (alias of /add)
      description: |
        dweet.io compatibility. Same validation, storage, and rate limit as
        `/add/{thing}`, answered in dweet's classic envelope. The `key` and
        `callback` query parameters are accepted and ignored (locks and JSONP
        are not emulated).
      requestBody:
        description: A flat JSON object with the metrics for this thing.
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Metric'
      parameters:
        - in: path
          name: thing
          schema:
            type: string
          required: true
          description: The unique device name
      responses:
        '200':
          description: The stored dweet, in dweet's envelope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DweetSuccess'
        '400':
          description: Invalid thing name, metric name, or body structure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DweetFailure'
        '429':
          description: Rate limited (one message per device per 30 seconds). See the Retry-After header.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until the next message is accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DweetFailure'
    get:
      tags:
        - dweet.io compatibility
      summary: Dweet via query parameters
      description: |
        Metrics travel as query parameters, e.g.
        `/dweet/for/my-thing-name?hello=world`. `key` and `callback` are
        ignored, everything else becomes content.
      parameters:
        - in: path
          name: thing
          schema:
            type: string
          required: true
          description: The unique device name
      responses:
        '200':
          description: The stored dweet, in dweet's envelope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DweetSuccess'
        '400':
          description: Invalid thing name, metric name, or no metrics provided
        '429':
          description: Rate limited. See the Retry-After header.
  /dweet/quietly/for/{thing}:
    post:
      tags:
        - dweet.io compatibility
      summary: Dweet without the verbose response
      description: |
        Exactly like `/dweet/for/{thing}` (GET works too), but success is a
        bodiless 204. Failures still answer with the failure envelope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Metric'
      parameters:
        - in: path
          name: thing
          schema:
            type: string
          required: true
          description: The unique device name
      responses:
        '204':
          description: Dweet stored; no body
        '400':
          description: Invalid thing name, metric name, or body structure
        '429':
          description: Rate limited. See the Retry-After header.
  /get/latest/dweet/for/{thing}:
    get:
      tags:
        - dweet.io compatibility
      summary: The latest dweet for a thing (alias of /last)
      parameters:
        - in: path
          name: thing
          schema:
            type: string
          required: true
          description: The unique device name
      responses:
        '200':
          description: A one-element dweets array with the latest reading
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DweetReadSuccess'
        '404':
          description: Unknown thing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DweetFailure'
  /get/dweets/for/{thing}:
    get:
      tags:
        - dweet.io compatibility
      summary: The last dweets for a thing (approximated from /past)
      description: |
        Up to the 5 most recent readings of the past 24 hours, newest first,
        drawn from numeric history. Things with no history yet (text-only
        metrics, or data still in flight) answer with their latest reading.
      parameters:
        - in: path
          name: thing
          schema:
            type: string
          required: true
          description: The unique device name
      responses:
        '200':
          description: Up to 5 dweets, newest first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DweetReadSuccess'
        '404':
          description: Unknown thing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DweetFailure'
  /explore:
    get:
      tags:
        - Discovery
      summary: List the 20 most recently active devices
      responses:
        '200':
          description: Recently active devices with their latest data
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    ts:
                      type: integer
                      description: Last reception time, unix milliseconds
                    data:
                      $ref: '#/components/schemas/Metric'
