openapi: 3.1.0
info:
  title: Bolån Radar Data API
  description: |
    The demo API holds <b>real live data</b>, but is limited to three banks: Bluestep, Nordea and Sparbank Sörmland.
    If you would like access to the unlimited API, with data from <b>over 80 banks, spanning from 1980 to today</b>, <a href="/en/contact-us">please reach out to us</a>.
    Every request needs an API key. <a href="/en/data-api?key=create">Sign up for a free key</a> with your email address. The free key is for research and commercial use alike.

    <b>Plans.</b> Every key has the free plan or the paid plan. Ask for the paid plan through the <a href="/en/contact-us">contact form</a>.
    <ul>
    <li><b>Free.</b> <code>/api/v1/banks</code>, <code>/api/v1/municipalities</code>, <code>/api/v1/mfis</code>, <code>/api/v1/latestRates</code>, <code>/api/v1/historicRates/{bankId}/recent</code> and <code>/api/v1/webhooks/eventTypes</code>. <code>latestRates</code> can be up to 1 hour old: all free keys share one copy, which is refreshed at most once an hour. The <code>Age</code> header says how old it is in seconds. <code>recent</code> holds the average rates from January 2016 on and the list and range rates of the last 12 months.</li>
    <li><b>Paid.</b> Everything of the free plan, with fresh <code>latestRates</code>, plus the full history in <code>/api/v1/historicRates/{bankId}</code> and the webhooks. A free key gets 403 on a paid endpoint.</li>
    </ul>

    <b>Your key.</b> The key is shown once, when you open the link in the signup email. Each email address has one key. If you lose the key, ask for a new one on the signup page with the same email address. The new key replaces the old one. Your webhooks and your receiver secret stay the same.

    <b>Webhooks.</b> Instead of polling, you can subscribe one https URL per event type. Bolån Radar then sends a POST with the changed data.
    Subscribe with <code>PUT /api/v1/webhooks/{eventType}</code>. <code>GET /api/v1/webhooks/eventTypes</code> lists the event types. <code>GET /api/v1/webhooks</code> shows your subscriptions and your receiver secret.
    <ul>
    <li><b>Delivery.</b> Delivery is at least once, so the same event can arrive twice. Deduplicate on the <code>X-Request-Id</code> header. Answer with any 2xx status within 10 seconds.</li>
    <li><b>Retries.</b> After any other answer, Bolån Radar retries. The wait doubles from 1 minute up to 6 hours. After 7 days it stops.</li>
    <li><b>Order.</b> Deliveries can arrive out of order. Compare <code>createdAt</code> in the body and <code>validFrom</code> on the rate, and keep the newest.</li>
    <li><b>Signature.</b> <code>X-Bolan-Signature</code> holds <code>sha256=</code> and the hex HMAC-SHA256 of the raw request body, keyed with your receiver secret. Compare in constant time.
    <pre><code>expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    valid = hmac.compare_digest(expected, request.headers["X-Bolan-Signature"])</code></pre></li>
    <li><b>Freshness.</b> Rate events (<code>list_rate.*</code>, <code>range_rate.*</code>) arrive under undisturbed operation less than 30 minutes after the lender publishes. <code>average_rate.*</code> and <code>mfi.added</code> are updated daily. <code>bank.*</code> and <code>mfi.classified</code> arrive when Bolån Radar updates the data.</li>
    </ul>
    Higher frequency and fresher data are available on request, <a href="/en/contact-us">please reach out to us</a>.
    Free access has no SLA. For paid access, we can agree on an SLA.

    <b>Rate limits.</b> Each API key has a limit per minute and a limit per hour, in total and per endpoint. Each client IP address also has limits, checked before the API key.
    The windows follow the clock: a minute resets at :00 seconds, an hour resets on the hour.
    <table>
    <tr><th>Scope</th><th>Per minute</th><th>Per hour</th></tr>
    <tr><td>IP address, all requests</td><td>240</td><td>3000</td></tr>
    <tr><td>API key, all requests</td><td>180</td><td>2000</td></tr>
    <tr><td><code>GET /api/v1/banks</code></td><td>10</td><td>60</td></tr>
    <tr><td><code>GET /api/v1/latestRates</code></td><td>30</td><td>600</td></tr>
    <tr><td><code>GET /api/v1/historicRates/{bankId}</code></td><td>120</td><td>1000</td></tr>
    <tr><td><code>GET /api/v1/historicRates/{bankId}/recent</code></td><td>120</td><td>1000</td></tr>
    <tr><td><code>GET /api/v1/mfis</code></td><td>10</td><td>60</td></tr>
    <tr><td><code>GET /api/v1/municipalities</code></td><td>10</td><td>60</td></tr>
    <tr><td><code>GET /api/v1/webhooks/eventTypes</code></td><td>10</td><td>60</td></tr>
    <tr><td><code>GET /api/v1/webhooks</code></td><td>30</td><td>300</td></tr>
    <tr><td><code>PUT</code> and <code>DELETE /api/v1/webhooks/{eventType}</code>, together</td><td>30</td><td>120</td></tr>
    </table>
    The demo API allows 10 times the API key limits. Its IP limits are the same.
    The API key limits do not apply to the account endpoints and <code>/api/v1/rateCoverage</code>. Only the IP limits apply to rateCoverage. Signup, recover and email change together allow 10 requests per hour per IP address and 3 per hour per email address. Verify allows 5 failed requests per minute and 20 per hour per IP address.
    <ul>
    <li><b>Headers.</b> Every response carries <code>RateLimit-Policy</code> and <code>RateLimit</code>, as defined in <a href="https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/">draft-ietf-httpapi-ratelimit-headers</a>. <code>RateLimit-Policy</code> lists the limits that apply: <code>q</code> is the number of requests, <code>w</code> the window in seconds. <code>RateLimit</code> names the limit closest to running out: <code>r</code> is the number of requests left, <code>t</code> the seconds until it resets.
    <pre><code>RateLimit-Policy: "key-minute";q=180;w=60, "key-hour";q=2000;w=3600
    RateLimit: "key-minute";r=12;t=34</code></pre></li>
    <li><b>Over the limit.</b> The answer is 429 with a <code>Retry-After</code> header and an <code>application/problem+json</code> body that names the limit. Wait <code>Retry-After</code> seconds before the next request. Requests over a limit still count against your other limits.</li>
    <li><b>Higher limits.</b> Higher limits are available on request, <a href="/en/contact-us">please reach out to us</a>.</li>
    </ul>
  version: 1.0.0

