openapi: 3.1.0
info:
  title: Beezr Records API
  version: "2026-10-18"
  summary: Create, read, search, update and delete records in your Beezr workspace, with files.
  description: |
    The Beezr Records API lets your systems work with the records in your Beezr workspace.
    Every endpoint is a POST with a JSON body, authenticated with the `X-API-Key` header.
    Writes live under `/api/v1/records`, reads under `/api/v4/records`.
    Every JSON response uses the same envelope: `{ "success": true, "data": ..., "meta": ... }`
    or `{ "success": false, "error": { "code": ..., "message": ..., "details": ... } }`.
  contact:
    email: sales@beezr.io
servers:
  - url: https://services.beezr.io/generic
    description: Beezr API
security:
  - ApiKey: []
tags:
  - name: Records
    description: Create, update and delete records, one at a time or up to 100 per call.
  - name: Read and search
    description: Query records with filters, sorting and paging, or get one record by id.
  - name: Files
    description: Download a file attached to a record. Upload files inside create and update.

paths:
  /api/v1/records/create:
    post:
      tags: [Records]
      operationId: createRecord
      summary: Create a record
      description: Creates one record in a module. Fields you don't send stay empty. Send an `Idempotency-Key` to make retries safe.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateRequest" }
            example:
              module: cm_equipment_requests_base
              data: { title: "Laptop for new hire", priority: 2 }
      responses:
        "201":
          description: Created. `data` is the new record.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RecordEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/ModuleNotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/Refused" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/OperationFailed" }

  /api/v1/records/bulk_create:
    post:
      tags: [Records]
      operationId: bulkCreateRecords
      summary: Create up to 100 records
      description: Creates each record in order. Items succeed or fail independently; failed items are listed in `failures` with their index.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BulkCreateRequest" }
            example:
              module: cm_equipment_requests_base
              dataList:
                - { title: "Laptop", priority: 2 }
                - { title: "Monitor", priority: 3 }
      responses:
        "201":
          description: Processed. Check `failed` and `failures`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data: { $ref: "#/components/schemas/BulkCreateResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/ModuleNotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /api/v1/records/update:
    post:
      tags: [Records]
      operationId: updateRecord
      summary: Update a record
      description: Changes only the fields you send. Returns the record as it is after the update. Send an `Idempotency-Key` to make retries safe.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateRequest" }
            example:
              module: cm_equipment_requests_base
              id: 482913
              data: { priority: 1 }
      responses:
        "200":
          description: Updated. `data` is the record after the update.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RecordEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/ModuleNotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/Refused" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/OperationFailed" }

  /api/v1/records/bulk_update:
    post:
      tags: [Records]
      operationId: bulkUpdateRecords
      summary: Update up to 100 records
      description: Applies each update in order. Items succeed or fail independently; failed items are listed in `failures` with their id.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BulkUpdateRequest" }
            example:
              module: cm_equipment_requests_base
              updates:
                - { id: 482913, data: { priority: 1 } }
                - { id: 482914, data: { priority: 1 } }
      responses:
        "200":
          description: Processed. Check `failed` and `failures`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data: { $ref: "#/components/schemas/BulkUpdateResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/ModuleNotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /api/v1/records/delete:
    post:
      tags: [Records]
      operationId: deleteRecord
      summary: Delete a record
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DeleteRequest" }
            example: { module: cm_equipment_requests_base, id: 482913 }
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      deleted: { type: boolean, const: true }
                      id: { type: integer }
              example: { success: true, data: { deleted: true, id: 482913 } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/ModuleNotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/Refused" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/OperationFailed" }

  /api/v1/records/bulk_delete:
    post:
      tags: [Records]
      operationId: bulkDeleteRecords
      summary: Delete up to 100 records
      description: Deletes each record in order. Items succeed or fail independently.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BulkDeleteRequest" }
            example: { module: cm_equipment_requests_base, ids: [482913, 482914] }
      responses:
        "200":
          description: Processed. Check `failed` and `failures`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data: { $ref: "#/components/schemas/BulkDeleteResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/ModuleNotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /api/v4/records/query:
    post:
      tags: [Read and search]
      operationId: queryRecords
      summary: Query records
      description: |
        Returns a page of records that match your filters, in the order you choose.
        Pages hold 1 to 100 records (default 20). There are more pages while `offset + returned < total`.
        File fields come back as references; download a file with `/api/v4/records/content`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QueryRequest" }
            example:
              module: cm_equipment_requests_base
              fields: [title, priority, needed_by]
              filters:
                and:
                  - { field: priority, operator: lte, value: 2 }
                  - { field: title, operator: contains, value: laptop }
              sort: [ { field: needed_by, direction: asc } ]
              limit: 20
              offset: 0
      responses:
        "200":
          description: A page of records.
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data: { type: array, items: { $ref: "#/components/schemas/Record" } }
                  meta: { $ref: "#/components/schemas/PageMeta" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/ModuleNotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/OperationFailed" }

  /api/v4/records/get:
    post:
      tags: [Read and search]
      operationId: getRecord
      summary: Get a record
      description: Returns one record by its id.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GetRequest" }
            example: { module: cm_equipment_requests_base, id: 482913 }
      responses:
        "200":
          description: The record.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RecordEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404":
          description: "`MODULE_NOT_FOUND` or `NOT_FOUND` (no record with that id)."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /api/v4/records/content:
    post:
      tags: [Files]
      operationId: downloadFile
      summary: Download a file
      description: |
        Returns the bytes of one file attached to a record. Take `unstructuredId` and `module`
        from the file reference in the record, and use the record's `id` as `objectId`.
        Files are up to 10 MB.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContentRequest" }
            example:
              unstructuredId: "a1b2c3d4"
              objectId: 482913
              unstructuredModel: { module: "cm_equipment_requests_base" }
      responses:
        "200":
          description: The file bytes.
          headers:
            Content-Disposition: { description: 'attachment; filename="..."', schema: { type: string } }
            X-Beezr-File-Name: { description: The file name, schema: { type: string } }
            X-Beezr-File-Extension: { description: The file extension, schema: { type: string } }
          content:
            application/octet-stream: { schema: { type: string, format: binary } }
            application/pdf: { schema: { type: string, format: binary } }
            image/png: { schema: { type: string, format: binary } }
            image/jpeg: { schema: { type: string, format: binary } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404":
          description: "`CONTENT_NOT_FOUND`: the file is not attached to that record."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
        "413":
          description: "`CONTENT_TOO_LARGE`: the file is over 10 MB."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Create keys in Beezr Designer under Account settings → API Integration. A key acts for your whole workspace; keep it on your server.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: "1 to 128 visible ASCII characters. Repeat the same request with the same key and Beezr returns the first result instead of running it again (header `Idempotency-Replayed: true`)."
      schema: { type: string, minLength: 1, maxLength: 128 }

  headers:
    X-RateLimit-Limit: { description: Read calls allowed per key per hour, schema: { type: integer } }
    X-RateLimit-Remaining: { description: Read calls left in this hour for this key, schema: { type: integer } }
    X-RateLimit-Reset: { description: When the hourly read window resets, in epoch milliseconds, schema: { type: integer } }
    Retry-After: { description: Seconds to wait before retrying, schema: { type: integer } }

  schemas:
    ModuleName:
      type: string
      description: The module's table name, shown under the module name on its card in Beezr Designer (for example `cm_equipment_requests_base`).
      examples: [cm_equipment_requests_base]
    FieldValues:
      type: object
      description: |
        Field values keyed by each field's System name (Designer → module → Fields).
        Lookup, relation and user fields take the id of the chosen item. File fields take a FileUpload object (or an array for multi-file fields).
        Fields not in the module are ignored.
      additionalProperties: true
    FileUpload:
      type: object
      required: [fileName, data]
      properties:
        fileName: { type: string, examples: [contract.pdf] }
        contentType: { type: string, description: Informational, examples: [application/pdf] }
        data: { type: string, format: byte, description: Base64 file bytes, up to 10 MB decoded }
    FileReference:
      type: object
      description: A file attached to a record, as returned by v4 reads.
      properties:
        unstructuredId: { type: string }
        fieldName: { type: string }
        fileName: { type: string }
        fileExtension: { type: string }
        fileSize: { type: integer }
        module: { type: string }
        objectId: { type: integer, description: Use the record's id when downloading. }
        context: { type: string }
    Record:
      type: object
      description: A record. `id` is the record id; every other property is a field keyed by its System name.
      properties:
        id: { type: integer }
      additionalProperties: true
    RecordEnvelope:
      type: object
      properties:
        success: { type: boolean, const: true }
        data: { $ref: "#/components/schemas/Record" }
    PageMeta:
      type: object
      properties:
        total: { type: integer, description: Records that match }
        limit: { type: integer }
        offset: { type: integer }
        returned: { type: integer, description: Records in this page }
    CreateRequest:
      type: object
      required: [module, data]
      properties:
        module: { $ref: "#/components/schemas/ModuleName" }
        data: { $ref: "#/components/schemas/FieldValues" }
    BulkCreateRequest:
      type: object
      required: [module, dataList]
      properties:
        module: { $ref: "#/components/schemas/ModuleName" }
        dataList: { type: array, minItems: 1, maxItems: 100, items: { $ref: "#/components/schemas/FieldValues" } }
    UpdateRequest:
      type: object
      required: [module, id, data]
      properties:
        module: { $ref: "#/components/schemas/ModuleName" }
        id: { type: integer, minimum: 1 }
        data: { $ref: "#/components/schemas/FieldValues" }
    BulkUpdateRequest:
      type: object
      required: [module, updates]
      properties:
        module: { $ref: "#/components/schemas/ModuleName" }
        updates:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            required: [id, data]
            properties:
              id: { type: integer, minimum: 1 }
              data: { $ref: "#/components/schemas/FieldValues" }
    DeleteRequest:
      type: object
      required: [module, id]
      properties:
        module: { $ref: "#/components/schemas/ModuleName" }
        id: { type: integer, minimum: 1 }
    BulkDeleteRequest:
      type: object
      required: [module, ids]
      properties:
        module: { $ref: "#/components/schemas/ModuleName" }
        ids: { type: array, minItems: 1, maxItems: 100, items: { type: integer } }
    GetRequest:
      type: object
      required: [module, id]
      properties:
        module: { $ref: "#/components/schemas/ModuleName" }
        id: { type: integer, minimum: 1 }
        fields: { type: array, items: { type: string }, description: Return only these fields (id always comes back) }
    QueryRequest:
      type: object
      required: [module]
      properties:
        module: { $ref: "#/components/schemas/ModuleName" }
        fields: { type: array, items: { type: string }, description: Return only these fields (id always comes back) }
        filters: { $ref: "#/components/schemas/FilterGroup" }
        sort:
          type: array
          items:
            type: object
            required: [field]
            properties:
              field: { type: string }
              direction: { type: string, enum: [asc, desc], default: asc }
        limit: { type: integer, minimum: 1, maximum: 100, default: 20, description: Values outside 1–100 are clamped }
        offset: { type: integer, minimum: 0, default: 0 }
    FilterGroup:
      type: object
      description: |
        `and`: every condition must match. `or`: at least one must match (the OR group is ANDed with the rest).
        `groups`: nested groups, each ANDed with the rest.
      properties:
        and: { type: array, items: { $ref: "#/components/schemas/FilterCondition" } }
        or: { type: array, items: { $ref: "#/components/schemas/FilterCondition" } }
        groups: { type: array, items: { $ref: "#/components/schemas/FilterGroup" } }
    FilterCondition:
      type: object
      required: [field, operator]
      properties:
        field: { type: string, description: A field's System name }
        operator:
          type: string
          enum: [eq, ne, gt, gte, lt, lte, in, not_in, contains, starts_with, ends_with, is_null, is_not_null]
        value:
          description: Not used by is_null / is_not_null. An array for in / not_in.
    ContentRequest:
      type: object
      required: [unstructuredId, objectId, unstructuredModel]
      properties:
        unstructuredId: { type: string, description: From the file reference }
        objectId: { type: integer, description: The record's id }
        unstructuredModel:
          type: object
          required: [module]
          properties:
            module: { type: string, description: From the file reference }
    BulkFailure:
      type: object
      properties:
        index: { type: integer, description: Position in dataList (bulk create) }
        id: { type: integer, description: Record id (bulk update and delete) }
        data: { type: object }
        error: { type: string }
    BulkCreateResult:
      type: object
      properties:
        total: { type: integer }
        created: { type: integer }
        failed: { type: integer }
        records: { type: array, items: { $ref: "#/components/schemas/Record" } }
        failures: { type: array, items: { $ref: "#/components/schemas/BulkFailure" } }
    BulkUpdateResult:
      type: object
      properties:
        total: { type: integer }
        updated: { type: integer }
        failed: { type: integer }
        records: { type: array, items: { $ref: "#/components/schemas/Record" } }
        failures: { type: array, items: { $ref: "#/components/schemas/BulkFailure" } }
    BulkDeleteResult:
      type: object
      properties:
        total: { type: integer }
        deleted: { type: integer }
        failed: { type: integer }
        ids: { type: array, items: { type: integer } }
        failures: { type: array, items: { $ref: "#/components/schemas/BulkFailure" } }
    Error:
      type: object
      properties:
        code: { type: string, examples: [MODULE_NOT_FOUND] }
        message: { type: string }
        details: {}
    ErrorEnvelope:
      type: object
      properties:
        success: { type: boolean, const: false }
        error: { $ref: "#/components/schemas/Error" }

  responses:
    BadRequest:
      description: "`MISSING_REQUIRED_FIELD`, `INVALID_DATA`, `INVALID_FILTER`, `INVALID_SORT`, `INVALID_IDEMPOTENCY_KEY`, `KEY_FIELD_UNRESOLVED`, or a rejected file: `INVALID_CONTENT_PAYLOAD`, `CONTENT_DECODE_FAILED`, `CONTENT_TOO_LARGE` (over 10 MB). File errors name the field in the message."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          example: { success: false, error: { code: INVALID_FILTER, message: One or more filters are invalid } }
    Unauthorized:
      description: "`AUTHENTICATION_FAILED`: the key is missing, invalid or paused."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          example: { success: false, error: { code: AUTHENTICATION_FAILED, message: Invalid or missing API key } }
    ModuleNotFound:
      description: "`MODULE_NOT_FOUND`: check the module's table name."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    IdempotencyConflict:
      description: "`CREATE_FAILED` / `UPDATE_FAILED`: a record with this key already exists (`error.message` says so). Or an idempotency conflict: `IDEMPOTENCY_KEY_REUSED`, `IDEMPOTENCY_IN_PROGRESS` (with `Retry-After: 2`) or `IDEMPOTENCY_RESULT_UNKNOWN`."
      headers:
        Retry-After: { $ref: "#/components/headers/Retry-After" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    PayloadTooLarge:
      description: "`PAYLOAD_TOO_LARGE`: more than 100 items."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    TooManyRequests:
      description: "`RATE_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_REQUESTS`: wait `Retry-After` seconds, then retry. `MONTHLY_QUOTA_EXCEEDED`: the plan's monthly API calls are used up; don't retry until the 1st of next month (or until the plan is upgraded)."
      headers:
        Retry-After: { $ref: "#/components/headers/Retry-After" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          example: { success: false, error: { code: RATE_LIMIT_EXCEEDED, message: Too many requests for this account. Retry in 15s. } }
    Refused:
      description: "`CREATE_FAILED`, `UPDATE_FAILED` or `DELETE_FAILED`: a rule in your workspace or a plan limit refused the change. `error.message` says why. Fix the data; retrying the same request gives the same answer."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    OperationFailed:
      description: "`CREATE_FAILED`, `UPDATE_FAILED`, `DELETE_FAILED`, `QUERY_FAILED` or `GET_FAILED` with a general message: a problem on our side. Retry once."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
