openapi: 3.0.3
info:
  title: ROS Connect Canonical API
  version: "1.0"
  description: |
    Canonical partner contract for ROS Connect (`/v1/`).

    Signature: HMAC-SHA256 over `timestamp + "." + rawBody`.
    Header: `X-ROS-Signature: sha256=<hex>` plus `X-ROS-Timestamp`, `X-ROS-Event-Id`, `X-ROS-Key-Id`.
    Replay window: 5 minutes. Idempotency required on mutating calls.

    SUPPORTED NOW: Native FLOW (adapter demonstration) and Mock restaurant POS (TEST / DEVELOPMENT ONLY).
    DECLARED / NOT CONNECTED: Petpooja, Toast, LucidPOS, Square. Do not treat those as implemented adapters.

    Generic events never create G37 billable lines, G38 invoices, or G39 payments.
  contact:
    name: RestroSync
servers:
  - url: http://localhost:5002
    description: Local development
  - url: https://api.restrosync.com
    description: Production API host
tags:
  - name: Events
  - name: Commands
  - name: Discovery
  - name: Webhooks
security:
  - RosSignature: []
paths:
  /v1/integrations/{integrationId}/events/customer.updated:
    post:
      tags: [Events]
      operationId: ingestCustomerUpdated
      summary: Inbound customer upsert
      parameters:
        - $ref: "#/components/parameters/integrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventEnvelope"
            example:
              eventId: evt_01HDEMOCUSTOMER
              eventType: customer.updated
              schemaVersion: "1.0"
              integrationId: int_demo_0001
              merchantId: merchant_demo
              outletId: outlet_demo
              occurredAt: "2026-08-17T12:00:00.000Z"
              idempotencyKey: cust_demo_v1
              data:
                externalCustomerId: cust_demo_1
                mobile: "+910000000099"
                firstName: Asha
                lastName: Khan
                source: mock_restaurant_pos
      responses:
        "202":
          $ref: "#/components/responses/Accepted"
        "401":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
  /v1/integrations/{integrationId}/events/bill.created:
    post:
      tags: [Events]
      operationId: ingestBillCreated
      parameters:
        - $ref: "#/components/parameters/integrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventEnvelope"
            example:
              eventId: evt_01HDEMOBILL
              eventType: bill.created
              schemaVersion: "1.0"
              integrationId: int_demo_0001
              occurredAt: "2026-08-17T12:00:00.000Z"
              idempotencyKey: bill_1480_created_v1
              data:
                externalBillId: bill_1480
                billNumber: MOCK-1480
                items:
                  - name: Chicken Tikka
                    quantity: 2
                    unitPriceMinor: 50000
                    totalMinor: 100000
                  - name: Beer
                    quantity: 2
                    unitPriceMinor: 15000
                    totalMinor: 30000
                  - name: Fries
                    quantity: 1
                    unitPriceMinor: 18000
                    totalMinor: 18000
                totalMinor: 148000
                currency: INR
                status: COMPLETED
                vendorStatus: closed
                rosStatus: COMPLETED
      responses:
        "202":
          $ref: "#/components/responses/Accepted"
        "401":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
  /v1/integrations/{integrationId}/events/bill.updated:
    post:
      tags: [Events]
      operationId: ingestBillUpdated
      parameters:
        - $ref: "#/components/parameters/integrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventEnvelope"
      responses:
        "202":
          $ref: "#/components/responses/Accepted"
        "409":
          $ref: "#/components/responses/Error"
  /v1/integrations/{integrationId}/events/bill.voided:
    post:
      tags: [Events]
      operationId: ingestBillVoided
      parameters:
        - $ref: "#/components/parameters/integrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventEnvelope"
      responses:
        "202":
          $ref: "#/components/responses/Accepted"
  /v1/integrations/{integrationId}/events/sale.returned:
    post:
      tags: [Events]
      operationId: ingestSaleReturned
      parameters:
        - $ref: "#/components/parameters/integrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventEnvelope"
      responses:
        "202":
          $ref: "#/components/responses/Accepted"
  /v1/integrations/{integrationId}/events/payment-types.updated:
    post:
      tags: [Events]
      operationId: ingestPaymentTypesUpdated
      parameters:
        - $ref: "#/components/parameters/integrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventEnvelope"
      responses:
        "202":
          $ref: "#/components/responses/Accepted"
  /v1/integrations/{integrationId}/events/menu.updated:
    post:
      tags: [Events]
      operationId: ingestMenuUpdated
      parameters:
        - $ref: "#/components/parameters/integrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventEnvelope"
      responses:
        "202":
          $ref: "#/components/responses/Accepted"
  /v1/integrations/{integrationId}/webhooks:
    post:
      tags: [Webhooks]
      operationId: ingestGenericWebhook
      parameters:
        - $ref: "#/components/parameters/integrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventEnvelope"
      responses:
        "202":
          $ref: "#/components/responses/Accepted"
        "401":
          $ref: "#/components/responses/Error"
  /v1/integrations/{integrationId}/actions/customer.lookup:
    post:
      tags: [Commands]
      operationId: actionCustomerLookup
      parameters:
        - $ref: "#/components/parameters/integrationId"
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommandEnvelope"
      responses:
        "200":
          $ref: "#/components/responses/CommandOk"
        "403":
          $ref: "#/components/responses/Error"
  /v1/integrations/{integrationId}/actions/customer.upsert:
    post:
      tags: [Commands]
      operationId: actionCustomerUpsert
      parameters:
        - $ref: "#/components/parameters/integrationId"
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommandEnvelope"
      responses:
        "200":
          $ref: "#/components/responses/CommandOk"
  /v1/integrations/{integrationId}/actions/reward.validate:
    post:
      tags: [Commands]
      operationId: actionRewardValidate
      parameters:
        - $ref: "#/components/parameters/integrationId"
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommandEnvelope"
      responses:
        "200":
          $ref: "#/components/responses/CommandOk"
  /v1/integrations/{integrationId}/actions/reward.redeem:
    post:
      tags: [Commands]
      operationId: actionRewardRedeem
      parameters:
        - $ref: "#/components/parameters/integrationId"
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommandEnvelope"
      responses:
        "200":
          $ref: "#/components/responses/CommandOk"
  /v1/integrations/{integrationId}/actions/reward.revert:
    post:
      tags: [Commands]
      operationId: actionRewardRevert
      parameters:
        - $ref: "#/components/parameters/integrationId"
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommandEnvelope"
      responses:
        "200":
          $ref: "#/components/responses/CommandOk"
  /v1/integrations/{integrationId}/actions/menu.push:
    post:
      tags: [Commands]
      operationId: actionMenuPush
      parameters:
        - $ref: "#/components/parameters/integrationId"
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommandEnvelope"
      responses:
        "200":
          $ref: "#/components/responses/CommandOk"
        "403":
          $ref: "#/components/responses/Error"
  /v1/integrations/{integrationId}/actions/payment-types.push:
    post:
      tags: [Commands]
      operationId: actionPaymentTypesPush
      parameters:
        - $ref: "#/components/parameters/integrationId"
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommandEnvelope"
      responses:
        "200":
          $ref: "#/components/responses/CommandOk"
  /v1/integrations/{integrationId}/capabilities:
    get:
      tags: [Discovery]
      operationId: getCapabilities
      parameters:
        - $ref: "#/components/parameters/integrationId"
      responses:
        "200":
          description: Capability flags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Success"
  /v1/integrations/{integrationId}/health:
    get:
      tags: [Discovery]
      operationId: getHealth
      parameters:
        - $ref: "#/components/parameters/integrationId"
      responses:
        "200":
          description: Health
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Success"
components:
  securitySchemes:
    RosSignature:
      type: apiKey
      in: header
      name: X-ROS-Signature
      description: HMAC-SHA256. Value format sha256=<hex> of timestamp + "." + raw body.
  parameters:
    integrationId:
      name: integrationId
      in: path
      required: true
      schema:
        type: string
        example: int_demo_0001
    idempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
  responses:
    Accepted:
      description: Event accepted
      headers:
        X-Request-Id:
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Success"
    CommandOk:
      description: Command result
      headers:
        X-Request-Id:
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Success"
    Error:
      description: Canonical error
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            signature:
              value:
                success: false
                error:
                  code: SIGNATURE_INVALID
                  message: Invalid signature
                  requestId: req_demo
            duplicate:
              value:
                success: false
                error:
                  code: DUPLICATE_EVENT
                  message: Event already accepted
                  requestId: req_demo
            conflict:
              value:
                success: false
                error:
                  code: IDEMPOTENCY_CONFLICT
                  message: Idempotency key reused with a different payload
            capability:
              value:
                success: false
                error:
                  code: UNSUPPORTED_CAPABILITY
                  message: Scope menu.write is not granted
  schemas:
    Success:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum:
            - AUTHENTICATION_FAILED
            - SIGNATURE_INVALID
            - SIGNATURE_EXPIRED
            - REPLAY_DETECTED
            - INVALID_SCHEMA
            - INVALID_FIELD
            - MISSING_REQUIRED_FIELD
            - UNSUPPORTED_CAPABILITY
            - INTEGRATION_NOT_FOUND
            - INTEGRATION_NOT_CONNECTED
            - INTEGRATION_DISABLED
            - DUPLICATE_EVENT
            - STALE_EVENT
            - RATE_LIMITED
            - PROVIDER_TIMEOUT
            - PROVIDER_ERROR
            - IDEMPOTENCY_CONFLICT
            - INTERNAL_ERROR
        message:
          type: string
        details: {}
        requestId:
          type: string
        retryable:
          type: boolean
        providerCode:
          type: string
    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          $ref: "#/components/schemas/Error"
    EventEnvelope:
      type: object
      required: [eventId, eventType, schemaVersion, integrationId, occurredAt, idempotencyKey, data]
      properties:
        eventId:
          type: string
        eventType:
          type: string
          enum:
            - customer.updated
            - bill.created
            - bill.updated
            - bill.voided
            - sale.returned
            - payment-types.updated
            - menu.updated
        schemaVersion:
          type: string
          example: "1.0"
        integrationId:
          type: string
        merchantId:
          type: string
        outletId:
          type: string
        occurredAt:
          type: string
          format: date-time
        receivedAt:
          type: string
          format: date-time
        idempotencyKey:
          type: string
        data:
          type: object
    CommandEnvelope:
      type: object
      required: [commandId, action, schemaVersion, integrationId, idempotencyKey, requestedAt, data]
      properties:
        commandId:
          type: string
        action:
          type: string
          enum:
            - customer.lookup
            - customer.upsert
            - reward.validate
            - reward.redeem
            - reward.revert
            - menu.push
            - payment-types.push
        schemaVersion:
          type: string
          example: "1.0"
        integrationId:
          type: string
        merchantId:
          type: string
        outletId:
          type: string
        idempotencyKey:
          type: string
        requestedAt:
          type: string
          format: date-time
        data:
          type: object
    ExternalCustomer:
      type: object
      required: [externalCustomerId]
      properties:
        schemaVersion:
          type: string
        externalCustomerId:
          type: string
        sourceCustomerId:
          type: string
        merchantId:
          type: string
        mobile:
          type: string
          example: "+910000000099"
        email:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        gender:
          type: string
        dateOfBirth:
          type: string
        preferences:
          type: object
        source:
          type: string
        createdAt:
          type: string
        updatedAt:
          type: string
    ExternalBill:
      type: object
      required: [externalBillId, totalMinor, currency]
      properties:
        schemaVersion:
          type: string
        externalBillId:
          type: string
        billNumber:
          type: string
        outletId:
          type: string
        merchantId:
          type: string
        items:
          type: array
          items:
            $ref: "#/components/schemas/BillItem"
        totalMinor:
          type: integer
          example: 148000
        currency:
          type: string
          example: INR
        status:
          type: string
        vendorStatus:
          type: string
        rosStatus:
          type: string
          enum: [OPEN, COMPLETED, VOIDED, RETURNED, PARTIALLY_RETURNED]
        revision:
          type: integer
    BillItem:
      type: object
      required: [name, quantity, unitPriceMinor, totalMinor]
      properties:
        name:
          type: string
          example: Chicken Tikka
        quantity:
          type: number
        unitPriceMinor:
          type: integer
        totalMinor:
          type: integer
        sku:
          type: string
        category:
          type: string
    ExternalTransaction:
      type: object
      required: [externalTransactionId, amountMinor, currency, transactionType, occurredAt]
      properties:
        externalTransactionId:
          type: string
        billId:
          type: string
        amountMinor:
          type: integer
        currency:
          type: string
        transactionType:
          type: string
          enum: [SALE, RETURN, VOID, ADJUSTMENT]
        occurredAt:
          type: string
        source:
          type: string
    PaymentTypes:
      type: object
      properties:
        schemaVersion:
          type: string
        categories:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              canonicalType:
                type: string
              types:
                type: array
                items:
                  type: string
    ExternalMenu:
      type: object
      properties:
        menuVersion:
          type: string
        externalMenuVersion:
          type: string
        mappingVersion:
          type: string
        categories:
          type: array
          items: {}
        items:
          type: array
          items: {}
x-ros-connect:
  deprecation:
    policy: v1 field meanings do not change. Breaking changes ship as /v2/.
    headers: Sunset, Deprecation
  rateLimits:
    webhook: 30 per minute per integration
    commands: 30 per minute per integration
    retryAfter: 60
  supportedNow:
    - native_flow
    - mock_restaurant_pos
  declaredNotConnected:
    - petpooja
    - toast
    - lucid
    - square
