openapi: 3.1.0
info:
  title: MTPL Policy Demo API
  description: 'A **fully synthetic** mandatory motor liability (MTPL) insurance API,
    run as a

    live teaching aid for analyst workshops. Every client, vehicle, policy and

    person in it is fictional by construction — the insurer has no name on purpose.


    **Getting started:** call `GET /mtpl/v1/coverage-options` (public), then trade

    your workshop credentials for a bearer token at `POST /mtpl/v1/auth/token` and

    create a policy with `POST /mtpl/v1/policies`.


    **Errors** always use RFC 9457 problem details (`application/problem+json`).

    **Rate limits:** 60 requests/minute per client address across the API

    (`RateLimit-*` headers on every response); `GET /mtpl/v1/limited/ping` allows

    only 5/minute so you can watch a 429 happen.


    **One rule to remember:** a vehicle can hold only one active policy across the

    whole register — violations return `409` with problem type

    `…/duplicate-policy`.

    '
  contact:
    name: Workshop host
  license:
    name: Synthetic demo — no real data
  version: 1.0.0
paths:
  /mtpl/v1/coverage-options:
    get:
      tags:
      - Products
      summary: List MTPL coverage options
      description: Returns the mandatory motor liability (MTPL) product catalogue
        of the demo insurer. Public — no authentication required. This is usually
        the first successful call a workshop participant makes.
      operationId: coverage_options_mtpl_v1_coverage_options_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CoverageOptionsResponse'
  /mtpl/v1/demo-clients:
    post:
      tags:
      - Demo clients
      summary: Create throwaway demo credentials (no auth required)
      description: 'Creates a throwaway client id and secret so you can complete the
        workshop

        without waiting for an in-session handout. No sign-up, no email, no auth.


        Feed the returned `client_id` and `client_secret` straight into

        `POST /mtpl/v1/auth/token`.


        The credentials expire, but **using them extends the deadline** — an active

        session is never cut off mid-exercise. An abandoned one is removed along with

        its policies.


        Your policies are yours alone. The shared `fleet.demo` account is visible
        to

        the one-active-policy rule, which is why your first attempt on a seeded vehicle

        like `ABC-101` returns a 409 naming a policy you cannot read: that asymmetry
        is

        how a real national register behaves, and it is deliberate.'
      operationId: create_demo_client_mtpl_v1_demo_clients_post
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DemoClientResponse'
  /mtpl/v1/auth/token:
    post:
      tags:
      - Authentication
      summary: Get a bearer token (client credentials)
      description: Exchanges demo client credentials for a short-lived (≤ 60 min)
        signed JWT bearer token. The token is a real HS256 JWT — paste it into jwt.io
        to inspect the sub, scope and exp claims. Send credentials as JSON or as a
        form body; camelCase keys (clientId/clientSecret) are accepted too.
      operationId: create_token_mtpl_v1_auth_token_post
      requestBody:
        content:
          application/json:
            schema:
              properties:
                client_id:
                  type: string
                  description: 'Workshop client id: analyst-01 … analyst-20 or fleet.demo.'
                  example: analyst-01
                client_secret:
                  type: string
                  description: Shared workshop secret (handed out in-session).
                  example: <workshop secret>
                grant_type:
                  type: string
                  description: Optional; must be 'client_credentials' when present.
                  example: client_credentials
              type: object
              required:
              - client_id
              - client_secret
          application/x-www-form-urlencoded:
            schema:
              properties:
                client_id:
                  type: string
                  description: 'Workshop client id: analyst-01 … analyst-20 or fleet.demo.'
                  example: analyst-01
                client_secret:
                  type: string
                  description: Shared workshop secret (handed out in-session).
                  example: <workshop secret>
                grant_type:
                  type: string
                  description: Optional; must be 'client_credentials' when present.
                  example: client_credentials
              type: object
              required:
              - client_id
              - client_secret
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
  /mtpl/v1/policies:
    post:
      tags:
      - Policies
      summary: Create an MTPL policy
      description: 'Signs a vehicle up for mandatory motor liability cover, active
        immediately with a one-year term (P1Y). Identify the vehicle by regNumber,
        vin, or both. Business rule: **one active policy per vehicle** across the
        whole register — a conflict returns 409 with problem type `.../duplicate-policy`.'
      operationId: create_policy_mtpl_v1_policies_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PolicyCreate'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Policy'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - bearerAuth: []
    get:
      tags:
      - Policies
      summary: List your policies (paginated)
      description: Lists policies owned by the authenticated client, ordered by id.
        Use `page` (1-based) and `pageSize` (max 100) to page through — the fleet.demo
        account holds 70+ policies, so pagination is not optional there. RateLimit-Limit
        / RateLimit-Remaining / RateLimit-Reset headers accompany every response.
      operationId: list_policies_mtpl_v1_policies_get
      parameters:
      - name: page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          description: 1-based page number.
          default: 1
          title: Page
        description: 1-based page number.
      - name: pageSize
        in: query
        required: false
        schema:
          type: integer
          maximum: 100
          minimum: 1
          description: Items per page (1-100).
          default: 20
          title: Pagesize
        description: Items per page (1-100).
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyList'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - bearerAuth: []
  /mtpl/v1/policies/{policyId}:
    get:
      tags:
      - Policies
      summary: Get one policy
      description: Fetches a single policy you own. Unknown or foreign ids return
        a 404 problem+json.
      operationId: get_policy_mtpl_v1_policies__policyId__get
      parameters:
      - name: policyId
        in: path
        required: true
        schema:
          type: string
          title: Policyid
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Policy'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - bearerAuth: []
    put:
      tags:
      - Policies
      summary: Update a policy
      description: Updates the allowed fields (regNumber, vin, holder) of an in-force
        policy; its status becomes `updated`. Cancelled and expired policies are immutable
        — modifying one returns 409 problem type `.../immutable-policy`. Changing
        the vehicle re-checks the one-active-policy rule.
      operationId: update_policy_mtpl_v1_policies__policyId__put
      parameters:
      - name: policyId
        in: path
        required: true
        schema:
          type: string
          title: Policyid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PolicyUpdate'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Policy'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - bearerAuth: []
  /mtpl/v1/policies/{policyId}/cancel:
    post:
      tags:
      - Policies
      summary: Cancel a policy (idempotent)
      description: 'Cancels an in-force policy. Idempotent by design: cancelling an
        already-cancelled policy returns 200 with the same representation, not an
        error. Expired policies cannot be cancelled (409).'
      operationId: cancel_policy_mtpl_v1_policies__policyId__cancel_post
      parameters:
      - name: policyId
        in: path
        required: true
        schema:
          type: string
          title: Policyid
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Policy'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
      - bearerAuth: []
  /mtpl/v1/limited/ping:
    get:
      tags:
      - Rate limiting
      summary: Deliberately low rate limit (429 demo)
      description: Answers pong — but only 5 times per minute per client address.
        Call it in a quick loop to observe a live 429 problem+json with a Retry-After
        header and the RateLimit-* headers counting down.
      operationId: limited_ping_mtpl_v1_limited_ping_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                additionalProperties: true
                type: object
                title: Response Limited Ping Mtpl V1 Limited Ping Get
  /mtpl/v1/signed/policy-check:
    post:
      tags:
      - Signed requests
      summary: HMAC-signed register check (Workshop 3 showpiece)
      description: 'Checks whether a vehicle currently has an in-force MTPL policy
        anywhere in the register — like a national insurance-register lookup. No bearer
        token; instead the request must carry a message signature: Expected header:
        X-Signature: hex(HMAC-SHA256(shared_secret, raw_body)). Compute an HMAC-SHA256
        over the raw request body — the exact bytes sent, including any whitespace
        — keyed with the workshop HMAC secret, and send the lowercase hex digest in
        the X-Signature header. Wrong or missing signatures get a deliberately verbose
        401 problem+json — this endpoint teaches request signing.'
      operationId: signed_policy_check_mtpl_v1_signed_policy_check_post
      requestBody:
        content:
          application/json:
            schema:
              properties:
                regNumber:
                  type: string
                  example: ABC-101
                vin:
                  type: string
                  example: 5TDEM000000000101
              type: object
              description: Provide regNumber, vin, or both.
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyCheckResult'
      security:
      - hmacSignature: []
