openapi: 3.1.0
info:
  title: Conbool Channel API
  version: 1.2.0
  description: |
    Bestellschnittstelle für Distributoren. Endkunden anlegen, Conbool-Produkte
    bestellen, Bestand ändern, Sammelrechnungen und Leistung abrufen.

    - Alle Beträge in Cent, netto, EUR. Prozente in Basispunkten (10000 = 100 %).
    - Artikelnummern `CB-<Familie>-<Produkt>-<Laufzeit><Zahlweise>`, etwa `CB-SUI-S2-1MM`
      (Monatsvertrag) oder `CB-SUI-S2-1YA` (Jahresvertrag, jährlich im Voraus).
      Bestellbar ist, was der Katalog liefert.
    - Conbool Mail gibt es nur als Monatsvertrag.
    - Zertifikate (`CB-CRT-…`): jede Bestellung ist ein eigenes Abo über ein Jahr.
      Menge und Artikel sind danach fest (`422 channel.per_order_quantity_fixed`
      bzw. `channel.subscription_not_changeable`); weitere Zertifikate als neue Bestellung.
    - Jede schreibende Anfrage braucht die Kopfzeile `Idempotency-Key`.
    - Fehler als `application/problem+json` mit festem `code`.
    - Sandbox: Schlüssel `cbk_test_…`, gleiche Adresse, keine Mandanten, keine Rechnungen.

    Anleitung: Conbool-Channel-API-Anbindung.pdf, Fragen an info@conbool.com
  contact:
    name: Conbool Partnerbetreuung
    email: info@conbool.com
servers:
  - url: https://channel-api.conbool.com/v1
security:
  - bearer: []
tags:
  - name: Zugang
  - name: Katalog
  - name: Wiederverkäufer
  - name: Endkunden
  - name: Bestellungen
  - name: Bestand
  - name: Abrechnung
  - name: Leistung
  - name: Webhooks

