openapi: 3.0.3
info:
  title: Beeylo external API
  version: 1.0.0
  description: |
    Use Beeylo's reply technique from your own CRM. Beeylo never sends anything through
    this API: your system shows the reply, the user reviews it, and your system sends it.
    Read README.md next to this file for the full flow and the rules you agree to.
    Writing endpoints take 10 to 30 seconds: use a client timeout of 90 seconds.
servers:
  - url: https://business-api.beeylo.com
security:
  - apiKey: []
paths:
  /v1/mails:
    get:
      operationId: listMails
      summary: Open mails that need a reply
      description: Same selection as the Beeylo app, newest activity first, from the mailboxes this key's user may use.
      parameters:
        - name: limiet
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: voor
          in: query
          description: Only items with laatste_activiteit before this timestamp. Use `volgende` of the previous page.
          schema: { type: string, format: date-time }
      responses:
        '200':
          description: A page of mails
          content:
            application/json:
              schema:
                type: object
                required: [mails, volgende]
                properties:
                  mails:
                    type: array
                    items: { $ref: '#/components/schemas/Mail' }
                  volgende:
                    type: string
                    format: date-time
                    nullable: true
                    description: Pass as `voor` for the next page; null on the last page.
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooMany' }
  /v1/mails/{id}/voorstel:
    post:
      operationId: getProposal
      summary: The questions for the owner, or a ready reply
      description: |
        Returns the proposal Beeylo prepared when the mail arrived (free, instant), or makes a new
        one (10 to 30 seconds). Send `voorstel.vraag_id` back in `schrijven`.
      parameters: [{ $ref: '#/components/parameters/Id' }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                taal: { $ref: '#/components/schemas/Language' }
      responses:
        '200':
          description: The proposal
          content:
            application/json:
              schema:
                type: object
                required: [taal, voorstel]
                properties:
                  taal: { $ref: '#/components/schemas/Language' }
                  voorstel: { $ref: '#/components/schemas/Proposal' }
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/TooMany' }
        '503': { $ref: '#/components/responses/ModelUnavailable' }
  /v1/mails/{id}/schrijven:
    post:
      operationId: writeReply
      summary: Turn the owner's answers into a reply
      parameters: [{ $ref: '#/components/parameters/Id' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WriteRequest' }
      responses:
        '200':
          description: The reply, with the sentences that need checking
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WriteResponse' }
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/TooMany' }
        '503': { $ref: '#/components/responses/ModelUnavailable' }
  /v1/mails/{id}/aanpassen:
    post:
      operationId: adjustReply
      summary: Change a reply with an instruction ("shorter")
      parameters: [{ $ref: '#/components/parameters/Id' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AdjustRequest' }
      responses:
        '200':
          description: The new text
          content:
            application/json:
              schema:
                type: object
                required: [tekst]
                properties:
                  tekst: { type: string }
                  twijfels: { type: array, items: { type: string } }
                  twijfelzinnen:
                    type: array
                    items: { $ref: '#/components/schemas/DoubtfulSentence' }
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/TooMany' }
        '503': { $ref: '#/components/responses/ModelUnavailable' }
  /v1/mails/{id}/afgehandeld:
    post:
      operationId: markHandled
      summary: Mark the mail as handled in Beeylo
      description: Same as ticking it off in the app, including what Beeylo then does in Gmail (such as archiving). Idempotent. In read-only mode nothing changes in the mailbox.
      parameters: [{ $ref: '#/components/parameters/Id' }]
      requestBody:
        required: false
        content:
          application/json:
            schema: { type: object, additionalProperties: false }
      responses:
        '200':
          description: Done
          content:
            application/json:
              schema:
                type: object
                required: [ok, id, status]
                properties:
                  ok: { type: boolean, enum: [true] }
                  id: { type: string, format: uuid }
                  status: { type: string, enum: [afgehandeld] }
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooMany' }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer bk_live_...`. Server to server only; no CORS."
  parameters:
    Id:
      name: id
      in: path
      required: true
      description: Beeylo item id from GET /v1/mails. Unknown or foreign items answer 404.
      schema: { type: string, format: uuid }
  responses:
    Invalid:
      description: ongeldige_invoer (see `velden`), ongeldige_json, or te_groot (413)
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: sleutel_ontbreekt or ongeldige_sleutel
      headers:
        WWW-Authenticate: { schema: { type: string } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: zaak_onbekend (unknown or not yours) or niet_gevonden
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflict:
      description: nieuwe_mail (call voorstel again), geen_antwoord, or geen_tekst
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    TooMany:
      description: te_vaak (60 per minute per key, see Retry-After) or dagcap (daily writing limit)
      headers:
        Retry-After: { schema: { type: integer }, description: Seconds to wait (te_vaak). }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ModelUnavailable:
      description: model_weg. Retry in a minute.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  schemas:
    Language: { type: string, enum: [nl, en], default: nl }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, description: Stable code. }
        message: { type: string }
        velden:
          type: array
          items: { type: string }
          description: Names of the invalid fields (ongeldige_invoer). Never their contents.
    Mail:
      type: object
      required: [id, titel, laatste_activiteit]
      properties:
        id: { type: string, format: uuid }
        titel: { type: string, nullable: true, description: Beeylo's short title of the case. }
        samenvatting: { type: string, nullable: true }
        categorie: { type: string, nullable: true }
        mailbox: { type: string, nullable: true, description: Address of the mailbox the mail came in on. }
        onderwerp: { type: string, nullable: true, description: Subject of the newest incoming mail. }
        van_naam: { type: string, nullable: true }
        van_adres: { type: string, nullable: true }
        ontvangen_op: { type: string, format: date-time, nullable: true }
        message_id:
          type: string
          nullable: true
          description: RFC 822 Message-ID of the newest incoming mail, with angle brackets. Match your own copy on this.
        laatste_activiteit: { type: string, format: date-time }
    Proposal:
      type: object
      required: [modus, beslissingen, vraag_id]
      properties:
        vraag_id: { type: string, description: The customer mail this proposal answers. Send it back in schrijven. }
        modus:
          type: string
          enum: [volledig, stukken, niets]
          description: volledig means a full reply is in `tekst`; the owner decides in the other two.
        tekst: { type: string, nullable: true }
        aanhef: { type: string, nullable: true }
        groet: { type: string, nullable: true }
        klant_taal: { type: string, description: Language of the reply to the customer, when it differs. }
        stukken:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              vraag: { type: string }
              tekst: { type: string }
              bron: { type: string }
              citaat: { type: string, nullable: true }
              nummers: { type: array, items: { type: string } }
        aan_jou: { type: array, items: { type: string } }
        beslissingen:
          type: array
          items: { $ref: '#/components/schemas/Decision' }
        vervolg:
          type: object
          properties:
            opties: { type: array, items: { type: string } }
        koppen: { type: object, additionalProperties: true }
        zelf_melden: { type: boolean }
        een_voor_een: { type: boolean }
        groet_per_persoon: { type: boolean }
      additionalProperties: true
    Decision:
      type: object
      required: [id, label, vraag, soort, opties]
      properties:
        id: { type: string, example: b1 }
        label: { type: string }
        vraag: { type: string, description: The question for the owner. }
        soort: { type: string, enum: [ja_nee, keuze, datum, tekst] }
        opties: { type: array, items: { type: string }, description: The choices when soort is keuze. }
        alleen_zaterdag: { type: boolean }
        citaat: { type: string, nullable: true, description: Quote from the customer's mail. }
        uitleg:
          type: object
          additionalProperties: { type: string }
          description: Per option a short line for under the option.
        vervolg:
          type: object
          properties:
            opties: { type: array, items: { type: string } }
        nummers: { type: array, items: { type: string } }
        detail: { type: string, enum: [wanneer, hoeveel], description: After a Yes, ask for the moment or the amount. }
        tijden: { type: array, items: { type: string, nullable: true } }
        vraag_tijd: { type: string, nullable: true }
    WriteRequest:
      type: object
      additionalProperties: false
      required: [vraag_id, antwoorden]
      properties:
        vraag_id: { type: string, minLength: 1, maxLength: 100 }
        taal: { $ref: '#/components/schemas/Language' }
        antwoord_taal: { type: string, pattern: '^[a-z]{2}$', description: Force the language of the reply. }
        aanhef: { type: string, maxLength: 200 }
        groet: { type: string, maxLength: 200 }
        antwoorden:
          type: array
          maxItems: 40
          description: May be empty when aanwijzingen or vervolg is given.
          items:
            type: object
            additionalProperties: false
            required: [id, vraag, antwoord]
            properties:
              id: { type: string, minLength: 1, maxLength: 40 }
              vraag: { type: string, maxLength: 200 }
              antwoord: { type: string, maxLength: 20000 }
              soort: { type: string, enum: [ja_nee, keuze, datum, tekst] }
              toelichting: { type: string, maxLength: 20000 }
              vervolg: { type: string, maxLength: 80 }
              nummers: { type: array, maxItems: 6, items: { type: string, maxLength: 12 } }
              citaat: { type: string, maxLength: 300 }
              label: { type: string, maxLength: 80 }
              detail:
                type: object
                additionalProperties: false
                required: [soort]
                properties:
                  soort: { type: string, enum: [wanneer, hoeveel] }
                  tekst: { type: string, maxLength: 80 }
        overgeslagen: { type: array, maxItems: 40, items: { type: string, maxLength: 80 } }
        aanwijzingen:
          type: array
          maxItems: 48
          items:
            type: object
            additionalProperties: false
            required: [vraag, tekst]
            properties:
              vraag: { type: string, maxLength: 200 }
              tekst: { type: string, maxLength: 20000 }
        vervolg: { type: string, maxLength: 80 }
        maak_mail: { type: boolean, description: After stijf true, one more attempt at a natural mail. }
    WriteResponse:
      type: object
      required: [klant_taal, tekst, twijfels, twijfelzinnen, stijf, ontbreekt]
      properties:
        klant_taal: { type: string }
        tekst: { type: string, description: The reply, for the user to review and edit. }
        twijfels:
          type: array
          items: { type: string }
          description: Review points in plain language. Show them above the text.
        twijfelzinnen:
          type: array
          items: { $ref: '#/components/schemas/DoubtfulSentence' }
          description: Sentences Beeylo is not sure about ("Klopt dit?"). Always mark them and have the user confirm or remove them.
        stijf: { type: boolean, description: A literal, stiff version. Offer maak_mail. }
        ontbreekt:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              vraag: { type: string }
              antwoord: { type: string }
              melding: { type: string }
        herkomst:
          type: array
          items:
            type: object
            required: [zin, bron]
            properties:
              zin: { type: string }
              bron: { type: string, enum: [jij, kennis, beeylo] }
              beleefd: { type: boolean }
        bijlage_voorstel: { type: boolean, description: The reply says a document is attached. }
    AdjustRequest:
      type: object
      additionalProperties: false
      required: [concept, opdracht]
      properties:
        taal: { $ref: '#/components/schemas/Language' }
        concept: { type: string, description: The text as it is now. }
        opdracht: { type: string, maxLength: 20000, description: 'What should change, in free text ("korter").' }
        antwoord_taal: { type: string, pattern: '^[a-z]{2}$' }
        vraag_id: { type: string, minLength: 1, maxLength: 100, description: With it, a newer customer mail gives 409 nieuwe_mail. }
        sleutel: { type: string, format: uuid, description: Makes a retry return the result of the first call. }
    DoubtfulSentence:
      type: object
      required: [zin]
      properties:
        zin: { type: string, description: The sentence exactly as it appears in the text. }