servers:
  - url: https://data-api-demo.bolanradar.se
  - url: https://data-api.bolanradar.se
  - url: http://localhost:8080

security:
  - bearerAuth: []

paths:
  /api/v1/banks:
    get:
      operationId: getBanks
      summary: Get all bank metadata
      description: Returns metadata for all banks tracked by Bolån Radar, including lending criteria, discounts, service areas, and rate coverage information. Does not include actual rate data.
      responses:
        "200":
          description: List of all banks with metadata
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Bank"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/municipalities:
    get:
      operationId: getMunicipalities
      summary: Get all Swedish municipalities
      description: Returns all Swedish municipalities as a map of ID to name
      responses:
        "200":
          description: Map of municipality IDs to names
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MunicipalityMap"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/latestRates:
    get:
      operationId: getLatestRates
      summary: Get latest rates for all banks
      description: |
        Returns the most recent list rates, average rates, and range rates for all tracked banks. Each bank may have any combination of rate types depending on what they publish.
        Free keys share one copy of the response, refreshed at most once an hour, so it can be up to 1 hour old. The Age header gives its age in seconds. Paid keys always get fresh data.
      responses:
        "200":
          description: Map of bank IDs to their latest rates
          headers:
            Age:
              description: Only for free keys. Seconds since the shared copy of the response was made, at most 3600.
              schema:
                type: integer
              example: 1520
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LatestRatesMap"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/mfis:
    get:
      operationId: getMfis
      summary: Get all MFI RIAD entries
      description: Returns all Monetary Financial Institutions (MFIs) from the ECB RIAD registry tracked by Bolån Radar.
      responses:
        "200":
          description: List of all MFIs
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Mfi"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/historicRates/{bankId}:
    get:
      operationId: getHistoricRates
      summary: Get historic rates for a bank
      description: Returns all historic rates for a specific bank, grouped by rate type and term. Each term contains a chronologically ordered array of rate entries. Needs the paid plan. Free keys use /api/v1/historicRates/{bankId}/recent.
      parameters:
        - name: bankId
          in: path
          required: true
          description: Unique bank identifier
          schema:
            type: string
          example: "nordea"
      responses:
        "200":
          description: Historic rates grouped by rate type and term
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HistoricRates"
        "404":
          description: Bank not found
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/historicRates/{bankId}/recent:
    get:
      operationId: getRecentHistoricRates
      summary: Get recent historic rates for a bank
      description: Returns the same shape as /api/v1/historicRates/{bankId}, cut to the average rates from 2016-01 on, and the list and range rates that were valid at some time in the last 12 months. Free and paid keys can call it.
      parameters:
        - name: bankId
          in: path
          required: true
          description: Unique bank identifier
          schema:
            type: string
          example: "nordea"
      responses:
        "200":
          description: Recent historic rates grouped by rate type and term
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HistoricRates"
        "404":
          description: Bank not found
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/rateCoverage:
    get:
      operationId: getRateCoverage
      summary: Get the days on which each bank has list rates and range rates
      description: |
        Returns, per bank, the days on which Bolån Radar has at least one list rate or range rate, with the gaps and the per-quarter counts. It holds no rate values.
        A break without a rate is a gap when the next day with a rate is 3 calendar months or more after the first day without one. A shorter break is not listed, but its days do not count in daysCovered.
        Each server keeps the response for 1 hour, so it can be up to 1 hour old. asOf is the day it was computed, in Swedish time.
        The Swedbank sparbanker are not in banks. They use Swedbank's list rates, so their list rate coverage is that of the swedbank row.
        It needs no API key.
      security: []
      responses:
        "200":
          description: Rate coverage of every bank
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CoverageOverview"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/webhooks/eventTypes:
    get:
      operationId: getWebhookEventTypes
      summary: List the webhook event types
      description: Returns every event type that can be subscribed to, with a description and how soon after the real-world change the event is sent.
      responses:
        "200":
          description: All event types
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/EventTypeInfo"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/webhooks:
    get:
      operationId: getWebhooks
      summary: Get the webhook subscriptions of this API key
      description: Returns the subscriptions of the calling API key and its receiver secret. Responses are never cached. Needs the paid plan.
      responses:
        "200":
          description: The subscriptions and the receiver secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookList"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/webhooks/{eventType}:
    parameters:
      - name: eventType
        in: path
        required: true
        description: One of the names returned by /api/v1/webhooks/eventTypes
        schema:
          type: string
        example: list_rate.changed

    put:
      operationId: putWebhook
      summary: Subscribe to an event type
      description: Subscribes the calling API key to the event type, or replaces the URL of its existing subscription. Needs the paid plan.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookSubscribeRequest"
      responses:
        "200":
          description: The subscription
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscription"
        "400":
          description: The event type is unknown, or the URL is not a valid https URL
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

    delete:
      operationId: deleteWebhook
      summary: Unsubscribe from an event type
      description: Removes the subscription of the calling API key for the event type. Needs the paid plan.
      responses:
        "204":
          description: Unsubscribed
        "404":
          description: The API key has no subscription for the event type
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/account/signup:
    post:
      x-internal: true
      operationId: accountSignup
      summary: Sign up for a free API key
      description: |
        Mails a link to the email address. The link opens the signup page, which sends its token to /api/v1/account/verify and shows the new key.
        If the email address has a key already, the mail holds a link to a new key instead, as with /api/v1/account/recover.
        The answer is 202 with the same body, whether the address has a key or not.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccountSignupRequest"
      responses:
        "202":
          $ref: "#/components/responses/AccountAccepted"
        "400":
          $ref: "#/components/responses/AccountBadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/AccountUnavailable"

  /api/v1/account/recover:
    post:
      x-internal: true
      operationId: accountRecover
      summary: Get a new API key for a lost one
      description: Mails a link to a new key when the email address has a key. The new key replaces the old one on the same account, so webhooks and the receiver secret stay. The answer is 202 with the same body, whether the address has a key or not.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccountRecoverRequest"
      responses:
        "202":
          $ref: "#/components/responses/AccountAccepted"
        "400":
          $ref: "#/components/responses/AccountBadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/AccountUnavailable"

  /api/v1/account/changeEmail:
    post:
      operationId: accountChangeEmail
      summary: Change the email address of an API key
      description: Mails a confirm link to the new address. When the link is opened, the key moves to the new address and the old address gets a notice. Nothing is sent when another key uses the new address.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccountChangeEmailRequest"
      responses:
        "202":
          $ref: "#/components/responses/AccountAccepted"
        "400":
          $ref: "#/components/responses/ChangeEmailBadRequest"
        "401":
          description: The API key is missing or not valid
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/AccountUnavailable"

  /api/v1/account/verify:
    post:
      x-internal: true
      operationId: accountVerify
      summary: Carry out the link of an account email
      description: |
        Takes the token from the fragment of a mailed link, after "#t=". A link works once and for 1 hour.
        <ul>
        <li><code>signup</code> verifies the email and returns the new key.</li>
        <li><code>recover</code> replaces the key and returns the new one. The old key stops working.</li>
        <li><code>changeEmail</code> moves the key to the new address and returns no key.</li>
        </ul>
        Show the key to the user once. It cannot be read again.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccountVerifyRequest"
      responses:
        "200":
          description: The link was carried out
          headers:
            Cache-Control:
              description: Always no-store, since the body can hold an API key.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountVerifyResponse"
        "400":
          description: The token is malformed, expired or used
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: Another key uses the new email address of a changeEmail link
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/AccountUnavailable"