components:
  schemas:
    CoverageOption:
      properties:
        code:
          type: string
          title: Code
          examples:
          - MTPL-STD
        name:
          type: string
          title: Name
          examples:
          - Mandatory motor liability
        term:
          type: string
          title: Term
          description: Policy term as an ISO 8601 duration.
          examples:
          - P1Y
      type: object
      required:
      - code
      - name
      - term
      title: CoverageOption
    CoverageOptionsResponse:
      properties:
        options:
          items:
            $ref: '#/components/schemas/CoverageOption'
          type: array
          title: Options
      type: object
      required:
      - options
      title: CoverageOptionsResponse
    DemoClientQuota:
      properties:
        requests_per_day:
          type: integer
          title: Requests Per Day
          examples:
          - 500
        max_policies:
          type: integer
          title: Max Policies
          examples:
          - 50
      type: object
      required:
      - requests_per_day
      - max_policies
      title: DemoClientQuota
    DemoClientResponse:
      properties:
        client_id:
          type: string
          title: Client Id
          examples:
          - demo-a7f3c9
        client_secret:
          type: string
          title: Client Secret
          description: Shown once. Never retrievable again.
        expires_at:
          type: string
          title: Expires At
          description: ISO 8601 UTC. Extends automatically while you keep using it.
          examples:
          - '2026-08-05T12:00:00Z'
        quota:
          $ref: '#/components/schemas/DemoClientQuota'
      type: object
      required:
      - client_id
      - client_secret
      - expires_at
      - quota
      title: DemoClientResponse
      description: 'Snake_case here, unlike the policy schemas.


        Deliberate: these fields feed straight into POST /mtpl/v1/auth/token, whose

        body is client_id / client_secret. Matching that spelling means a learner

        can copy a value across without wondering whether the case matters — which

        is a better teaching outcome than schema-wide consistency, and the reason

        OAuth-shaped payloads are snake_case everywhere.'
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    Policy:
      properties:
        id:
          type: string
          title: Id
          examples:
          - pol-1f3a9c2b
        productCode:
          type: string
          title: Productcode
          examples:
          - MTPL-STD
        regNumber:
          anyOf:
          - type: string
          - type: 'null'
          title: Regnumber
          examples:
          - ABC-103
        vin:
          anyOf:
          - type: string
          - type: 'null'
          title: Vin
          examples:
          - 5TDEM000000000103
        holder:
          type: string
          title: Holder
          examples:
          - Demo Holder 03
        status:
          type: string
          title: Status
          description: 'Lifecycle state: active → updated → cancelled; expired policies
            come from seed data. Cancelled and expired policies are immutable.'
          examples:
          - active
        startDate:
          type: string
          title: Startdate
          examples:
          - '2026-07-10'
        endDate:
          type: string
          title: Enddate
          examples:
          - '2027-07-10'
        createdAt:
          type: string
          title: Createdat
        updatedAt:
          type: string
          title: Updatedat
      type: object
      required:
      - id
      - productCode
      - regNumber
      - vin
      - holder
      - status
      - startDate
      - endDate
      - createdAt
      - updatedAt
      title: Policy
    PolicyCheckResult:
      properties:
        regNumber:
          anyOf:
          - type: string
          - type: 'null'
          title: Regnumber
          examples:
          - ABC-101
        vin:
          anyOf:
          - type: string
          - type: 'null'
          title: Vin
        inForce:
          type: boolean
          title: Inforce
          description: True when any client holds an active or updated MTPL policy
            for this vehicle.
        policyId:
          anyOf:
          - type: string
          - type: 'null'
          title: Policyid
          examples:
          - pol-demo-0001
        policyStatus:
          anyOf:
          - type: string
          - type: 'null'
          title: Policystatus
          examples:
          - active
      type: object
      required:
      - regNumber
      - vin
      - inForce
      - policyId
      - policyStatus
      title: PolicyCheckResult
    PolicyCreate:
      properties:
        regNumber:
          anyOf:
          - type: string
          - type: 'null'
          title: Regnumber
          description: Vehicle registration (licence plate) number.
          examples:
          - ABC-103
        vin:
          anyOf:
          - type: string
          - type: 'null'
          title: Vin
          description: Vehicle identification number (17 characters).
          examples:
          - 5TDEM000000000103
        holder:
          anyOf:
          - type: string
            maxLength: 80
          - type: 'null'
          title: Holder
          description: Policy holder display name. Optional — a synthetic name is
            generated when omitted. Use demo names only.
          examples:
          - Demo Holder 03
      type: object
      title: PolicyCreate
    PolicyList:
      properties:
        items:
          items:
            $ref: '#/components/schemas/Policy'
          type: array
          title: Items
        page:
          type: integer
          title: Page
          examples:
          - 1
        pageSize:
          type: integer
          title: Pagesize
          examples:
          - 20
        total:
          type: integer
          title: Total
          description: Total policies owned by this client.
          examples:
          - 74
      type: object
      required:
      - items
      - page
      - pageSize
      - total
      title: PolicyList
    PolicyUpdate:
      properties:
        regNumber:
          anyOf:
          - type: string
          - type: 'null'
          title: Regnumber
          description: New registration number.
          examples:
          - ABC-104
        vin:
          anyOf:
          - type: string
          - type: 'null'
          title: Vin
          description: New VIN.
        holder:
          anyOf:
          - type: string
            maxLength: 80
          - type: 'null'
          title: Holder
          description: New policy holder display name.
      type: object
      title: PolicyUpdate
    TokenResponse:
      properties:
        access_token:
          type: string
          title: Access Token
        token_type:
          type: string
          title: Token Type
          default: Bearer
        expires_in:
          type: integer
          title: Expires In
          description: Token lifetime in seconds.
          examples:
          - 3600
        scope:
          type: string
          title: Scope
          examples:
          - policies:read policies:write
      type: object
      required:
      - access_token
      - expires_in
      - scope
      title: TokenResponse
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Short-lived workshop JWT from POST /mtpl/v1/auth/token.
    hmacSignature:
      type: apiKey
      in: header
      name: X-Signature
      description: hex(HMAC-SHA256(shared_secret, raw_body)) — see POST /mtpl/v1/signed/policy-check.
tags:
- name: Products
  description: Public product catalogue.
- name: Demo clients
  description: Self-service throwaway credentials — no sign-up, no auth.
- name: Authentication
  description: Demo client-credentials flow issuing short-lived JWTs.
- name: Policies
  description: Create, read, update and cancel MTPL policies. Requires a bearer token;
    you only see your own policies.
- name: Rate limiting
  description: A deliberately throttled endpoint for demonstrating 429s.
- name: Signed requests
  description: HMAC-signed register lookup — Workshop 3 material.
