openapi: 3.1.0
info:
  title: Relasi.U Omnichannel Developer API
  version: '2026-06-01'
  description: >
    Public REST API for the Relasi.U omnichannel chat platform. Use it to read

    conversations and messages, send outbound messages across any connected

    channel (WhatsApp, Telegram, Instagram, Facebook, Email, Livechat), and

    manage contacts.


    ## Authentication

    Authenticate every request with an API key in the `Authorization` header:


    ```

    Authorization: Bearer ru_live_<id>.<secret>

    ```


    Create and manage keys in the dashboard under **Settings → Developer /
    API**.

    The full key (including its secret) is shown only once at creation — store
    it

    securely. Each key carries a set of **scopes** that gate which endpoints it

    can call, and an optional **IP allowlist**.


    ## Modes

    Keys are either `live` (`ru_live_…`) or `test` (`ru_test_…`).


    ## Rate limits

    Requests are throttled per API key (~60 req/s). Exceeding the limit returns

    `429 Too Many Requests` with a `Retry-After` header.
  contact:
    name: Developer Support
servers:
  - url: https://api.relasiu.com/api/public/v1
    description: Production
  - url: http://localhost:8081/api/public/v1
    description: Local development
security:
  - ApiKeyAuth: []
tags:
  - name: Meta
    description: Credential introspection
  - name: Conversations
    description: Read conversations and messages
  - name: Messages
    description: Send outbound messages
  - name: Contacts
    description: Manage contacts (customers)
  - name: Channels
    description: Read connected channels
