openapi: 3.0.3

info:
  title: Облачная касса
  version: "1.0.0"
  description: |
    Фискализация чеков для интернет-расчётов. Сайт отправляет данные о состоявшемся
    платеже, облако ставит чек в очередь и пробивает его на физической кассе,
    оператор данных отправляет чек покупателю на почту.

    ## Как это работает

    Ответ приходит сразу, до того как чек пробит: касса — физическое устройство и
    тратит на документ пару секунд. Поэтому `POST /receipts` возвращает `202` и
    идентификатор, а фискальные данные приходят вебхуком либо запросом
    `GET /receipts/{receiptId}`.

    ## Идемпотентность

    Заголовок `Idempotency-Key` обязателен, и в него кладут идентификатор заказа.
    Повтор запроса с тем же ключом никогда не создаёт второй чек — возвращается
    результат первого. Поэтому запрос безопасно повторять при любой сетевой ошибке.

    ## Суммы

    В рублях, числом или строкой: `1490`, `1490.90`, `"1490.90"`. Максимум два знака
    после точки. `total` обязан совпадать с суммой позиций и с `payments.electronic`.

    ## Ключ

    Передаётся заголовком `X-Service-Key` и должен оставаться на сервере сайта.
    В HTML или JavaScript на странице его помещать нельзя: он даёт возможность
    пробивать чеки от имени владельца.

servers:
  - url: https://aserfiscal.ru/api/v1

security:
  - ServiceKey: []

tags:
  - name: Чеки
  - name: Кассы
  - name: Справочники

paths:
  /receipts:
    post:
      tags: [Чеки]
      summary: Пробить чек
      description: |
        Вызывается после того, как эквайринг подтвердил оплату. Чек попадает в
        очередь и печатается на кассе организации.
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: Идентификатор заказа. Повтор с тем же ключом не создаёт второй чек.
          schema: { type: string, maxLength: 128 }
          example: order-10024
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReceiptRequest' }
      responses:
        '202':
          description: Принят в очередь. Фискальных данных ещё нет.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReceiptStatus' }
        '200':
          description: Чек по этому ключу уже пробит — возвращается его результат.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReceiptStatus' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409':
          description: Тот же ключ идемпотентности прислан с другим содержимым чека.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422':
          description: К организации не привязано ни одной кассы.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

    get:
      tags: [Чеки]
      summary: Список чеков или поиск по ключу идемпотентности
      parameters:
        - name: idempotencyKey
          in: query
          schema: { type: string }
          description: Вернуть один чек по вашему ключу заказа.
        - name: status
          in: query
          schema: { type: string, enum: [queued, assigned, done, failed, cancelled] }
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 500 }
      responses:
        '200':
          description: Список чеков
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  receipts:
                    type: array
                    items: { $ref: '#/components/schemas/ReceiptStatus' }

  /receipts/{receiptId}:
    get:
      tags: [Чеки]
      summary: Состояние чека
      parameters:
        - name: receiptId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Состояние чека
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReceiptStatus' }
        '404': { $ref: '#/components/responses/NotFound' }

  /devices:
    get:
      tags: [Кассы]
      summary: Состояние касс организации
      responses:
        '200':
          description: Список касс
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  devices:
                    type: array
                    items: { $ref: '#/components/schemas/Device' }

  /settings:
    get:
      tags: [Справочники]
      summary: Что разрешено присылать этой организации
      description: |
        Системы налогообложения, на которые действительно зарегистрированы кассы,
        разрешённые ставки НДС и справочники признаков. Полезно вызвать один раз
        при настройке интеграции.
      responses:
        '200':
          description: Настройки и справочники
          content:
            application/json:
              schema: { type: object }

