openapi: 3.1.0
info:
  title: autoMessage REST API
  version: "1.0.0"
  description: |
    Send and receive iMessages from any language over standard HTTPS.
    autoMessage is a native macOS connector plus a cloud API. Every message is
    ultimately sent from the customer's own Apple device using the customer's
    own Apple ID.
  contact:
    name: autoMessage support
    email: hello@automessage.app
    url: https://automessage.app
  license:
    name: Proprietary — see https://automessage.app/terms/
servers:
  - url: https://api.automessage.app/v1
    description: Production
security:
  - apiKey: []
tags:
  - name: Messages
    description: Send and read iMessages.
  - name: Drafts
    description: Queue and manage outbound drafts.
  - name: Conversations
    description: Read conversation threads.
  - name: Contacts
    description: Manage your contact address book.
  - name: Status
    description: Health and connector-online checks.
paths:
  /messages:
    get:
      tags: [Messages]
      summary: List messages
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: direction
          in: query
          description: Only inbound or outbound messages.
          schema: { type: string, enum: [inbound, outbound] }
        - name: since
          in: query
          description: Only messages sent at or after this ISO-8601 timestamp.
          schema: { type: string, format: date-time }
        - name: before
          in: query
          description: Only messages sent before this ISO-8601 timestamp.
          schema: { type: string, format: date-time }
      responses:
        "200":
          description: Paginated list of messages.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessageList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Messages]
      summary: Queue an outbound iMessage
      description: |
        Creates a draft that the on-device connector will send from the customer's
        Apple ID as an iMessage (or as SMS fallback if the recipient is not on
        iMessage).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendMessageRequest" }
      responses:
        "202":
          description: Message queued.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /messages/{guid}:
    get:
      tags: [Messages]
      summary: Retrieve a single message
      parameters:
        - name: guid
          in: path
          required: true
          description: The Apple `chat.db` GUID for the message.
          schema: { type: string }
      responses:
        "200":
          description: The message.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "404": { $ref: "#/components/responses/NotFound" }

  /conversations:
    get:
      tags: [Conversations]
      summary: List conversations
      description: >
        Find conversations. Pass at most ONE primary filter — participant,
        handle, name, group, or unread; each also accepts activeSince/activeBefore,
        limit, and cursor. Combining primaries returns 400.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: participant
          in: query
          description: >
            Phone number or email that appears in the conversation — returns
            every thread (1:1 AND group) that includes this contact. Loosely
            formatted numbers are normalized to E.164 (e.g. "(502) 544-2686" →
            "+15025442686"). The recommended way to find conversations with a
            given number.
          schema: { type: string }
          example: "+15025442686"
        - name: name
          in: query
          description: >
            Case-insensitive substring of the contact or group display name
            (e.g. "bax" matches "Baxter Parrot"). Scans your most recent
            conversations; response `pagination.truncated` is true if the scan
            window may hide older matches.
          schema: { type: string }
        - name: handle
          in: query
          description: >
            Exact recipient handle (E.164 phone or email, normalized) — returns
            only the 1:1 conversation with that contact. Prefer `participant`
            unless you specifically want the 1:1 thread.
          schema: { type: string }
        - name: group
          in: query
          description: "true: group chats only. false: 1:1 conversations only."
          schema: { type: boolean }
        - name: unread
          in: query
          description: Only conversations with unread messages.
          schema: { type: boolean }
        - name: activeSince
          in: query
          description: Only conversations active at or after this ISO-8601 timestamp.
          schema: { type: string, format: date-time }
        - name: activeBefore
          in: query
          description: Only conversations last active before this ISO-8601 timestamp.
          schema: { type: string, format: date-time }
      responses:
        "200":
          description: Paginated list of conversations.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ConversationList" }

  /drafts:
    get:
      tags: [Drafts]
      summary: List drafts
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Paginated list of drafts.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DraftList" }
    post:
      tags: [Drafts]
      summary: Create a draft (equivalent to POST /messages)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendMessageRequest" }
      responses:
        "201":
          description: Draft created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Draft" }
  /drafts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Drafts]
      summary: Retrieve a draft
      responses:
        "200":
          description: The draft.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Draft" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Drafts]
      summary: Update a draft that has not yet been sent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                body: { type: string }
                to: { type: string, description: "E.164 phone or email" }
      responses:
        "200":
          description: Updated draft.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Draft" }
        "409":
          description: Draft already sent; cannot modify.
    delete:
      tags: [Drafts]
      summary: Cancel a draft
      responses:
        "204": { description: Draft cancelled. }
        "409":
          description: Draft already sent; cannot cancel.

  /contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Paginated list of contacts.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContactList" }
    post:
      tags: [Contacts]
      summary: Create a contact
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactInput" }
      responses:
        "201":
          description: Contact created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
  /contacts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Contacts]
      summary: Retrieve a contact
      responses:
        "200":
          description: The contact.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Contacts]
      summary: Update a contact
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactInput" }
      responses:
        "200":
          description: Updated contact.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
    delete:
      tags: [Contacts]
      summary: Delete a contact
      responses:
        "204": { description: Contact deleted. }

  /status:
    get:
      tags: [Status]
      summary: Health and connector liveness
      description: |
        Reports whether the API is healthy and whether the customer's on-device
        connector has checked in recently (`lastSeenAt` and `lastSyncAt`).
      responses:
        "200":
          description: Status snapshot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Status" }

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-KEY
      description: Issued at signup. Manage in the dashboard.
  parameters:
    Limit:
      name: limit
      in: query
      description: Maximum number of items to return (1–100).
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    Cursor:
      name: cursor
      in: query
      description: Opaque pagination cursor from a previous response.
      schema: { type: string }
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BadRequest:
      description: The request was malformed.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: The requested resource does not exist.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, example: "invalid_request" }
            message: { type: string }

    SendMessageRequest:
      type: object
      required: [to, body]
      properties:
        to:
          type: string
          description: |
            Recipient identifier. Phone numbers are normalized to E.164
            (e.g. `+14155550123`); email addresses are accepted for Apple ID recipients.
          example: "+14155550123"
        body:
          type: string
          example: "hi from autoMessage"
        attachments:
          type: array
          items:
            type: object
            required: [url]
            properties:
              url: { type: string, format: uri }
              mimeType: { type: string, example: "image/png" }
        scheduledAt:
          type: string
          format: date-time
          description: Optional ISO-8601 send-at time.

    Message:
      type: object
      required: [guid, conversation, body, outbound, createdAt]
      properties:
        guid: { type: string, description: "Apple chat.db message GUID." }
        conversation: { type: string }
        body: { type: string }
        outbound: { type: boolean, description: "True if we sent it." }
        contact: { type: string, description: "E.164 phone or email of the counterparty." }
        reference: { type: string, nullable: true, description: "Message this replies to, if any." }
        deliveredAt: { type: string, format: date-time, nullable: true }
        readAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }

    MessageList:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Message" }
        nextCursor: { type: string, nullable: true }

    Conversation:
      type: object
      required: [id, participants, updatedAt]
      properties:
        id: { type: string }
        displayName: { type: string, nullable: true }
        participants:
          type: array
          items: { type: string }
        lastMessageAt: { type: string, format: date-time, nullable: true }
        updatedAt: { type: string, format: date-time }

    ConversationList:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Conversation" }
        nextCursor: { type: string, nullable: true }

    Draft:
      type: object
      required: [id, to, body, sent, createdAt]
      properties:
        id: { type: string }
        to: { type: string }
        body: { type: string }
        sent: { type: boolean }
        sentAt: { type: string, format: date-time, nullable: true }
        scheduledAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }

    DraftList:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Draft" }
        nextCursor: { type: string, nullable: true }

    ContactInput:
      type: object
      required: [displayName]
      properties:
        displayName: { type: string }
        phone: { type: string, description: "E.164; normalized on write." }
        email: { type: string, format: email }
        notes: { type: string }
        tags:
          type: array
          items: { type: string }

    Contact:
      allOf:
        - $ref: "#/components/schemas/ContactInput"
        - type: object
          required: [id, createdAt]
          properties:
            id: { type: string }
            createdAt: { type: string, format: date-time }

    ContactList:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Contact" }
        nextCursor: { type: string, nullable: true }

    Status:
      type: object
      required: [ok, connectorOnline]
      properties:
        ok: { type: boolean, example: true }
        connectorOnline: { type: boolean, description: "Has the customer's Mac connector checked in recently?" }
        lastSeenAt: { type: string, format: date-time, nullable: true }
        lastSyncAt: { type: string, format: date-time, nullable: true }
        version: { type: string, example: "1.0.0" }