paths:
  /oauth/token:
    post:
      tags: [Zugang]
      summary: Zugriffstoken holen (OAuth 2.0 Client Credentials)
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type: { type: string, enum: [client_credentials] }
                client_id: { type: string, format: uuid }
                client_secret: { type: string, description: 'API-Schlüssel cbk_live_… oder cbk_test_…' }
      responses:
        '200':
          description: Token für 15 Minuten
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token: { type: string }
                  token_type: { type: string, enum: [Bearer] }
                  expires_in: { type: integer, example: 900 }
                  scope: { type: string }
        default: { $ref: '#/components/responses/Problem' }

  /catalog/articles:
    get:
      tags: [Katalog]
      summary: Bestellbare Artikel mit Listen- und Einkaufspreis
      parameters:
        - name: valid_at
          in: query
          schema: { type: string, format: date }
          description: Stichtag, Vorgabe heute. Künftige Preislisten sind abrufbar, sobald angelegt.
      responses:
        '200':
          description: Artikel
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid_at: { type: string, format: date }
                  price_list_valid_from: { type: [string, 'null'], format: date }
                  next_price_list_valid_from: { type: [string, 'null'], format: date }
                  data: { type: array, items: { $ref: '#/components/schemas/Article' } }
        default: { $ref: '#/components/responses/Problem' }

  /resellers:
    get:
      tags: [Wiederverkäufer]
      summary: Wiederverkäufer auflisten
      parameters: [{ $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/starting_after' }]
      responses:
        '200': { $ref: '#/components/responses/ResellerList' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      tags: [Wiederverkäufer]
      summary: Wiederverkäufer anlegen
      description: Gleiche external_id liefert den vorhandenen (200).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ResellerCreate' }
      responses:
        '201': { $ref: '#/components/responses/Reseller' }
        '200': { $ref: '#/components/responses/Reseller' }
        default: { $ref: '#/components/responses/Problem' }
  /resellers/{id}:
    parameters: [{ $ref: '#/components/parameters/id' }]
    get:
      tags: [Wiederverkäufer]
      summary: Wiederverkäufer lesen
      responses:
        '200': { $ref: '#/components/responses/Reseller' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      tags: [Wiederverkäufer]
      summary: Wiederverkäufer ändern oder sperren
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name: { type: string, minLength: 2, maxLength: 200 }
                vat_id: { type: [string, 'null'] }
                admin_email: { type: [string, 'null'], format: email }
                status: { type: string, enum: [active, suspended] }
      responses:
        '200': { $ref: '#/components/responses/Reseller' }
        default: { $ref: '#/components/responses/Problem' }

  /customers:
    get:
      tags: [Endkunden]
      summary: Endkunden auflisten
      parameters:
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/starting_after' }
        - { name: external_id, in: query, schema: { type: string } }
      responses:
        '200':
          description: Endkunden
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Customer' } }
                  has_more: { type: boolean }
        default: { $ref: '#/components/responses/Problem' }
    post:
      tags: [Endkunden]
      summary: Endkunden anlegen
      description: |
        Legt den Kunden an und stellt den Conbool-Mandanten im Hintergrund bereit.
        Antwort mit Status `provisioning`; fertig mit `active` und Webhook
        `customer.provisioned`. Der Administrator des Kunden bekommt eine
        Einladung und bestätigt AGB und AVV bei der ersten Anmeldung.

        Gehört die Domäne schon einem Conbool-Kunden: 409
        `customer.exists_other_channel`, Wechsel über `/customers/transfer-requests`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CustomerCreate' }
      responses:
        '201': { $ref: '#/components/responses/Customer' }
        '200': { $ref: '#/components/responses/Customer' }
        default: { $ref: '#/components/responses/Problem' }
  /customers/{id}:
    parameters: [{ $ref: '#/components/parameters/id' }]
    get:
      tags: [Endkunden]
      summary: Endkunden lesen
      responses:
        '200': { $ref: '#/components/responses/Customer' }
        default: { $ref: '#/components/responses/Problem' }
  /customers/transfer-requests:
    post:
      tags: [Endkunden]
      summary: Bestehenden Conbool-Kunden übernehmen
      description: |
        Die Inhaber des Kontos bestätigen per Mail. Ohne Bestätigung ändert sich
        nichts. Wirksam zum nächsten Monatsersten, Webhook `transfer.completed`.
        Die Artikel müssen die heutigen Lizenzen des Kunden mindestens decken.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [domain, external_customer_id, lines]
              properties:
                domain: { type: string, example: kunde.de }
                external_customer_id: { type: string }
                reseller_external_id: { type: [string, 'null'] }
                lines: { type: array, minItems: 1, maxItems: 50, items: { $ref: '#/components/schemas/OrderLine' } }
      responses:
        '202':
          description: Anfrage angelegt, wartet auf Bestätigung
        default: { $ref: '#/components/responses/Problem' }

  /orders:
    post:
      tags: [Bestellungen]
      summary: Artikel für einen Endkunden kaufen
      description: |
        Eine Bestellung je Kauf, 1 bis 100 Positionen, jeder Artikel höchstens
        einmal. Ein schon gebuchter Artikel wird über `PATCH /subscriptions/{id}`
        geändert, nicht neu gekauft. Ein Kauf desselben Produkts wandelt eine
        laufende Testphase um.

        201 erledigt, 202 wartet auf Freigabe durch Conbool oder wird wiederholt,
        422 abgelehnt mit Grund. Gleicher Idempotency-Key liefert dieselbe Antwort
        mit `Idempotent-Replayed: true`.
      parameters: [{ $ref: '#/components/parameters/idempotency' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OrderCreate' }
      responses:
        '201': { $ref: '#/components/responses/Order' }
        '200': { $ref: '#/components/responses/Order' }
        '202': { $ref: '#/components/responses/Order' }
        default: { $ref: '#/components/responses/Problem' }
    get:
      tags: [Bestellungen]
      summary: Bestellungen auflisten
      parameters:
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/starting_after' }
        - { name: status, in: query, schema: { $ref: '#/components/schemas/OrderStatus' } }
      responses:
        '200':
          description: Bestellungen
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Order' } }
                  has_more: { type: boolean }
        default: { $ref: '#/components/responses/Problem' }
  /orders/{id}:
    parameters: [{ $ref: '#/components/parameters/id' }]
    get:
      tags: [Bestellungen]
      summary: Bestellung mit Positionen
      responses:
        '200': { $ref: '#/components/responses/Order' }
        default: { $ref: '#/components/responses/Problem' }

  /subscriptions:
    get:
      tags: [Bestand]
      summary: Bestand, für den täglichen Abgleich
      parameters:
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/starting_after' }
        - { name: customer_id, in: query, schema: { type: string, format: uuid } }
        - { name: changed_since, in: query, schema: { type: string, format: date-time } }
        - { name: status, in: query, schema: { type: string, enum: [active, trial, suspended, cancelled, expired] } }
      responses:
        '200':
          description: Abos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Subscription' } }
                  has_more: { type: boolean }
        default: { $ref: '#/components/responses/Problem' }
  /subscriptions/{id}:
    parameters: [{ $ref: '#/components/parameters/id' }]
    get:
      tags: [Bestand]
      summary: Abo lesen
      responses:
        '200': { $ref: '#/components/responses/Subscription' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      tags: [Bestand]
      summary: Menge ändern
      description: |
        Mehrung sofort, tagesgenau berechnet. Minderung zum Laufzeitende
        (`pending_quantity`). Sofort und ohne Kosten: im Fehlbestellungsfenster
        von 7 Tagen nach Laufzeitbeginn, und bis zur Höhe einer Mehrung der
        letzten 7 Tage.
      parameters: [{ $ref: '#/components/parameters/idempotency' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [quantity]
              properties:
                quantity: { type: integer, minimum: 1 }
                external_order_id: { type: [string, 'null'] }
      responses:
        '201': { $ref: '#/components/responses/Order' }
        '202': { $ref: '#/components/responses/Order' }
        default: { $ref: '#/components/responses/Problem' }
  /subscriptions/{id}/upgrade:
    parameters: [{ $ref: '#/components/parameters/id' }, { $ref: '#/components/parameters/idempotency' }]
    post:
      tags: [Bestand]
      summary: Artikel wechseln
      description: Innerhalb der Familie bei gleicher Laufzeit und Zahlweise. Höher sofort und anteilig, niedriger zum Laufzeitende.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [article_no]
              properties:
                article_no: { $ref: '#/components/schemas/ArticleNo' }
                external_order_id: { type: [string, 'null'] }
      responses:
        '201': { $ref: '#/components/responses/Order' }
        default: { $ref: '#/components/responses/Problem' }
  /subscriptions/{id}/cancel:
    parameters: [{ $ref: '#/components/parameters/id' }, { $ref: '#/components/parameters/idempotency' }]
    post:
      tags: [Bestand]
      summary: Kündigen
      description: Zum Laufzeitende. Im Fehlbestellungsfenster und in der Testphase sofort und ohne Kosten.
      responses:
        '201': { $ref: '#/components/responses/Order' }
        default: { $ref: '#/components/responses/Problem' }
  /subscriptions/{id}/suspend:
    parameters: [{ $ref: '#/components/parameters/id' }, { $ref: '#/components/parameters/idempotency' }]
    post:
      tags: [Bestand]
      summary: Sperren
      description: Sperrt das Abo, etwa bei Zahlungsverzug des Endkunden. Bestand und Abrechnung bleiben.
      responses:
        '201': { $ref: '#/components/responses/Order' }
        default: { $ref: '#/components/responses/Problem' }
  /subscriptions/{id}/resume:
    parameters: [{ $ref: '#/components/parameters/id' }, { $ref: '#/components/parameters/idempotency' }]
    post:
      tags: [Bestand]
      summary: Sperre aufheben
      responses:
        '201': { $ref: '#/components/responses/Order' }
        default: { $ref: '#/components/responses/Problem' }

  /billing/preview:
    get:
      tags: [Abrechnung]
      summary: Vorschau eines Monats
      parameters:
        - { name: period, in: query, schema: { type: string, pattern: '^\d{4}-(0[1-9]|1[0-2])$' }, description: 'YYYY-MM, Vorgabe laufender Monat' }
      responses:
        '200':
          description: Voraussichtliche Positionen je Rechnungsempfänger
        default: { $ref: '#/components/responses/Problem' }
  /invoices:
    get:
      tags: [Abrechnung]
      summary: Ausgestellte Sammelrechnungen
      parameters: [{ $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/starting_after' }]
      responses:
        '200':
          description: Rechnungen
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Invoice' } }
                  has_more: { type: boolean }
        default: { $ref: '#/components/responses/Problem' }
  /invoices/{id}:
    parameters: [{ $ref: '#/components/parameters/id' }]
    get:
      tags: [Abrechnung]
      summary: Rechnung lesen
      responses:
        '200':
          description: Rechnung
          content: { application/json: { schema: { $ref: '#/components/schemas/Invoice' } } }
        default: { $ref: '#/components/responses/Problem' }
  /invoices/{id}/xml:
    parameters: [{ $ref: '#/components/parameters/id' }]
    get:
      tags: [Abrechnung]
      summary: E-Rechnung XRechnung 3.0 (CII, EN 16931)
      responses:
        '200': { description: XML, content: { application/xml: {} } }
        default: { $ref: '#/components/responses/Problem' }
  /invoices/{id}/pdf:
    parameters: [{ $ref: '#/components/parameters/id' }]
    get:
      tags: [Abrechnung]
      summary: Sichtfassung als PDF
      responses:
        '200': { description: PDF, content: { application/pdf: {} } }
        default: { $ref: '#/components/responses/Problem' }
  /invoices/{id}/lines:
    parameters:
      - { $ref: '#/components/parameters/id' }
      - { name: format, in: query, schema: { type: string, enum: [json, csv] } }
    get:
      tags: [Abrechnung]
      summary: Alle Positionen, als JSON oder CSV
      responses:
        '200': { description: Positionen }
        default: { $ref: '#/components/responses/Problem' }

  /performance:
    get:
      tags: [Leistung]
      summary: Eigener Stand gegen Ziele und Staffel
      responses:
        '200': { description: MRR, Sitze, Umsatz je Monat, Ziele, Staffel }
        default: { $ref: '#/components/responses/Problem' }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: Webhook-Ziele
      responses:
        '200': { description: Ziele }
    post:
      tags: [Webhooks]
      summary: Webhook-Ziel anlegen
      description: Das Geheimnis zur Prüfung der Signatur steht nur in dieser Antwort.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [url]
              properties:
                url: { type: string, format: uri, pattern: '^https://' }
                events: { type: array, items: { $ref: '#/components/schemas/WebhookEvent' }, description: 'Leer = alle' }
      responses:
        '201': { description: 'Ziel mit secret' }
        default: { $ref: '#/components/responses/Problem' }
  /webhooks/{id}:
    parameters: [{ $ref: '#/components/parameters/id' }]
    delete:
      tags: [Webhooks]
      summary: Webhook-Ziel löschen
      responses:
        '204': { description: gelöscht }
        default: { $ref: '#/components/responses/Problem' }

components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: |
        Zugriffstoken aus /oauth/token oder der API-Schlüssel selbst.
        Produktion zusätzlich nur mit Client-Zertifikat (mTLS) und von freigegebenen IP-Adressen.
  parameters:
    id: { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
    starting_after: { name: starting_after, in: query, schema: { type: string, format: uuid }, description: Kennung des letzten Eintrags der vorigen Seite }
    idempotency:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 8, maxLength: 200 }
      description: Eindeutig je Vorgang. Wiederholung mit gleichem Schlüssel und Inhalt liefert die erste Antwort, mit anderem Inhalt 422.
  responses:
    Problem:
      description: Fehler nach RFC 9457
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    Reseller:
      description: Wiederverkäufer
      content: { application/json: { schema: { $ref: '#/components/schemas/Reseller' } } }
    ResellerList:
      description: Wiederverkäufer
      content:
        application/json:
          schema:
            type: object
            properties:
              data: { type: array, items: { $ref: '#/components/schemas/Reseller' } }
              has_more: { type: boolean }
    Customer:
      description: Endkunde
      content: { application/json: { schema: { $ref: '#/components/schemas/Customer' } } }
    Order:
      description: Bestellung
      content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } }
    Subscription:
      description: Abo
      content: { application/json: { schema: { $ref: '#/components/schemas/Subscription' } } }
  schemas:
    ArticleNo:
      type: string
      pattern: '^CB-[A-Z]{2,4}-[A-Z0-9]{1,8}-((1M|1Y|3Y)(M|A)|TRIAL)$'
      example: CB-SUI-S2-1YA
    Article:
      type: object
      properties:
        article_no: { $ref: '#/components/schemas/ArticleNo' }
        buyer_article_no: { type: [string, 'null'], description: Ihre Artikelnummer, falls hinterlegt }
        name: { type: object, properties: { de: { type: string }, en: { type: string } } }
        family: { type: string }
        term: { type: string, enum: ['1M', '1Y', '3Y', TRIAL] }
        billing: { type: [string, 'null'], enum: [M, A, null] }
        unit: { type: string, enum: [user, mailbox, domain, tenant, gb] }
        min_quantity: { type: integer }
        max_quantity: { type: integer }
        successor_article_no: { type: [string, 'null'] }
        gtin: { type: [string, 'null'] }
        currency: { type: string, enum: [EUR] }
        list_unit_cents: { type: [integer, 'null'], description: Listenpreis je Einheit und Monat der ersten Stufe }
        your_unit_cents: { type: [number, 'null'], description: Ihr Einkaufspreis je Einheit und Monat der ersten Stufe, vor Staffelrabatt }
        price_tiers:
          type: array
          description: Mengenstaffel. Der Preis einer Stufe gilt für die ganze Menge eines Abos ab min_quantity.
          items:
            type: object
            properties:
              min_quantity: { type: integer, minimum: 1 }
              list_unit_cents: { type: integer }
              your_unit_cents: { type: [number, 'null'] }
        next_list_unit_cents: { type: [integer, 'null'], description: Listenpreis der ersten Stufe ab der nächsten Preisliste }
    ResellerCreate:
      type: object
      additionalProperties: false
      required: [external_id, name, country]
      properties:
        external_id: { type: string, maxLength: 120 }
        name: { type: string, minLength: 2, maxLength: 200 }
        country: { type: string, pattern: '^[A-Z]{2}$' }
        vat_id: { type: [string, 'null'] }
        admin_email: { type: [string, 'null'], format: email, description: Bekommt Zugang zu seinen Endkunden, wenn das Portal für Wiederverkäufer eingeschaltet ist }
    Reseller:
      allOf:
        - $ref: '#/components/schemas/ResellerCreate'
        - type: object
          properties:
            id: { type: string, format: uuid }
            status: { type: string, enum: [active, suspended] }
            created_at: { type: string, format: date-time }
    CustomerCreate:
      type: object
      additionalProperties: false
      required: [external_id, company_name, country, primary_domain, admin_email]
      properties:
        external_id: { type: string, maxLength: 120, description: Ihre Kundennummer }
        company_name: { type: string, minLength: 2, maxLength: 200 }
        country: { type: string, pattern: '^[A-Z]{2}$' }
        vat_id: { type: [string, 'null'] }
        primary_domain: { type: string, example: kunde.de }
        admin_email: { type: string, format: email }
        admin_name: { type: [string, 'null'] }
        reseller_external_id: { type: [string, 'null'] }
    Customer:
      type: object
      properties:
        id: { type: string, format: uuid }
        external_id: { type: string }
        company_name: { type: string }
        country: { type: string }
        vat_id: { type: [string, 'null'] }
        primary_domain: { type: string }
        admin_email: { type: string }
        status: { type: string, enum: [pending, provisioning, active, failed, transferred_out] }
        reseller_external_id: { type: [string, 'null'] }
        tenant_id: { type: [string, 'null'], format: uuid }
        created_at: { type: string, format: date-time }
    OrderLine:
      type: object
      additionalProperties: false
      required: [article_no, quantity]
      properties:
        article_no: { $ref: '#/components/schemas/ArticleNo' }
        quantity: { type: integer, minimum: 1 }
    OrderCreate:
      type: object
      additionalProperties: false
      required: [lines]
      description: Genau eines von customer_id oder customer_external_id.
      properties:
        customer_id: { type: string, format: uuid }
        customer_external_id: { type: string }
        external_order_id: { type: [string, 'null'], description: Ihre Bestellnummer, steht auf der Rechnung }
        lines: { type: array, minItems: 1, maxItems: 100, items: { $ref: '#/components/schemas/OrderLine' } }
    OrderStatus:
      type: string
      enum: [received, pending_approval, provisioning, completed, rejected, failed]
    Order:
      type: object
      properties:
        id: { type: string, format: uuid }
        kind: { type: string, enum: [purchase, change, upgrade, cancel, suspend, resume] }
        status: { $ref: '#/components/schemas/OrderStatus' }
        external_order_id: { type: [string, 'null'] }
        customer_id: { type: [string, 'null'] }
        subscription_id: { type: [string, 'null'] }
        reject_code: { type: [string, 'null'] }
        reject_message: { type: [string, 'null'] }
        sandbox: { type: boolean }
        created_at: { type: string, format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        lines: { type: array, items: { type: object } }
    Subscription:
      type: object
      properties:
        id: { type: string, format: uuid }
        customer_id: { type: string, format: uuid }
        customer_external_id: { type: [string, 'null'] }
        article_no: { $ref: '#/components/schemas/ArticleNo' }
        quantity: { type: integer }
        status: { type: string, enum: [active, trial, suspended, cancelled, expired] }
        term_start: { type: string, format: date }
        term_end: { type: string, format: date }
        pending_quantity: { type: [integer, 'null'] }
        pending_article_no: { type: [string, 'null'] }
        cancel_at: { type: [string, 'null'], format: date }
        auto_renew: { type: boolean }
        sandbox: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Invoice:
      type: object
      properties:
        id: { type: string, format: uuid }
        number: { type: string }
        recipient_id: { type: string, format: uuid }
        period: { type: string, example: 2026-11 }
        net_cents: { type: integer }
        tax_cents: { type: integer }
        gross_cents: { type: integer }
        status: { type: string, enum: [issued, paid, void] }
        issued_at: { type: string, format: date-time }
        due_on: { type: string, format: date }
    WebhookEvent:
      type: string
      enum:
        - order.completed
        - order.rejected
        - order.failed
        - order.pending_approval
        - customer.provisioned
        - customer.provisioning_failed
        - subscription.changed
        - subscription.cancelled
        - subscription.suspended
        - subscription.resumed
        - subscription.renewed
        - catalog.updated
        - invoice.issued
        - transfer.completed
        - transfer.rejected
        - contract.tier_changed
    Problem:
      type: object
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        code: { type: string, example: subscription.exists }
        detail: { type: string }
        errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              code: { type: string }
              message: { type: string }
        request_id: { type: string, format: uuid }