components:
  securitySchemes:
    ServiceKey:
      type: apiKey
      in: header
      name: X-Service-Key
      description: Ключ сайта из личного кабинета. Хранить только на сервере.

  schemas:
    Amount:
      description: Сумма в рублях, максимум два знака после точки.
      oneOf:
        - type: number
        - type: string
      example: 1490.90

    ReceiptRequest:
      type: object
      required: [customer, items, payments, total]
      properties:
        type:
          type: string
          enum: [sell, sell_refund]
          default: sell
          description: |
            `sell` — приход, `sell_refund` — возврат прихода. Возврат оформляется
            как самостоятельный документ: ссылки на исходный чек нет, право на
            возврат отслеживает ваш учёт.
        customer:
          type: object
          description: Адрес покупателя. Обязателен — на него отправляется чек.
          properties:
            email: { type: string, format: email, example: client@example.com }
            phone: { type: string, example: "+79991234567" }
        items:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: '#/components/schemas/ReceiptItem' }
        payments:
          type: object
          required: [electronic]
          properties:
            electronic:
              $ref: '#/components/schemas/Amount'
              description: Безналичная оплата. Для интернет-расчётов единственный вид.
        total:
          $ref: '#/components/schemas/Amount'
        taxSystem:
          type: string
          enum: [osn, usn_income, usn_income_outcome, esn, patent]
          description: |
            Система налогообложения. Можно не передавать, если задана по умолчанию
            в личном кабинете. В чеке допустима ровно одна.
        externalId:
          type: string
          description: Ваш идентификатор заказа. Вернётся в вебхуке.

    ReceiptItem:
      type: object
      required: [name, price, quantity, sum]
      properties:
        name: { type: string, maxLength: 128, example: "Подписка «Профи», 1 месяц" }
        price: { $ref: '#/components/schemas/Amount' }
        quantity: { type: number, example: 1, description: Допускает дробное, до 3 знаков. }
        sum:
          $ref: '#/components/schemas/Amount'
          description: Итог позиции. Проверяется против price × quantity.
        vat:
          type: string
          enum: [vat20, vat10, vat7, vat5, vat20_120, vat10_110, vat7_107, vat5_105, vat0, none]
          default: none
        paymentMethod:
          type: string
          enum: [full_payment, full_prepayment, prepayment, advance, partial_payment, credit, credit_payment]
          default: full_payment
        paymentObject:
          type: string
          enum: [service, commodity, work, payment, excise, another]
          default: service
        measure:
          type: string
          default: piece

    ReceiptStatus:
      type: object
      properties:
        ok: { type: boolean }
        receiptId: { type: string, example: rcp_7f3a91c4e8b2 }
        status:
          type: string
          enum: [queued, assigned, done, failed, cancelled]
        idempotencyKey: { type: string }
        externalId: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        fiscal:
          type: object
          nullable: true
          description: Появляется только при статусе done.
          properties:
            fiscalDocumentNumber: { type: integer, example: 4412 }
            fiscalSign: { type: string, example: "2846591037" }
            shiftNumber: { type: integer, example: 214 }
            receiptDateTime: { type: string, format: date-time }
            fnSerial: { type: string }
            kktRegNumber: { type: string }
            total: { type: string, example: "1490.90" }
        error:
          type: object
          nullable: true
          description: Появляется только при статусе failed.
          properties:
            code: { type: string }
            message: { type: string }

    Device:
      type: object
      properties:
        deviceId: { type: string }
        label: { type: string, nullable: true }
        online: { type: boolean }
        blocked:
          type: boolean
          description: Касса не прошла проверку и исключена из работы; причина в issues.
        issues:
          type: array
          items:
            type: object
            properties:
              code: { type: string }
              message: { type: string }
              blocking: { type: boolean }
        shift:
          type: object
          properties:
            state: { type: string, enum: [closed, opened, expired, unknown] }
            number: { type: integer, nullable: true }
        fn:
          type: object
          properties:
            serial: { type: string, nullable: true }
            expiresAt: { type: string, nullable: true }
            unsentDocs:
              type: integer
              description: Не переданные оператору документы. Растут без интернета у кассы.
        queueDepth: { type: integer }

    Error:
      type: object
      properties:
        ok: { type: boolean, example: false }
        error: { type: string }
        code: { type: string }

  responses:
    BadRequest:
      description: Чек не прошёл проверку.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Ключ неизвестен или отозван.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Не найдено.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
