openapi: 3.1.0
info:
  title: Loops Partner API
  version: 1.0.0-preview
  description: |
    Every endpoint and webhook of the Loops Partner API, generated from the
    [OpenAPI specification](/openapi.yaml).

    New here? Start with the [Quickstart](/getting-started/quickstart/). Authentication,
    errors and conventions are covered in the guides.
  contact:
    name: Loops Partners
    email: partners@loops.sa
servers:
  - url: https://sandbox.partners.loops.sa/v1
    description: Sandbox
  - url: https://api.partners.loops.sa/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Connection
  - name: Locations
  - name: Channels
  - name: Orders
  - name: Events

paths:
  /connection:
    get:
      tags: [Connection]
      operationId: getConnection
      summary: Get the current connection
      description: Returns what the merchant granted your application, including scopes and locations.
      parameters:
        - $ref: "#/components/parameters/MerchantId"
      responses:
        "200":
          description: The connection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Connection"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /locations:
    get:
      tags: [Locations]
      operationId: listLocations
      summary: List locations
      description: Locations the merchant shared with your application.
      security:
        - bearerAuth: [locations:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of locations.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Location"
                  next_cursor:
                    $ref: "#/components/schemas/NextCursor"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /locations/{location_id}:
    get:
      tags: [Locations]
      operationId: getLocation
      summary: Get a location
      description: One location the merchant shared with your application.
      security:
        - bearerAuth: [locations:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
        - $ref: "#/components/parameters/LocationId"
      responses:
        "200":
          description: The location.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Location"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /locations/{location_id}/channel-connections:
    get:
      tags: [Locations]
      operationId: listChannelConnections
      summary: List a location's channel connections
      description: The channels this location receives orders from, and their status.
      security:
        - bearerAuth: [locations:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
        - $ref: "#/components/parameters/LocationId"
      responses:
        "200":
          description: Channel connections for the location.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ChannelConnection"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /channels:
    get:
      tags: [Channels]
      operationId: listChannels
      summary: List channels
      description: |
        Channels available through Loops, with the capabilities each one supports.
        Use it to build your own UI and to know what a channel can do before calling it.
      responses:
        "200":
          description: The channel catalog.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Channel"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /orders:
    get:
      tags: [Orders]
      operationId: listOrders
      summary: List orders
      description: |
        Orders from all channels at the locations you can access, newest update first.
        For incremental sync, pass the `updated_at` of the last order you processed as `updated_since`.
      security:
        - bearerAuth: [orders:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
        - name: location_id
          in: query
          description: Only orders for this location.
          schema:
            type: string
            examples: [loc_2Xf9K]
        - name: channel
          in: query
          description: Only orders from this channel (an ID from `GET /channels`).
          schema:
            type: string
        - name: status
          in: query
          description: Only orders in this status.
          schema:
            $ref: "#/components/schemas/OrderStatus"
        - name: updated_since
          in: query
          description: Only orders updated at or after this time (ISO 8601, UTC).
          schema:
            type: string
            format: date-time
        - name: created_from
          in: query
          description: Only orders created at or after this time.
          schema:
            type: string
            format: date-time
        - name: created_to
          in: query
          description: Only orders created before this time.
          schema:
            type: string
            format: date-time
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of orders.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Order"
                  next_cursor:
                    $ref: "#/components/schemas/NextCursor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /orders/{order_id}:
    get:
      tags: [Orders]
      operationId: getOrder
      summary: Get an order
      description: One order, with items, totals and status history.
      security:
        - bearerAuth: [orders:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
        - name: order_id
          in: path
          required: true
          description: The order ID (`ord_…`).
          schema:
            type: string
            examples: [ord_01J9QW3T8M]
      responses:
        "200":
          description: The order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /sandbox/orders:
    post:
      tags: [Orders]
      operationId: createSandboxOrder
      summary: Create a test order (sandbox only)
      description: |
        Creates a realistic order on a test location, so you receive real `order.*` events.
        Available on the sandbox server only.
      servers:
        - url: https://sandbox.partners.loops.sa/v1
          description: Sandbox
      security:
        - bearerAuth: [orders:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [location_id]
              properties:
                location_id:
                  type: string
                  examples: [loc_2Xf9K]
                channel:
                  type: string
                  description: Simulated channel to create the order on. Defaults to the location's first channel connection.
      responses:
        "201":
          description: The test order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /events:
    get:
      tags: [Events]
      operationId: listEvents
      summary: List events
      description: |
        Events for the current connection, oldest first, starting after `after`.
        Use it to recover after downtime. Events are kept for 30 days.
      security:
        - bearerAuth: [events:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
        - name: after
          in: query
          description: Return events after this event ID. Omit to start from the oldest retained event.
          schema:
            type: string
            examples: [evt_01J9C2X4RS]
        - name: types
          in: query
          description: Comma-separated event types; wildcards such as `order.*` are allowed.
          schema:
            type: string
            examples: ["order.*,connection.revoked"]
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: A page of events.
          content:
            application/json:
              schema:
                type: object
                required: [data, has_more]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Event"
                  has_more:
                    type: boolean
                    description: Whether more events exist after the last one returned.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /events/{event_id}:
    get:
      tags: [Events]
      operationId: getEvent
      summary: Get an event
      description: One event by ID.
      security:
        - bearerAuth: [events:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
        - $ref: "#/components/parameters/EventId"
      responses:
        "200":
          description: The event.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Event"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /events/{event_id}/redeliver:
    post:
      tags: [Events]
      operationId: redeliverEvent
      summary: Redeliver an event
      description: Sends the event to your webhook endpoints again.
      security:
        - bearerAuth: [events:read]
      parameters:
        - $ref: "#/components/parameters/MerchantId"
        - $ref: "#/components/parameters/EventId"
      responses:
        "202":
          description: Redelivery queued.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

webhooks:
  order.created:
    post:
      tags: [Orders]
      operationId: onOrderCreated
      summary: A new order arrived
      description: Sent when a new order arrives from any channel. `data` contains the full order.
      parameters:
        - $ref: "#/components/parameters/WebhookId"
        - $ref: "#/components/parameters/WebhookTimestamp"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/Event"
                - type: object
                  properties:
                    data:
                      $ref: "#/components/schemas/Order"
      responses:
        "200":
          description: Return any 2xx within 10 seconds to acknowledge.
  order.status_changed:
    post:
      tags: [Orders]
      operationId: onOrderStatusChanged
      summary: An order's status changed
      description: Sent when an order moves to a new status, for example from `placed` to `accepted`.
      parameters:
        - $ref: "#/components/parameters/WebhookId"
        - $ref: "#/components/parameters/WebhookTimestamp"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/Event"
                - type: object
                  properties:
                    data:
                      $ref: "#/components/schemas/OrderStatusChange"
      responses:
        "200":
          description: Return any 2xx within 10 seconds to acknowledge.
  connection.revoked:
    post:
      tags: [Connection]
      operationId: onConnectionRevoked
      summary: A merchant revoked your access
      description: Stop calling the API for this merchant. Further requests return `connection_not_active`.
      parameters:
        - $ref: "#/components/parameters/WebhookId"
        - $ref: "#/components/parameters/WebhookTimestamp"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Event"
      responses:
        "200":
          description: Return any 2xx within 10 seconds to acknowledge.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        An access token from the OAuth 2.0 client credentials grant. Your token URL, client ID
        and client secret are in the Loops Partners portal, under Applications; sandbox and
        production each have their own. Tokens last 15 minutes; request a new one when it expires.

        Scopes listed on an endpoint must be approved for your application and granted by the merchant:
        `merchant:read`, `locations:read`, `orders:read`, `events:read`.

  parameters:
    MerchantId:
      name: Loops-Merchant-Id
      in: header
      required: false
      description: |
        The merchant you are acting for (`mer_…`). Required for platform applications;
        optional for direct integrations, which have a single merchant.
      schema:
        type: string
        examples: [mer_7Hq2Lw8Z]
    LocationId:
      name: location_id
      in: path
      required: true
      description: The location ID (`loc_…`).
      schema:
        type: string
        examples: [loc_2Xf9K]
    EventId:
      name: event_id
      in: path
      required: true
      description: The event ID (`evt_…`).
      schema:
        type: string
        examples: [evt_01J9C2X4RS]
    Limit:
      name: limit
      in: query
      description: Page size, 1–100.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    Cursor:
      name: cursor
      in: query
      description: The `next_cursor` from the previous page.
      schema:
        type: string
    WebhookId:
      name: webhook-id
      in: header
      required: true
      description: Unique delivery ID; equal to the event ID. Use it to ignore duplicates.
      schema:
        type: string
    WebhookTimestamp:
      name: webhook-timestamp
      in: header
      required: true
      description: Unix timestamp (seconds) when the delivery was signed.
      schema:
        type: string
    WebhookSignature:
      name: webhook-signature
      in: header
      required: true
      description: Standard Webhooks signature (`v1,<base64 HMAC-SHA256>`).
      schema:
        type: string

  responses:
    BadRequest:
      description: The request is invalid.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    Unauthorized:
      description: The access token is missing, expired or invalid.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    Forbidden:
      description: |
        Not allowed: the scope isn't approved or granted, the connection is not active,
        or your application is suspended. See `code`.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    NotFound:
      description: Not found, or not accessible with this connection.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    RateLimited:
      description: Too many requests. Wait for the number of seconds in `Retry-After`.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"

  schemas:
    NextCursor:
      type: [string, "null"]
      description: Pass as `cursor` to get the next page; `null` on the last page.

    Problem:
      type: object
      description: RFC 9457 Problem Details with a stable `code`.
      required: [type, title, status, code, request_id]
      properties:
        type:
          type: string
          format: uri
          examples: [https://docs.partners.loops.sa/errors/scope_not_granted]
        title:
          type: string
          examples: [Scope not granted]
        status:
          type: integer
          examples: [403]
        code:
          type: string
          description: Stable machine-readable error code. Switch on this, not on `title`.
          enum:
            - invalid_request
            - merchant_required
            - unauthorized
            - insufficient_scope
            - scope_not_granted
            - connection_not_active
            - application_suspended
            - channel_not_permitted
            - not_found
            - merchant_not_found
            - rate_limited
            - upstream_unavailable
        detail:
          type: string
          examples: [The merchant has not granted orders:read to this application.]
        request_id:
          type: string
          examples: [req_01J9X7P2QK]
        errors:
          type: array
          description: Field errors, for `invalid_request`.
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string

    Money:
      type: object
      description: An amount in minor units (halalas for SAR).
      required: [amount, currency]
      properties:
        amount:
          type: integer
          examples: [4550]
        currency:
          type: string
          description: ISO 4217 code.
          examples: [SAR]

    LocalizedText:
      type: object
      description: Text in the languages the merchant provided.
      properties:
        en:
          type: [string, "null"]
        ar:
          type: [string, "null"]

    Connection:
      type: object
      required: [id, merchant_id, status, scopes, location_ids, created_at]
      properties:
        id:
          type: string
          examples: [con_4Tz8Yp1W]
        merchant_id:
          type: string
          examples: [mer_7Hq2Lw8Z]
        external_ref:
          type: [string, "null"]
          description: Your own reference for this merchant, if you provided one.
          examples: [BRQ-10021]
        status:
          type: string
          enum: [active, revoked]
        scopes:
          type: array
          items:
            type: string
          examples: [[locations:read, orders:read, events:read]]
        location_ids:
          type: array
          items:
            type: string
        include_future_locations:
          type: boolean
          description: Whether locations the merchant adds later are shared automatically.
        created_at:
          type: string
          format: date-time

    Location:
      type: object
      required: [id, merchant_id, name, status, timezone]
      properties:
        id:
          type: string
          examples: [loc_2Xf9K]
        merchant_id:
          type: string
        brand_id:
          type: [string, "null"]
          examples: [brd_9Lk3]
        name:
          $ref: "#/components/schemas/LocalizedText"
        status:
          type: string
          enum: [open, closed, paused]
        timezone:
          type: string
          examples: [Asia/Riyadh]
        address:
          type: object
          properties:
            line1:
              type: [string, "null"]
            city:
              type: [string, "null"]
              examples: [Riyadh]
            country:
              type: string
              examples: [SA]
            latitude:
              type: [number, "null"]
            longitude:
              type: [number, "null"]
        updated_at:
          type: string
          format: date-time

    ChannelConnection:
      type: object
      required: [id, location_id, channel, status]
      properties:
        id:
          type: string
          examples: [chc_5Rm1Pa]
        location_id:
          type: string
        channel:
          type: string
          description: Channel ID from `GET /channels`.
        status:
          type: string
          enum: [active, needs_reauthorization, paused, failing, disconnected]
        updated_at:
          type: string
          format: date-time

    Channel:
      type: object
      required: [id, name, type, status, capabilities]
      properties:
        id:
          type: string
        name:
          type: string
        type:
          type: string
          enum: [delivery, marketplace, ecommerce, logistics]
        status:
          type: string
          enum: [ga, beta, deprecated]
        capabilities:
          type: object
          description: Operation → supported (`true`/`false`) or a qualifier such as `with_reason`.
          additionalProperties:
            oneOf:
              - type: boolean
              - type: string
          examples:
            - order.receive: true
              order.accept: true
              order.cancel: with_reason
        fields:
          type: object
          description: Field availability notes, e.g. masked customer phone numbers.
          additionalProperties:
            type: string

    OrderStatus:
      type: string
      description: Channel-neutral order status.
      enum: [placed, accepted, preparing, ready, picked_up, delivered, rejected, cancelled]

    Order:
      type: object
      required: [id, version, merchant_id, location_id, channel, channel_ref, type, status, items, totals, created_at, updated_at]
      properties:
        id:
          type: string
          examples: [ord_01J9QW3T8M]
        version:
          type: integer
          description: Increases on every change. Use it to discard stale updates.
          examples: [3]
        merchant_id:
          type: string
        location_id:
          type: string
        channel:
          type: string
          description: Channel ID from `GET /channels`.
        channel_connection_id:
          type: string
        channel_ref:
          type: string
          description: The order number shown on the channel, for staff and support.
          examples: [CH-88213]
        type:
          type: string
          enum: [delivery, pickup, dine_in]
        status:
          $ref: "#/components/schemas/OrderStatus"
        status_history:
          type: array
          items:
            type: object
            required: [status, at]
            properties:
              status:
                $ref: "#/components/schemas/OrderStatus"
              at:
                type: string
                format: date-time
        scheduled_for:
          type: [string, "null"]
          format: date-time
        items:
          type: array
          items:
            $ref: "#/components/schemas/OrderItem"
        totals:
          type: object
          required: [subtotal, total]
          properties:
            subtotal:
              $ref: "#/components/schemas/Money"
            discount:
              $ref: "#/components/schemas/Money"
            delivery_fee:
              $ref: "#/components/schemas/Money"
            tax:
              $ref: "#/components/schemas/Money"
            total:
              $ref: "#/components/schemas/Money"
        payment:
          type: object
          properties:
            method:
              type: string
              enum: [online, cash, card_on_delivery]
            paid:
              type: boolean
        notes:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        extensions:
          type: object
          description: Channel-specific fields, keyed by channel ID. Safe to ignore.
          additionalProperties: true

    OrderItem:
      type: object
      required: [name, quantity, unit_price]
      properties:
        name:
          $ref: "#/components/schemas/LocalizedText"
        sku:
          type: [string, "null"]
          examples: [CB-01]
        quantity:
          type: integer
          examples: [2]
        unit_price:
          $ref: "#/components/schemas/Money"
        modifiers:
          type: array
          items:
            type: object
            properties:
              name:
                $ref: "#/components/schemas/LocalizedText"
              quantity:
                type: integer
              unit_price:
                $ref: "#/components/schemas/Money"
        notes:
          type: [string, "null"]

    OrderStatusChange:
      type: object
      required: [order_id, previous_status, status, version]
      properties:
        order_id:
          type: string
        channel:
          type: string
        channel_ref:
          type: string
        previous_status:
          $ref: "#/components/schemas/OrderStatus"
        status:
          $ref: "#/components/schemas/OrderStatus"
        version:
          type: integer

    Event:
      type: object
      required: [id, type, created_at, merchant_id, connection_id, origin, data]
      properties:
        id:
          type: string
          examples: [evt_01J9C2X4RS]
        type:
          type: string
          examples: [order.status_changed]
        created_at:
          type: string
          format: date-time
        merchant_id:
          type: string
        location_id:
          type: [string, "null"]
        connection_id:
          type: string
        origin:
          type: object
          description: What caused the change. Ignore events where `application_id` is your own.
          properties:
            type:
              type: string
              enum: [channel, api, web_app, system]
            application_id:
              type: [string, "null"]
        data:
          type: object
          description: Event-specific payload.
          additionalProperties: true