webhooks:
  list_rate.changed:
    post:
      operationId: listRateChanged
      summary: list_rate.changed
      description: A bank changed, added or reintroduced the list rate for one term.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListRateChangedEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  list_rate.withdrawn:
    post:
      operationId: listRateWithdrawn
      summary: list_rate.withdrawn
      description: A bank stopped publishing the list rate for one term.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListRateWithdrawnEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  range_rate.changed:
    post:
      operationId: rangeRateChanged
      summary: range_rate.changed
      description: A bank changed or added the rate range for one term.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RangeRateChangedEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  range_rate.withdrawn:
    post:
      operationId: rangeRateWithdrawn
      summary: range_rate.withdrawn
      description: A bank stopped publishing the rate range for one term.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RangeRateWithdrawnEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  average_rate.published:
    post:
      operationId: averageRatePublished
      summary: average_rate.published
      description: A bank published the average rate for one term and month.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AverageRatePublishedEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  average_rate.corrected:
    post:
      operationId: averageRateCorrected
      summary: average_rate.corrected
      description: The average rate for one term and month changed after it was published.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AverageRateCorrectedEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  bank.added:
    post:
      operationId: bankAdded
      summary: bank.added
      description: A new bank joined the tracked banks.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BankAddedEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  bank.updated:
    post:
      operationId: bankUpdated
      summary: bank.updated
      description: The metadata of a bank changed.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BankUpdatedEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  mfi.added:
    post:
      operationId: mfiAdded
      summary: mfi.added
      description: A new MFI appeared in the ECB RIAD registry.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MfiAddedEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

  mfi.classified:
    post:
      operationId: mfiClassified
      summary: mfi.classified
      description: The isMortgageProvider value of an MFI changed.
      parameters:
        - $ref: "#/components/parameters/WebhookRequestId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MfiClassifiedEvent"
      responses:
        "200":
          description: Any 2xx status acknowledges the delivery. Bolån Radar retries on any other status.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key passed as a Bearer token in the Authorization header.

  parameters:
    WebhookRequestId:
      name: X-Request-Id
      in: header
      required: true
      description: The messageId of the delivery. It stays the same on every retry, so receivers deduplicate on it.
      schema:
        type: string
        format: uuid
    WebhookEventType:
      name: X-Bolan-Event-Type
      in: header
      required: true
      description: The event type, the same as the type field in the body.
      schema:
        type: string
      example: list_rate.changed
    WebhookSignature:
      name: X-Bolan-Signature
      in: header
      required: true
      description: sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the receiver secret of the API key.
      schema:
        type: string
      example: sha256=4f9d1c0b4e2a7d0d6f0b5c3e8a1f2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c

  headers:
    RateLimitPolicy:
      description: The limits that apply to the request, per draft-ietf-httpapi-ratelimit-headers. q is the number of requests a limit allows, w its window in seconds.
      schema:
        type: string
      example: '"ip-minute";q=240;w=60, "ip-hour";q=3000;w=3600, "key-minute";q=180;w=60, "key-hour";q=2000;w=3600, "endpoint-minute";q=10;w=60, "endpoint-hour";q=60;w=3600'
    RateLimit:
      description: The limit closest to running out. r is the number of requests left, t the seconds until the limit resets.
      schema:
        type: string
      example: '"endpoint-minute";r=0;t=27'
    RetryAfter:
      description: Seconds to wait before the next request.
      schema:
        type: integer
      example: 27

  responses:
    Forbidden:
      description: The request did not come through the public address of the API, or a free key called an endpoint of the paid plan. The detail field says which. Send requests to https://data-api.bolanradar.se, or to https://data-api-demo.bolanradar.se for the demo API.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    AccountAccepted:
      description: The request was accepted. If the address can get a link, the link is on its way.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/AccountAccepted"
    AccountBadRequest:
      description: A field of the body is missing or not valid, or the Turnstile token was rejected
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    ChangeEmailBadRequest:
      description: A field of the body is missing or not valid
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    AccountUnavailable:
      description: |
        Nothing was sent. Either the abuse limits cannot be checked, or emails failed to send in the last 5 minutes.
        During a mail outage, signup, recover and changeEmail all answer 503 with Retry-After, whether a request would send an email or not.
      headers:
        Retry-After:
          description: Seconds until the mail outage ends. Missing when the abuse limits are the cause.
          schema:
            type: integer
          example: 240
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    TooManyRequests:
      description: A rate limit is exceeded. Wait Retry-After seconds before the next request.
      headers:
        RateLimit-Policy:
          $ref: "#/components/headers/RateLimitPolicy"
        RateLimit:
          $ref: "#/components/headers/RateLimit"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/RateLimitProblem"

  schemas:
    Problem:
      type: object
      description: An RFC 9457 problem.
      required:
        - type
        - title
        - status
        - detail
      properties:
        type:
          type: string
          example: about:blank
        title:
          type: string
          example: Forbidden
        status:
          type: integer
          example: 403
        detail:
          type: string
          example: Direct requests to this server are not allowed. Use https://data-api.bolanradar.se.
    RateLimitProblem:
      type: object
      description: The RFC 9457 problem of a 429. It names the limit that blocked the request.
      required:
        - type
        - title
        - status
        - detail
        - policy
        - limit
        - window
        - retryAfter
      properties:
        type:
          type: string
          example: https://www.bolanradar.se/data-api#rate-limits
        title:
          type: string
          example: Too Many Requests
        status:
          type: integer
          example: 429
        detail:
          type: string
          example: You sent more than 10 requests per minute to GET /api/v1/banks with this API key. The limit resets at 2026-10-11T12:01:00Z.
        policy:
          type: string
          description: The name of the limit, as in the RateLimit-Policy header.
          example: endpoint-minute
        limit:
          type: integer
          description: The number of requests the limit allows per window.
          example: 10
        window:
          type: integer
          description: The window of the limit in seconds.
          example: 60
        retryAfter:
          type: integer
          description: Seconds until the limit resets, the same as the Retry-After header.
          example: 27
    Term:
      type: string
      description: Mortgage binding period
      enum:
        - "3m"
        - "6m"
        - "1y"
        - "2y"
        - "3y"
        - "4y"
        - "5y"
        - "6y"
        - "7y"
        - "8y"
        - "9y"
        - "10y"
        - "15y"
        - "20y"

    DiscountId:
      type: string
      description: |
        Discount type identifier. Possible values:
        - `ENERGY_CLASS_A` - Discount for properties with energy class A rating.
        - `ENERGY_CLASS_B` - Discount for properties with energy class B rating.
        - `ENERGY_CLASS_C` - Discount for properties with energy class C rating.
        - `FASTIGHETSBYRAN_PARTNERSHIP` - Discount for customers who purchase or sell their property through Fastighetsbyrån.
        - `FORETAGARNA_MEMBERS` - Discount for members of Företagarna (Swedish Federation of Business Owners).
        - `HSB_BOSPAR_MEMBERS` - Discount for HSB Bospar savings account holders.
        - `ICA_STAMMIS_DISCOUNT` - Discount for ICA Stammis loyalty program members who shop at ICA and Apotek Hjärtat.
        - `IKEA_FAMILY_MEMBERS` - Discount for IKEA Family members.
        - `LEDARNA_MEMBERS` - Discount for members of Ledarna (Swedish Association for Managers).
        - `LO_MEMBERS` - Discount for members of a trade union that is part of LO.
        - `MILJOBYGGNAD_SILVER_GOLD` - Discount for properties with Miljöbyggnad Silver or Gold certification.
        - `OCCUPATIONAL_PENSION_WITH_BANK` - Discount for customers with occupational pension at this bank.
        - `PASSIVE_HOUSE` - Discount for properties certified according to the Passive House Institute.
        - `SACO_MEMBERS` - Discount for members of a trade union that is part of SACO.
        - `SPP_PENSION_CUSTOMERS` - Discount for SPP pension customers.
        - `SVANEN_CERTIFIED` - Discount for properties with Nordic Swan Ecolabel certification.
        - `SVENSK_FASTIGHETSFORMEDLING_PARTNERSHIP` - Discount for customers who purchase or sell their property through Svensk Fastighetsförmedling.
        - `TCO_MEMBERS` - Discount for members of a trade union that is part of TCO.
        - `VILLAAGARNA_MEMBERS` - Discount for members of Villaägarna (Swedish Homeowners Association).
        - `YOUNG_BORROWER` - Discount for young borrowers, usually up to 30, sometimes 35 years old.
      enum:
        - ENERGY_CLASS_A
        - ENERGY_CLASS_B
        - ENERGY_CLASS_C
        - FASTIGHETSBYRAN_PARTNERSHIP
        - FORETAGARNA_MEMBERS
        - HSB_BOSPAR_MEMBERS
        - ICA_STAMMIS_DISCOUNT
        - IKEA_FAMILY_MEMBERS
        - LEDARNA_MEMBERS
        - LO_MEMBERS
        - MILJOBYGGNAD_SILVER_GOLD
        - OCCUPATIONAL_PENSION_WITH_BANK
        - PASSIVE_HOUSE
        - SACO_MEMBERS
        - SPP_PENSION_CUSTOMERS
        - SVANEN_CERTIFIED
        - SVENSK_FASTIGHETSFORMEDLING_PARTNERSHIP
        - TCO_MEMBERS
        - VILLAAGARNA_MEMBERS
        - YOUNG_BORROWER

    # ──────────────────────────────────────────────
    # Bank metadata schemas
    # ──────────────────────────────────────────────

    Bank:
      type: object
      description: Bank metadata including lending criteria, discounts, service area, and rate coverage
      required:
        - id
        - name
        - isSwedbankSparbank
        - serviceArea
        - lendingCriteria
        - possibleDiscounts
        - currentlyOfferedTerms
        - mfiRiadCodes
        - rateCoverage
        - updatedAt
      properties:
        id:
          type: string
          description: Unique bank identifier
          example: "nordea"
        name:
          type: string
          description: Display name of the bank
          example: "Nordea"
        isSwedbankSparbank:
          type: boolean
          description: Whether this bank is part of the Swedbank/Sparbank network
          example: false
        serviceArea:
          $ref: "#/components/schemas/ServiceArea"
        lendingCriteria:
          $ref: "#/components/schemas/LendingCriteria"
        possibleDiscounts:
          type: array
          description: Discount types available at this bank
          items:
            $ref: "#/components/schemas/DiscountId"
          example: ["ENERGY_CLASS_A", "YOUNG_BORROWER", "SACO_MEMBERS"]
        currentlyOfferedTerms:
          type: array
          description: Mortgage binding periods currently offered by this bank
          items:
            $ref: "#/components/schemas/Term"
          example: ["3m", "1y", "2y", "3y", "5y"]
        mfiRiadCodes:
          type: array
          description: ECB MFI RIAD codes identifying this institution in the European Central Bank's register
          items:
            $ref: "#/components/schemas/MfiRiadCode"
          example:
            - riad: "SE5116000001"
              officialName: "Nordea Hypotek AB"
            - riad: "SE5120001350"
              officialName: "Nordea Bank Abp, filial i Sverige"
        rateCoverage:
          $ref: "#/components/schemas/RateCoverage"
        updatedAt:
          type: string
          format: date-time
          description: When this bank's metadata was last updated
          example: "2025-02-10T14:30:00Z"

    MfiRiadCode:
      type: object
      description: ECB MFI RIAD entry linking a bank to its official registration in the European Central Bank's register
      required:
        - riad
        - officialName
      properties:
        riad:
          type: string
          description: RIAD code uniquely identifying the institution
          example: "SE5116000001"
        officialName:
          type: string
          description: Official registered name of the institution in the ECB MFI register
          example: "Nordea Hypotek AB"

    Mfi:
      type: object
      description: Monetary Financial Institution from the ECB RIAD registry
      required:
        - riad
        - officialName
        - bankIds
        - isMortgageProvider
      properties:
        riad:
          type: string
          description: RIAD code uniquely identifying the institution
          example: "SE5116000001"
        officialName:
          type: string
          description: Official registered name of the institution in the ECB MFI register
          example: "Nordea Hypotek AB"
        bankIds:
          type: array
          items:
            type: string
          description: Bank IDs linking this MFI to banks in /api/v1/banks. Empty if not yet linked.
          example: ["nordea"]
        isMortgageProvider:
          type: [boolean, "null"]
          x-omitempty: false
          description: Whether this institution offers mortgages to private customers. Null if verification is still pending — this typically takes up to one week for new entries. All MFIs are regularly re-checked in case they change their offering.
          example: true

    ServiceArea:
      type: object
      description: Geographic area where the bank operates
      required:
        - municipalityIds
        - isNationwide
      properties:
        municipalityIds:
          type: array
          description: IDs of Swedish municipalities where the bank operates
          items:
            type: integer
          example: [1, 2, 3, 4, 5]
        isNationwide:
          type: boolean
          description: Whether the bank operates across all Swedish municipalities
          example: false

    LendingCriteria:
      type: object
      description: Bank lending requirements and eligibility rules
      required:
        - maxLtvPercent
        - minLoanAmountSek
        - maxLoanAmountSek
        - maxDebtToIncomeRatio
        - minAnnualGrossIncomeSek
        - minPropertyValueSek
        - maxPropertyValueSek
        - acceptsCreditRemarks
        - acceptsNonPermanentEmployment
        - brfMustBeGenuineAssociation
      properties:
        maxLtvPercent:
          type: [integer, "null"]
          x-omitempty: false
          description: Maximum loan-to-value ratio in percent
          example: 85
        minLoanAmountSek:
          type: [integer, "null"]
          x-omitempty: false
          description: Minimum loan amount in SEK
          example: 200000
        maxLoanAmountSek:
          type: [integer, "null"]
          x-omitempty: false
          description: Maximum loan amount in SEK
          example: 50000000
        maxDebtToIncomeRatio:
          type: [number, "null"]
          x-omitempty: false
          description: Maximum debt-to-income ratio allowed (e.g. 5.5 means total debt can be up to 5.5x gross annual income)
          example: 5.5
        minAnnualGrossIncomeSek:
          type: [integer, "null"]
          x-omitempty: false
          description: Minimum annual gross income required in SEK
          example: 300000
        minPropertyValueSek:
          type: [integer, "null"]
          x-omitempty: false
          description: Minimum property value in SEK
          example: 500000
        maxPropertyValueSek:
          type: [integer, "null"]
          x-omitempty: false
          description: Maximum property value in SEK
          example: null
        acceptsCreditRemarks:
          type: boolean
          description: Whether the bank accepts applicants with credit remarks (betalningsanmärkningar)
          example: false
        acceptsNonPermanentEmployment:
          type: boolean
          description: Whether the bank accepts applicants without permanent employment (tillsvidareanställning)
          example: true
        brfMustBeGenuineAssociation:
          type: boolean
          description: Whether the BRF (bostadsrättsförening) must be classified as a genuine housing association (äkta förening)
          example: true

    RateCoverage:
      type: object
      description: Information about what rate data is available for this bank and how fresh it is
      required:
        - listRates
        - averageRates
        - rangeRates
      properties:
        listRates:
          $ref: "#/components/schemas/TrackedRateCoverage"
        averageRates:
          $ref: "#/components/schemas/AverageRateCoverage"
        rangeRates:
          $ref: "#/components/schemas/TrackedRateCoverage"

    CoverageOverview:
      type: object
      description: The days on which each bank has list rates and range rates
      required:
        - asOf
        - gapMinMonths
        - banks
        - swedbankSparbanker
      properties:
        asOf:
          type: string
          format: date
          description: The day the response was computed, in Swedish time. Open rates run to this day, inclusive.
          example: "2026-10-18"
        gapMinMonths:
          type: integer
          description: The shortest break without a rate, in calendar months, that counts as a gap
          example: 3
        banks:
          type: array
          description: Every bank except the Swedbank sparbanker, sorted by name
          items:
            $ref: "#/components/schemas/BankCoverage"
        swedbankSparbanker:
          $ref: "#/components/schemas/SparbankCoverage"

    BankCoverage:
      type: object
      required:
        - bankId
        - name
        - listRates
        - rangeRates
      properties:
        bankId:
          type: string
          example: bluestep
        name:
          type: string
          example: Bluestep Bank
        listRates:
          oneOf:
            - $ref: "#/components/schemas/RateTypeCoverage"
            - type: "null"
          x-omitempty: false
          description: Null if Bolån Radar has no list rate of this bank
        rangeRates:
          oneOf:
            - $ref: "#/components/schemas/RateTypeCoverage"
            - type: "null"
          x-omitempty: false
          description: Null if Bolån Radar has no range rate of this bank

    RateTypeCoverage:
      type: object
      description: The days on which a bank has a rate of one type, for any term
      required:
        - firstDate
        - lastDate
        - daysCovered
        - daysTotal
        - gaps
        - quarters
      properties:
        firstDate:
          type: string
          format: date
          description: The first day with a rate
          example: "2005-09-24"
        lastDate:
          type: string
          format: date
          description: The last day with a rate. It is asOf while a rate is open.
          example: "2026-10-18"
        daysCovered:
          type: integer
          description: The number of days with a rate
          example: 7650
        daysTotal:
          type: integer
          description: The number of days from firstDate to asOf, both included
          example: 7695
        gaps:
          type: array
          description: The gaps, in order
          items:
            $ref: "#/components/schemas/CoverageGap"
        quarters:
          type: array
          description: The quarters with at least one day with a rate, in order
          items:
            $ref: "#/components/schemas/CoverageQuarter"

    CoverageGap:
      type: object
      description: A break without a rate of 3 months or more. Both days are included.
      required:
        - from
        - through
      properties:
        from:
          type: string
          format: date
          description: The first day without a rate
          example: "2005-11-26"
        through:
          type: string
          format: date
          description: The last day without a rate
          example: "2006-01-29"

    CoverageQuarter:
      type: object
      required:
        - quarter
        - daysCovered
        - days
        - terms
      properties:
        quarter:
          type: string
          pattern: ^\d{4}-Q[1-4]$
          example: 2005-Q4
        daysCovered:
          type: integer
          description: The number of days in the quarter with a rate
          example: 40
        days:
          type: integer
          description: The number of days in the quarter. For the quarter of asOf, only the days up to asOf.
          example: 92
        terms:
          type: integer
          description: The number of terms with a rate on at least one day of the quarter
          example: 2

    SparbankCoverage:
      type: object
      description: The Swedbank sparbanker, which use the list rates of another bank
      required:
        - bankCount
        - listRatesFromBankId
      properties:
        bankCount:
          type: integer
          description: The number of Swedbank sparbanker
          example: 55
        listRatesFromBankId:
          type: string
          description: The bank whose list rates the sparbanker use
          example: swedbank

    TrackedRateCoverage:
      type: object
      description: Coverage information for rates tracked from bank websites (used for both list rates and range rates)
      required:
        - historicSince
        - lastChangedOn
        - lastCheckedAt
      properties:
        historicSince:
          type: [string, "null"]
          format: date
          x-omitempty: false
          description: Earliest date for which historic rate data is available. Null if no data of this type exists.
          example: "2023-06-15"
        lastChangedOn:
          type: [string, "null"]
          format: date
          x-omitempty: false
          description: Date when the rates last changed. Null if no data of this type exists.
          example: "2025-02-03"
        lastCheckedAt:
          type: [string, "null"]
          format: date-time
          x-omitempty: false
          description: When rates were last checked from the bank's website. Null if no data of this type exists.
          example: "2025-02-10T06:00:00Z"

    AverageRateCoverage:
      type: object
      description: Coverage information for average rates (snitträntor) reported by the bank
      required:
        - historicSince
        - latestAvailable
      properties:
        historicSince:
          type: [string, "null"]
          x-omitempty: false
          description: Earliest month (YYYY-MM) for which historic average rate data is available. Null if no average rate data exists.
          example: "2022-01"
        latestAvailable:
          type: [string, "null"]
          x-omitempty: false
          description: Most recent month (YYYY-MM) for which average rate data is available. Null if no average rate data exists.
          example: "2025-01"

    # ──────────────────────────────────────────────
    # Latest rates schemas
    # ──────────────────────────────────────────────

    LatestRatesMap:
      type: object
      description: Map of bank IDs to their latest rates
      additionalProperties:
        $ref: "#/components/schemas/BankLatestRates"
      example:
        nordea:
          listRates:
            lastCheckedAt: "2026-02-12T11:28:23Z"
            rates:
              3m:
                rate: 3.04
                validFrom: "2026-01-01"
              1y:
                rate: 3.04
                validFrom: "2026-01-15"
              2y:
                rate: 3.23
                validFrom: "2026-01-15"
              3y:
                rate: 3.31
                validFrom: "2026-01-15"
              5y:
                rate: 3.61
                validFrom: "2026-01-15"
          averageRates:
            lastCheckedAt: "2026-02-12T11:31:02Z"
            rates:
              3m:
                average: 2.56
                referenceMonth: "2026-01"
              1y:
                average: 2.78
                referenceMonth: "2026-01"
              2y:
                average: 2.98
                referenceMonth: "2026-01"
              3y:
                average: 3.09
                referenceMonth: "2026-01"
              5y:
                average: 3.25
                referenceMonth: "2026-01"
          rangeRates: null

    BankLatestRates:
      type: object
      description: Latest rates for a single bank, grouped by rate type
      required:
        - listRates
        - averageRates
        - rangeRates
      properties:
        listRates:
          oneOf:
            - $ref: "#/components/schemas/LatestListRates"
            - type: "null"
          x-omitempty: false
          description: Current published list rates. Null if this bank does not publish list rates.
        averageRates:
          oneOf:
            - $ref: "#/components/schemas/LatestAverageRates"
            - type: "null"
          x-omitempty: false
          description: Most recent average rates reported by the bank. Null if this bank does not publish average rates.
        rangeRates:
          oneOf:
            - $ref: "#/components/schemas/LatestRangeRates"
            - type: "null"
          x-omitempty: false
          description: Current rate ranges. Null if this bank does not publish range rates.

    LatestListRates:
      type: object
      description: Current published list rates (listräntor) for a bank
      required:
        - lastCheckedAt
        - rates
      properties:
        lastCheckedAt:
          type: string
          format: date-time
          description: When list rates were last checked from the bank's website
          example: "2026-02-12T11:28:23Z"
        rates:
          type: object
          description: Map of term to list rate
          additionalProperties:
            $ref: "#/components/schemas/ListRate"
          example:
            3m:
              rate: 3.04
              validFrom: "2026-01-01"
            1y:
              rate: 3.04
              validFrom: "2026-01-15"
            2y:
              rate: 3.23
              validFrom: "2026-01-15"

    LatestAverageRates:
      type: object
      description: Most recent average rates (snitträntor) reported by the bank
      required:
        - lastCheckedAt
        - rates
      properties:
        lastCheckedAt:
          type: string
          format: date-time
          description: When average rates were last checked
          example: "2026-02-12T11:31:02Z"
        rates:
          type: object
          description: Map of term to average rate
          additionalProperties:
            $ref: "#/components/schemas/AverageRate"
          example:
            3m:
              average: 2.56
              referenceMonth: "2026-01"
            1y:
              average: 2.78
              referenceMonth: "2026-01"
            2y:
              average: 2.98
              referenceMonth: "2026-01"

    LatestRangeRates:
      type: object
      description: Current rate ranges published by the bank (min/max rates)
      required:
        - lastCheckedAt
        - rates
      properties:
        lastCheckedAt:
          type: string
          format: date-time
          description: When range rates were last checked from the bank's website
          example: "2026-01-31T21:00:02Z"
        rates:
          type: object
          description: Map of term to rate range
          additionalProperties:
            $ref: "#/components/schemas/RangeRate"
          example:
            3m:
              min: 4.45
              max: 6.69
              validFrom: "2026-01-19"
            3y:
              min: 4.45
              max: 6.69
              validFrom: "2026-01-19"
            5y:
              min: 4.45
              max: 6.69
              validFrom: "2026-01-19"

    ListRate:
      type: object
      description: A single list rate entry with its effective date
      required:
        - rate
        - validFrom
      properties:
        rate:
          type: number
          description: Interest rate in percent
          example: 3.04
        validFrom:
          type: string
          format: date
          description: Date from which this rate is valid
          example: "2026-01-15"

    HistoricListRate:
      type: object
      description: A historic list rate entry with its effective date range
      required:
        - rate
        - validFrom
        - validThrough
      properties:
        rate:
          type: number
          description: Interest rate in percent
          example: 3.04
        validFrom:
          type: string
          format: date
          description: Date from which this rate was valid
          example: "2026-01-15"
        validThrough:
          type: [string, "null"]
          format: date
          x-omitempty: false
          description: Date (inclusive) until which this rate was valid (null for the currently active rate)
          example: "2026-02-15"

    AverageRate:
      type: object
      description: Average rate for a specific reference month
      required:
        - average
        - referenceMonth
      properties:
        average:
          type: number
          description: Average interest rate in percent
          example: 2.78
        referenceMonth:
          type: string
          description: Reference month in YYYY-MM format
          example: "2026-01"

    RangeRate:
      type: object
      description: Rate range with min and max values
      required:
        - min
        - max
        - validFrom
      properties:
        min:
          type: number
          description: Minimum interest rate in percent
          example: 4.45
        max:
          type: [number, "null"]
          x-omitempty: false
          description: Maximum interest rate in percent. Null where the bank states a floor and no ceiling.
          example: 6.69
        validFrom:
          type: string
          format: date
          description: Date from which this rate range is valid
          example: "2026-01-19"

    HistoricRangeRate:
      type: object
      description: A historic rate range entry with its effective date range
      required:
        - min
        - max
        - validFrom
        - validThrough
      properties:
        min:
          type: number
          description: Minimum interest rate in percent
          example: 4.45
        max:
          type: [number, "null"]
          x-omitempty: false
          description: Maximum interest rate in percent. Null where the bank states a floor and no ceiling.
          example: 6.69
        validFrom:
          type: string
          format: date
          description: Date from which this rate range was valid
          example: "2026-01-19"
        validThrough:
          type: [string, "null"]
          format: date
          x-omitempty: false
          description: Date (inclusive) until which this rate range was valid (null for the currently active rate)
          example: "2026-02-15"

    MunicipalityMap:
      type: object
      description: Map of municipality IDs to names
      additionalProperties:
        type: string
        description: Name of the municipality
      example:
        "1": "Upplands Väsby"
        "2": "Vallentuna"
        "3": "Österåker"

    HistoricRates:
      type: object
      description: Historic rates for a bank, grouped by rate type. Each rate type contains a map of terms to chronologically ordered rate entries.
      required:
        - listRates
        - averageRates
        - rangeRates
      properties:
        listRates:
          type: object
          description: Historic list rates grouped by term
          additionalProperties:
            type: array
            items:
              $ref: "#/components/schemas/HistoricListRate"
          example:
            3m:
              - rate: 4.36
                validFrom: "2026-01-15"
                validThrough: "2026-02-15"
              - rate: 3.04
                validFrom: "2026-02-15"
                validThrough: null
            1y:
              - rate: 4.36
                validFrom: "2025-12-15"
                validThrough: "2026-01-10"
              - rate: 3.04
                validFrom: "2026-01-10"
                validThrough: null
        averageRates:
          type: object
          description: Historic average rates grouped by term
          additionalProperties:
            type: array
            items:
              $ref: "#/components/schemas/AverageRate"
          example:
            3m:
              - average: 4.36
                referenceMonth: "2026-01"
              - average: 3.04
                referenceMonth: "2026-02"
            1y:
              - average: 4.36
                referenceMonth: "2025-12"
              - average: 3.04
                referenceMonth: "2026-01"
        rangeRates:
          type: object
          description: Historic range rates grouped by term
          additionalProperties:
            type: array
            items:
              $ref: "#/components/schemas/HistoricRangeRate"
          example:
            3m:
              - min: 3.45
                max: 9.3
                validFrom: "2026-01-15"
                validThrough: "2026-02-15"
              - min: 5.4
                max: 10.3
                validFrom: "2026-02-15"
                validThrough: null
            1y:
              - min: 6.5
                max: 9.1
                validFrom: "2025-12-15"
                validThrough: "2026-01-10"
              - min: 0.45
                max: 22.3
                validFrom: "2026-01-10"
                validThrough: null

    # Webhook payloads

    ListRateChangedEvent:
      type: object
      description: Body of a list_rate.changed delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - list_rate.changed
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/ListRateChangedData"

    ListRateWithdrawnEvent:
      type: object
      description: Body of a list_rate.withdrawn delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - list_rate.withdrawn
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/ListRateWithdrawnData"

    RangeRateChangedEvent:
      type: object
      description: Body of a range_rate.changed delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - range_rate.changed
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/RangeRateChangedData"

    RangeRateWithdrawnEvent:
      type: object
      description: Body of a range_rate.withdrawn delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - range_rate.withdrawn
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/RangeRateWithdrawnData"

    AverageRatePublishedEvent:
      type: object
      description: Body of a average_rate.published delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - average_rate.published
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/AverageRatePublishedData"

    AverageRateCorrectedEvent:
      type: object
      description: Body of a average_rate.corrected delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - average_rate.corrected
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/AverageRateCorrectedData"

    BankAddedEvent:
      type: object
      description: Body of a bank.added delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - bank.added
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/BankAddedData"

    BankUpdatedEvent:
      type: object
      description: Body of a bank.updated delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - bank.updated
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/BankUpdatedData"

    MfiAddedEvent:
      type: object
      description: Body of an mfi.added delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - mfi.added
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/MfiAddedData"

    MfiClassifiedEvent:
      type: object
      description: Body of an mfi.classified delivery
      required:
        - messageId
        - eventId
        - type
        - createdAt
        - data
      properties:
        messageId:
          type: string
          format: uuid
          description: Unique per receiver and event. The same as the X-Request-Id header.
        eventId:
          type: integer
          format: int64
          description: Unique per event. Every receiver of the event gets the same eventId.
        type:
          type: string
          enum:
            - mfi.classified
        createdAt:
          type: string
          format: date-time
          description: When the change was recorded
        data:
          $ref: "#/components/schemas/MfiClassifiedData"

    ListRateChangedData:
      type: object
      description: The list rate of one term after a change
      required:
        - bankId
        - term
        - previous
        - current
      properties:
        bankId:
          type: string
          description: Unique bank identifier, as in /api/v1/banks
          example: "nordea"
        term:
          $ref: "#/components/schemas/Term"
        previous:
          oneOf:
            - $ref: "#/components/schemas/ListRate"
            - type: "null"
          x-omitempty: false
          description: The rate before the change. Null when the term is new or reintroduced.
        current:
          $ref: "#/components/schemas/ListRate"

    ListRateWithdrawnData:
      type: object
      description: The list rate of one term the bank no longer publishes
      required:
        - bankId
        - term
        - previous
      properties:
        bankId:
          type: string
          description: Unique bank identifier, as in /api/v1/banks
          example: "nordea"
        term:
          $ref: "#/components/schemas/Term"
        previous:
          $ref: "#/components/schemas/ListRate"

    RangeRateChangedData:
      type: object
      description: The rate range of one term after a change
      required:
        - bankId
        - term
        - previous
        - current
      properties:
        bankId:
          type: string
          description: Unique bank identifier, as in /api/v1/banks
          example: "nordea"
        term:
          $ref: "#/components/schemas/Term"
        previous:
          oneOf:
            - $ref: "#/components/schemas/RangeRate"
            - type: "null"
          x-omitempty: false
          description: The rate before the change. Null when the term is new or reintroduced.
        current:
          $ref: "#/components/schemas/RangeRate"

    RangeRateWithdrawnData:
      type: object
      description: The rate range of one term the bank no longer publishes
      required:
        - bankId
        - term
        - previous
      properties:
        bankId:
          type: string
          description: Unique bank identifier, as in /api/v1/banks
          example: "nordea"
        term:
          $ref: "#/components/schemas/Term"
        previous:
          $ref: "#/components/schemas/RangeRate"

    AverageRatePublishedData:
      type: object
      description: A newly published average rate
      required:
        - bankId
        - term
        - rate
      properties:
        bankId:
          type: string
          description: Unique bank identifier, as in /api/v1/banks
          example: "nordea"
        term:
          $ref: "#/components/schemas/Term"
        rate:
          $ref: "#/components/schemas/AverageRate"

    AverageRateCorrectedData:
      type: object
      description: An average rate that replaced the one published earlier for the same term and month
      required:
        - bankId
        - term
        - previous
        - current
      properties:
        bankId:
          type: string
          description: Unique bank identifier, as in /api/v1/banks
          example: "nordea"
        term:
          $ref: "#/components/schemas/Term"
        previous:
          $ref: "#/components/schemas/AverageRate"
        current:
          $ref: "#/components/schemas/AverageRate"

    BankAddedData:
      type: object
      description: The new bank
      required:
        - bank
      properties:
        bank:
          $ref: "#/components/schemas/Bank"

    BankUpdatedData:
      type: object
      description: A bank before and after a change to its metadata
      required:
        - previous
        - current
        - changedFields
      properties:
        previous:
          $ref: "#/components/schemas/Bank"
        current:
          $ref: "#/components/schemas/Bank"
        changedFields:
          type: array
          description: The top-level fields of Bank whose value differs between previous and current
          items:
            type: string
          example: ["lendingCriteria", "possibleDiscounts"]

    MfiAddedData:
      type: object
      description: The new MFI
      required:
        - mfi
      properties:
        mfi:
          $ref: "#/components/schemas/Mfi"

    MfiClassifiedData:
      type: object
      description: An MFI whose isMortgageProvider value changed
      required:
        - previous
        - mfi
      properties:
        previous:
          type: [boolean, "null"]
          x-omitempty: false
          description: isMortgageProvider before the change. Null when the MFI was not classified yet.
        mfi:
          $ref: "#/components/schemas/Mfi"

    EventTypeInfo:
      type: object
      description: A webhook event type
      required:
        - name
        - description
        - freshness
      properties:
        name:
          type: string
          example: list_rate.changed
        description:
          type: string
          example: A bank changed, added or reintroduced the list rate for one term.
        freshness:
          type: string
          description: How soon after the real-world change the event is sent
          example: under undisturbed operation, less than 30 minutes after the lender publishes

    WebhookSubscription:
      type: object
      description: The URL an API key receives one event type at
      required:
        - eventType
        - url
      properties:
        eventType:
          type: string
          example: list_rate.changed
        url:
          type: string
          format: uri
          example: https://example.com/hooks/bolan

    WebhookList:
      type: object
      description: The webhook subscriptions of an API key
      required:
        - receiverSecret
        - subscriptions
      properties:
        receiverSecret:
          type: string
          description: The key that signs the deliveries. See the signature header of the webhooks.
        subscriptions:
          type: array
          items:
            $ref: "#/components/schemas/WebhookSubscription"

    AccountSignupRequest:
      type: object
      required:
        - email
        - name
        - useCase
        - acceptTerms
        - turnstileToken
      properties:
        email:
          type: string
          format: email
          maxLength: 254
          example: anna@example.com
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: The person or organisation that uses the key
          example: Anna Andersson, Example AB
        useCase:
          type: string
          minLength: 1
          maxLength: 2000
          description: What the key is for
          example: Rate comparison in our budgeting app
        acceptTerms:
          type: boolean
          description: Must be true. The server records the time and the version of the terms.
        turnstileToken:
          type: string
          description: The Cloudflare Turnstile token of the signup form

    AccountRecoverRequest:
      type: object
      required:
        - email
        - turnstileToken
      properties:
        email:
          type: string
          format: email
          maxLength: 254
          example: anna@example.com
        turnstileToken:
          type: string

    AccountChangeEmailRequest:
      type: object
      required:
        - newEmail
      properties:
        newEmail:
          type: string
          format: email
          maxLength: 254
          example: data@example.com
        turnstileToken:
          type: string
          deprecated: true
          x-deprecated-reason: The API key proves who asks, so this endpoint needs no Turnstile token.
          description: Ignored. A client may still send it.

    AccountVerifyRequest:
      type: object
      required:
        - token
      properties:
        token:
          type: string
          description: The text after "#t=" in the mailed link

    AccountAccepted:
      type: object
      required:
        - message
      properties:
        message:
          type: string
          example: If this address can get a link, we have sent it. The link works for 1 hour.

    AccountVerifyResponse:
      type: object
      required:
        - purpose
      properties:
        purpose:
          type: string
          enum:
            - signup
            - recover
            - changeEmail
          description: What the link was for
        apiKey:
          type: string
          description: The new key, for signup and recover. It is shown only this once.
          example: br_live_4f9d1c0b4e2a7d0d6f0b5c3e8a1f2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c

    WebhookSubscribeRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: The https URL that receives the POST requests
          example: https://example.com/hooks/bolan