paths:
  /me:
    get:
      tags:
        - Meta
      summary: Inspect the authenticated API key
      description: >-
        Returns metadata about the API key making the request, including granted
        scopes. No additional scope required.
      operationId: whoAmI
      responses:
        '200':
          description: API key metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyIdentity'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /conversations:
    get:
      tags:
        - Conversations
      summary: List conversations
      description: Returns conversations for the workspace, newest activity first.
      operationId: listConversations
      security:
        - ApiKeyAuth:
            - conversations:read
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: last_updated_at
          in: query
          description: >-
            UNIX timestamp; only return conversations updated at/after this time
            (delta sync).
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Paginated conversations
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Conversation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /messages:
    get:
      tags:
        - Conversations
      summary: List messages
      description: Returns messages, optionally filtered by conversation or session.
      operationId: listMessages
      security:
        - ApiKeyAuth:
            - messages:read
      parameters:
        - name: conversation_id
          in: query
          schema:
            type: string
        - name: message_session_id
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated messages
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Message'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /messages/text:
    post:
      tags:
        - Messages
      summary: Send a text message
      description: |
        Sends a text message into an existing conversation. The message is
        dispatched to the underlying channel (WhatsApp, Telegram, …). Use a
        unique `client_request_id` per message for idempotency.
      operationId: sendTextMessage
      security:
        - ApiKeyAuth:
            - messages:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendTextRequest'
      responses:
        '200':
          description: Message accepted and dispatched
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    $ref: '#/components/schemas/Message'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /contacts:
    get:
      tags:
        - Contacts
      summary: List contacts
      operationId: listContacts
      security:
        - ApiKeyAuth:
            - customers:read
      parameters:
        - name: search
          in: query
          description: Fuzzy match on name, email, phone, company.
          schema:
            type: string
        - name: lead_status
          in: query
          schema:
            type: string
            enum:
              - new
              - contacted
              - qualified
              - won
              - lost
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated contacts
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
        - Contacts
      summary: Create a contact
      operationId: createContact
      security:
        - ApiKeyAuth:
            - customers:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContactRequest'
      responses:
        '201':
          description: Created contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /contacts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Contacts
      summary: Get a contact
      operationId: getContact
      security:
        - ApiKeyAuth:
            - customers:read
      responses:
        '200':
          description: Contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Contacts
      summary: Update a contact
      operationId: updateContact
      security:
        - ApiKeyAuth:
            - customers:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContactRequest'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '404':
          $ref: '#/components/responses/NotFound'
  /contacts/merge:
    post:
      tags:
        - Contacts
      summary: Merge two contacts
      description: >-
        Merges a source contact into a target, preserving channel identifiers
        and history.
      operationId: mergeContacts
      security:
        - ApiKeyAuth:
            - customers:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - primary_id
                - secondary_id
              properties:
                primary_id:
                  type: string
                  description: Contact to keep.
                secondary_id:
                  type: string
                  description: Contact to merge in and remove.
      responses:
        '200':
          description: Merged
        '400':
          $ref: '#/components/responses/BadRequest'
  /channels:
    get:
      tags:
        - Channels
      summary: List connected channels
      operationId: listChannels
      security:
        - ApiKeyAuth:
            - channels:read
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated channels
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Channel'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: |
        API key in the form `ru_live_<id>.<secret>`. Scopes listed on each
        operation are required for that endpoint.
  parameters:
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPage:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 500
  responses:
    Unauthorized:
      description: Missing, malformed, revoked, or expired API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: >-
        The API key lacks the required scope, or the client IP is not
        allowlisted.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadRequest:
      description: Invalid request body or parameters.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
        required_scope:
          type: string
          description: Present on 403 responses caused by insufficient scope.
    Pagination:
      type: object
      properties:
        current_page:
          type: integer
        from:
          type: integer
        to:
          type: integer
        per_page:
          type: integer
        last_page:
          type: integer
        total:
          type: integer
        has_next:
          type: boolean
        data:
          type: array
          items: {}
    ApiKeyIdentity:
      type: object
      properties:
        app_id:
          type: string
        key_id:
          type: string
        name:
          type: string
        public_id:
          type: string
        mode:
          type: string
          enum:
            - live
            - test
        scopes:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
    Conversation:
      type: object
      properties:
        id:
          type: string
        app_id:
          type: string
        channel:
          type: string
        channel_id:
          type: string
        external_id:
          type: string
        customer_id:
          type: string
        status:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Message:
      type: object
      properties:
        id:
          type: string
        app_id:
          type: string
        conversation_id:
          type: string
        message_session_id:
          type: string
        channel_id:
          type: string
        channel:
          type: string
        from_user_id:
          type: string
          nullable: true
        from_customer:
          type: boolean
        client_request_id:
          type: string
        content:
          type: string
        message_type:
          type: string
          enum:
            - text
            - image
            - audio
            - voice
            - video
            - document
            - location
            - rating
            - email
        send_state:
          type: string
          enum:
            - pending
            - sent
            - failed
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    SendTextRequest:
      type: object
      required:
        - client_request_id
        - conversation_id
        - text
      properties:
        client_request_id:
          type: string
          description: Unique idempotency key supplied by the caller.
        conversation_id:
          type: string
        text:
          type: string
          minLength: 1
          maxLength: 4000
        reply_to_message_id:
          type: string
          description: Optional id of the message being replied to.
        as_system:
          type: boolean
          default: false
          description: |
            Attribute the message to the workspace **System** rather than an
            agent (rendered as "System", no read receipts). Mutually exclusive
            with `from_user_id`. Takes precedence if both are set.
        from_user_id:
          type: string
          description: |
            Attribute the message to a specific agent/owner in your workspace.
            Must be a valid user id belonging to the same workspace. If omitted
            (and `as_system` is false) the message is attributed to the user who
            created the API key. The same overrides are accepted as form fields
            on the multipart media endpoints (`/messages/image`, `/audio`,
            `/video`, `/document`) and in the JSON body of `/messages/location`.
    Contact:
      type: object
      properties:
        id:
          type: string
        app_id:
          type: string
        name:
          type: string
        phone:
          type: string
        email:
          type: string
        company:
          type: string
        job_title:
          type: string
        lead_status:
          type: string
          enum:
            - new
            - contacted
            - qualified
            - won
            - lost
        lead_source:
          type: string
        address:
          type: string
        city:
          type: string
        country:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateContactRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          maxLength: 200
        phone:
          type: string
          maxLength: 50
        email:
          type: string
          maxLength: 200
        company:
          type: string
          maxLength: 200
        job_title:
          type: string
          maxLength: 200
        lead_status:
          type: string
          enum:
            - ''
            - new
            - contacted
            - qualified
            - won
            - lost
        lead_source:
          type: string
        address:
          type: string
          maxLength: 500
        city:
          type: string
          maxLength: 100
        country:
          type: string
          maxLength: 100
        comment:
          type: string
    Channel:
      type: object
      properties:
        id:
          type: string
        app_id:
          type: string
        type:
          type: string
          enum:
            - telegram
            - whatsapp
            - whatsapp_business
            - instagram
            - facebook
            - email
            - livechat
        external_id:
          type: string
        name:
          type: string
        username:
          type: string
        phone_number:
          type: string
        email:
          type: string
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time
