# The current version of the API which is hosted on api.rightfoot.com
openapi: 3.1.0
info:
  title: Rightfoot API
  description: Submit a batch of authorizers for balance checks, retrieve processed balances, and check the status of a batch.
  version: 0.3.0-alpha
servers:
  - url: https://api.rightfoot.com
tags:
  - name: General Availability
    description: |
      **General Availability** indicates that the endpoint is production-ready.
  - name: In Development
    description: |
      **In Development** indicates that the endpoint is currently being built and will be available in an upcoming release.
paths:
  /v1/balance_requests:
    post:
      tags:
        - General Availability
      summary: Submit a batch of authorizers for balance checks
      operationId: submit_balance_request
      description: |
        **Note:** We ask that you schedule the API call with sufficient time prior to ACH cut-offs to retrieve the balance results and omit authorizers with insufficient balances for the loan payment.

        **Batch Size Limit:** The maximum batch size is 1,000 authorizers per request.

        **SLA:** 1 hour

        **Idempotency:** Requests are cached for **24 hours** based on the request body hash. Submitting an identical batch within this period will return the same response without reprocessing.

        **Debit Card Information:** Balance requests using debit card information is currently restricted to approved customers only. If you would like to learn more please reach out to danielle@rightfoot.com or tristan@rightfoot.com.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                authorizers:
                  type: array
                  maxItems: 1000
                  items:
                    $ref: '#/components/schemas/Authorizer'
                webhook_url:
                  type: string
                  format: uri
                  description: Optional. Provide a webhook URL to receive notification when the batch is complete.
              required:
                - authorizers
            example:
              authorizers:
                - authorizer_unique_id: "1a2b3c4d"
                  first_name: "John"
                  last_name: "Doe"
                  address:
                    street_address_line1: "123 Main St"
                    city: "Anytown"
                    state: "CA"
                    zip_code: "12345"
                    country: "US"
                  phone_number: "5551234567"
                  date_of_birth:
                    day: 1
                    month: 1
                    year: 1980
                  ssn: "123456789"
                  ein: "987654321"
                  account_number: "1234567890"
                  routing_number: "021000021"
              webhook_url: "https://api.example.com/webhook/balance-batch-complete"
      responses:
        '201':
          description: Created
          headers:
            Location:
              schema:
                type: string
              description: URL of the created batch resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchCreatedResponse'
              example:
                batch_id: "5d3c6bbb-fc1a-46a4-93da-1ce4a54b0d83"
                #                status: "submitted"
                submitted_at: "2024-09-17T10:00:00Z"
                message: "Batch has been successfully submitted and is pending processing."
                failed_authorizers:
                  - authorizer_unique_id: "5e6f7g8h"
                    error: "Invalid routing number for authorizer(s). Routing number must be 9 digits long."
                  - authorizer_unique_id: "9i0j1k2l"
                    error: "Missing date of birth for authorizer(s)."
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 400
                error:
                  code: "BAD_REQUEST"
                  message: "Invalid batch size. The maximum batch size is 1,000 authorizers per request."
                  timestamp: "2024-09-16T12:00:00Z"
                  suggestion: "Please reduce the number of authorizers in your batch and try again."
                batch_id: null
                documentation_url: "https://api.rightfoot.com/docs"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 401
                error:
                  code: "UNAUTHORIZED"
                  message: "Invalid API key provided."
                  timestamp: "2024-09-16T12:01:00Z"
                  suggestion: "Please check your API key."
                batch_id: null
                documentation_url: "https://api.rightfoot.com/docs"
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 500
                error:
                  code: "INTERNAL_SERVER_ERROR"
                  message: "An unexpected error occurred while processing your request."
                  timestamp: "2024-09-16T12:06:00Z"
                  suggestion: "Please try again later. If the problem persists, contact our support team."
                batch_id: null
                documentation_url: "https://api.rightfoot.com/docs"
  /v1/balance_requests/csv:
    post:
      tags:
        - General Availability
      summary: Submit a batch of authorizers via CSV upload
      operationId: submit_balance_request_csv
      description: |
        Upload a CSV of authorizers for balance checks. The file is validated synchronously at request time — per-row failures are returned inline in the response body.

        **Batch Size Limit:** The maximum batch size is 5,000 rows per CSV.

        **Content-Type:** `multipart/form-data` with the file part as `text/csv`.

        **Encoding:** UTF-8 is preferred. Files exported from Excel on Windows are also accepted; if a non-UTF-8 file is decoded as Latin-1, a `warnings` entry is included in the response.

        **Leading zeros:** Leading zeros are significant. Many tools that generate CSVs treat numeric-looking columns as numbers and silently drop leading zeros, turning `021000021` into `21000021` — configure your export so these columns are written as text. Rows where `routing_number`, `ssn`, `zip_code`, or `debit_card_number` lost a leading zero fail validation and are returned in `failed_authorizers`. `account_number` has no length or format validation, so a truncated account number is accepted and processed and comes back as a no-match result that resembles a data-quality problem; verify that column before uploading. A 10-digit `ein` or a 4-digit `cvv` can also lose a leading zero and still pass validation.

        **CSV Columns:** The header row must use the column names below. Required and optional fields differ slightly depending on whether you submit bank-account info or debit-card info.

        **Always required (every row):**

        | Column | Format |
        |--------|--------|
        | `authorizer_unique_id` | Alphanumeric, must be unique within the CSV |
        | `phone_number` | Exactly 10 digits, no separators (e.g. `5551234567`) |
        | `ssn` *or* `ein` | At least one. `ssn`: 9 digits or `XXX-XX-XXXX`. `ein`: 9–10 digits or `XX-XXXXXXX`. |

        **Bank-account mode** — provide `account_number`:

        | Column | Required? | Format |
        |--------|-----------|--------|
        | `account_number` | required | Free-form string |
        | `routing_number` | required | 9 digits |

        **Debit-card mode** — provide `debit_card_number` (approved customers only):

        | Column | Required? | Format |
        |--------|-----------|--------|
        | `debit_card_number` | required | 16 digits |
        | `cvv` | optional | 3–4 digits |
        | `expiration_month` | optional | Integer 1–12 |
        | `expiration_year` | optional | 4 digits |
        | `routing_number` | optional | 9 digits |

        Provide exactly one of `account_number` or `debit_card_number` per row — providing both is rejected.

        **Always optional (any row):**

        | Column | Format |
        |--------|--------|
        | `first_name`, `last_name` | Free-form |
        | `street_address_line1`, `street_address_line2`, `city`, `state`, `country` | Free-form |
        | `zip_code` | 5 digits or `ZZZZZ-NNNN`. **Required for Discover** (routing number `031100649`). |
        | `date_of_birth` | `MM/DD/YYYY`. If provided, all three components must be present and year must be ≥ 1900. |
        | `meta_*` | Any column prefixed with `meta_` is collected into a per-row `metadata` object (prefix stripped). |

        **Debit Card Information:** Balance requests using debit card information are restricted to approved customers only.
      parameters:
        - in: query
          name: email
          schema:
            type: string
            format: email
          description: Optional. Email address to notify when the batch completes.
        - in: query
          name: webhookUrl
          schema:
            type: string
            format: uri
          description: Optional. Webhook URL to notify when the batch completes.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV file containing authorizer rows. Must be sent as `text/csv`.
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchCreatedResponse'
              example:
                batch_id: "5d3c6bbb-fc1a-46a4-93da-1ce4a54b0d83"
                submitted_at: "2024-09-17T10:00:00Z"
                message: "Batch has been successfully submitted and is pending processing."
                failed_authorizers:
                  - authorizer_unique_id: "5e6f7g8h"
                    error: "Invalid routing number for authorizer(s). Routing number must be 9 digits long."
                    account_type: "ACCOUNT_NUMBER"
                    row_number: 4
                warnings:
                  - "File was not UTF-8 encoded and was decoded using Latin-1. Some characters may appear incorrectly. For best results, save your CSV as UTF-8."
        '400':
          description: |
            Bad Request — returned when the upload is rejected before submission. Possible causes: invalid `Content-Type`, empty file, CSV contains no valid rows, or every row failed validation. When all rows failed validation, `error.details` contains the per-row `failed_authorizers` list and any decoding `warnings`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 400
                error:
                  code: "BAD_REQUEST"
                  message: "No valid authorizers in the batch."
                  timestamp: "2024-09-16T12:00:00Z"
                  suggestion: "All rows failed validation; correct the errors below and re-upload."
                  details:
                    failed_authorizers:
                      - authorizer_unique_id: "1a2b3c4d"
                        error: "Invalid routing number for authorizer(s). Routing number must be 9 digits long."
                        account_type: "ACCOUNT_NUMBER"
                        row_number: 2
                      - authorizer_unique_id: "9i0j1k2l"
                        error: "Missing date of birth for authorizer(s)."
                        account_type: "ACCOUNT_NUMBER"
                        row_number: 3
                documentation_url: "https://api.rightfoot.com/docs"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/balances:
    get:
      tags:
        - General Availability
      summary: Retrieve processed balances for a batch
      operationId: get_balances
      parameters:
        - in: query
          name: batchId
          required: true
          schema:
            type: string
          description: The ID of the batch to retrieve balances for
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
          description: The number of balances to return per page. Maximum is 100.
        - in: query
          name: nextPageToken
          schema:
            type: string
          description: A token to use as the cursor. Retrieves balances starting after this token.
      responses:
        '200':
          description: Balances retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchBalancesResponse'
              example:
                batch_id: "5d3c6bbb-fc1a-46a4-93da-1ce4a54b0d83"
                total_processed: 3
                #                total_count: 500
                balances:
                  - authorizer_unique_id: "1286672"
                    balance: -83.68
                    timestamp: "2024-08-09T18:10:44Z"
                    balance_status_code: 0
                    status_code: 0
                  - authorizer_unique_id: "1258580"
                    balance: 3851.09
                    timestamp: "2024-08-09T17:51:34Z"
                    balance_status_code: 0
                    status_code: 0
                  - authorizer_unique_id: "1258831"
                    balance: null
                    timestamp: "2024-08-09T17:55:25Z"
                    balance_status_code: 3300
                    status_code: 3300
                has_more: true
                next_page_token: "eyJ0ZXN0IjogImN1cnNvciJ9"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/balances/csv:
    get:
      tags:
        - General Availability
      summary: Retrieve Balance Results (CSV)
      operationId: get_balances_csv
      description: |
        Download balance results for one or more batches as a CSV file. Returns the same columns produced by the Bigfoot dashboard download in addition to the core fields returned by `GET /v1/balances`.
      parameters:
        - in: query
          name: batch_ids
          required: true
          schema:
            type: array
            items:
              type: string
              format: uuid
          style: form
          explode: true
          description: One or more batch IDs to export. Repeat the parameter to fetch multiple batches in a single CSV (`?batch_ids=<id1>&batch_ids=<id2>`). All batches must belong to the authenticated requester.
      responses:
        '200':
          description: CSV file containing one row per balance result.
          headers:
            Content-Disposition:
              schema:
                type: string
              description: 'Attachment header, e.g. `attachment; filename="batch-export-<timestamp>.csv"`.'
          content:
            text/csv:
              schema:
                type: string
              example: |
                Authorizer ID,Batch ID,Institution,Phone,Zip,Balance,Status,Creator,Timestamp,meta_customer_ref
                authz-001,8ac1f4a6-1234-4abc-9d3e-001122334455,Bank of America,+15551234567,10001,2843.51,SUCCESS,jane.doe@acme.com,2026-05-17T19:24:12+00:00,CR-9981
                authz-002,8ac1f4a6-1234-4abc-9d3e-001122334455,Wells Fargo,,90210,,NO_MATCH,,2026-05-17T19:25:03+00:00,
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden — one or more requested batches don't belong to the authenticated requester.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/monitoring/scheduled_balance_request:
    post:
      tags:
        - General Availability
      summary: Create a scheduled balance request
      operationId: create_scheduled_balance_request
      description: |
        Create a recurring scheduled balance request for a set of authorizers. The schedule will automatically run balance checks at the specified interval.

        **Scheduling:** Use `interval_days` together with `time_of_day` to run every N days at a specific time.

        **Expiration:** Schedules expire on the specified `expires_at` date (format: YYYY-MM-DD, default 30 days from creation, max 365 days).
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduledBalanceRequest'
            example:
              name: "Weekly Balance Check"
              authorizers:
                - authorizer_unique_id: "1a2b3c4d"
                  first_name: "John"
                  last_name: "Doe"
                  address:
                    street_address_line1: "123 Main St"
                    city: "Anytown"
                    state: "CA"
                    zip_code: "12345"
                    country: "US"
                  phone_number: "5551234567"
                  date_of_birth:
                    day: 1
                    month: 1
                    year: 1980
                  ssn: "123456789"
                  account_number: "1234567890"
                  routing_number: "021000021"
              webhook: "https://api.example.com/webhook/scheduled-balance"
              interval_days: 7
              time_of_day: "14:30"
              start_at: "2025-01-01"
              expires_at: "2025-03-15"
      responses:
        '201':
          description: Schedule created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduledBalanceResponse'
              example:
                schedule_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                name: "Weekly Balance Check"
                message: "Schedule created successfully"
                expires_at: "2025-01-15T00:00:00Z"
                next_run_at: "2024-12-19T02:30:00Z"
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 400
                error:
                  code: "BAD_REQUEST"
                  message: "Invalid scheduling configuration."
                  timestamp: "2024-12-12T12:00:00Z"
                  suggestion: "Provide interval_days with an optional time_of_day."
                documentation_url: "https://api.rightfoot.com/docs"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 409
                error:
                  code: "CONFLICT"
                  message: "Schedule name already exists."
                  timestamp: "2024-12-12T12:00:00Z"
                  suggestion: "A schedule with this name already exists. Please use a different name."
                documentation_url: "https://api.rightfoot.com/docs"
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/monitoring/schedules/{id}:
    get:
      tags:
        - General Availability
      summary: Get schedule details
      operationId: get_schedule_details
      description: |
        Retrieve details about a scheduled balance request, including the last run time, next scheduled run, and total number of authorizers in the schedule.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The schedule ID (UUID) returned when creating the scheduled balance request.
      responses:
        '200':
          description: Schedule details retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleDetailsResponse'
              example:
                schedule_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                name: "Weekly Balance Check"
                last_run_at: "2024-12-12T02:30:00Z"
                next_run_at: "2024-12-19T02:30:00Z"
                total_authorizers: 150
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 404
                error:
                  code: "NOT_FOUND"
                  message: "Schedule with the provided ID does not exist."
                  timestamp: "2024-12-12T12:00:00Z"
                  suggestion: "Please check the schedule ID."
                documentation_url: "https://api.rightfoot.com/docs"
        '500':
          $ref: '#/components/responses/InternalServerError'
    patch:
      tags:
        - General Availability
      summary: Update a schedule
      operationId: update_schedule
      description: |
        Update an existing scheduled balance request. You can modify the schedule interval, time of day, or expiration date.

        **Note:** At least one field must be provided. The schedule must be in ACTIVE status to be updated.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The schedule ID (UUID) returned when creating the scheduled balance request.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: Update the schedule name. Must be unique. Max 255 characters.
                interval_days:
                  type: integer
                  minimum: 1
                  maximum: 30
                  description: Update the schedule to run every N days (1-30).
                time_of_day:
                  type: string
                  description: Update the time of day to run in HH:mm format (24-hour, UTC).
                expires_at:
                  type: string
                  format: date-time
                  description: Update the schedule expiration date. Must be in the future.
            example:
              name: "Weekly Balance Check"
              interval_days: 7
              time_of_day: "14:30"
      responses:
        '200':
          description: Schedule updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  schedule_id:
                    type: string
                    format: uuid
                    description: Unique identifier for the schedule.
                  name:
                    type: string
                    description: The name of the schedule.
                  message:
                    type: string
                    description: Confirmation message.
                  expires_at:
                    type: string
                    format: date-time
                    description: ISO 8601 timestamp when the schedule will expire.
                  next_run_at:
                    type: string
                    format: date-time
                    description: ISO 8601 timestamp of the next scheduled run.
              example:
                schedule_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                name: "Weekly Balance Check"
                message: "Schedule updated successfully"
                expires_at: "2025-03-15T00:00:00Z"
                next_run_at: "2024-12-19T14:30:00Z"
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 400
                error:
                  code: "BAD_REQUEST"
                  message: "At least one update field must be provided."
                  timestamp: "2024-12-12T12:00:00Z"
                  suggestion: "Provide interval_days, time_of_day, or expires_at."
                documentation_url: "https://api.rightfoot.com/docs"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                schedule_not_active:
                  summary: Schedule not active
                  value:
                    status: "error"
                    status_code: 409
                    error:
                      code: "CONFLICT"
                      message: "Schedule is not in ACTIVE status."
                      timestamp: "2024-12-12T12:00:00Z"
                      suggestion: "Only active schedules can be updated."
                    documentation_url: "https://api.rightfoot.com/docs"
                duplicate_name:
                  summary: Duplicate schedule name
                  value:
                    status: "error"
                    status_code: 409
                    error:
                      code: "CONFLICT"
                      message: "Schedule name already exists."
                      timestamp: "2024-12-12T12:00:00Z"
                      suggestion: "A schedule with this name already exists. Please use a different name."
                    documentation_url: "https://api.rightfoot.com/docs"
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/monitoring/authorizers/{id}/history:
    get:
      tags:
        - General Availability
      summary: Get authorizer balance history
      operationId: get_authorizer_history
      description: |
        Retrieve the balance check history for a specific authorizer. Returns a list of recent balance results in reverse chronological order.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The authorizer_unique_id to retrieve history for.
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
          description: The maximum number of history entries to return. Default is 10, maximum is 100.
        - in: query
          name: next_page_token
          schema:
            type: string
          description: A cursor token for pagination. Pass the `next_page_token` from a previous response to fetch the next page of results.
      responses:
        '200':
          description: Authorizer history retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthorizerHistoryResponse'
              example:
                authorizer_unique_id: "1a2b3c4d"
                total_count: 250
                history:
                  - run_at: "2024-12-12T02:30:00Z"
                    batch_id: "BATCH-98765"
                    balance_amount: 1523.47
                    status: "success"
                    error_message: null
                  - run_at: "2024-12-05T02:30:00Z"
                    batch_id: "BATCH-98764"
                    balance_amount: 892.31
                    status: "success"
                    error_message: null
                  - run_at: "2024-11-28T02:30:00Z"
                    batch_id: "BATCH-98763"
                    balance_amount: null
                    status: "failed"
                    error_message: "Institution not supported"
                next_page_token: "eyJjdXJzb3IiOiA1MDAsICJ0b3RhbF9yb3dzX3JldHVybmVkIjogMjB9"
                has_more: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 404
                error:
                  code: "NOT_FOUND"
                  message: "Authorizer with the provided ID does not exist."
                  timestamp: "2024-12-12T12:00:00Z"
                  suggestion: "Please check the authorizer unique ID."
                documentation_url: "https://api.rightfoot.com/docs"
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/monitoring/schedules/{id}/authorizers:
    patch:
      tags:
        - General Availability
      summary: Remove authorizers from a schedule
      operationId: remove_schedule_authorizers
      description: |
        Remove specific authorizers from an existing scheduled balance request. The schedule will continue running for any remaining authorizers.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The schedule ID (UUID) returned when creating the scheduled balance request.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                authorizer_unique_ids:
                  type: array
                  items:
                    type: string
                  description: List of authorizer unique IDs to remove from the schedule.
              required:
                - authorizer_unique_ids
            example:
              authorizer_unique_ids:
                - "auth-123"
                - "auth-456"
                - "auth-789"
      responses:
        '200':
          description: Authorizers removed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoveScheduleAuthorizersResponse'
              example:
                removed_count: 3
                remaining_count: 147
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 404
                error:
                  code: "NOT_FOUND"
                  message: "Schedule not found."
                  timestamp: "2024-12-12T12:00:00Z"
                  suggestion: "Please check the schedule ID."
                documentation_url: "https://api.rightfoot.com/docs"
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/payment_enrollments:
    post:
      tags:
        - In Development
      summary: Enroll or update borrowers in payment monitoring
      operationId: create_payment_enrollments
      description: |
        Enroll a batch of borrowers so Rightfoot monitors each one's balance and acts
        when funds are available to collect.

        **Upsert.** Re-sending a borrower is how you update them: their details are
        refreshed and the `collectable_amount` in that request is applied to the
        monitor they already have, and they come back as `updated`. So you can send
        your full delinquent file every day without tracking what you have sent
        before, or which borrowers changed. An update applies to the next payment,
        not to one already in flight.

        **Partial success.** One bad record never blocks the rest of the batch. The
        response is `207` and every borrower carries its own outcome, so you can act
        on each independently.

        **Obligations.** Instead of one `collectable_amount`, a borrower can carry
        `obligations` — for example each past-due installment — collected
        separately from the same bank account. One balance reading decides them
        all, in `priority` order, each collected whole as its own payment. The push
        carries the full set: an obligation left out stops being collected.

        **Monitoring lifetime.** Monitoring expires 30 days after it starts. Sending a
        borrower again once their monitoring has expired starts fresh monitoring using
        the amount in that request; re-sending one who is still monitored updates them
        but does not extend the window. Your daily push is therefore what keeps
        monitoring alive — if you stop sending a borrower, monitoring for them lapses
        within 30 days.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentEnrollmentRequest'
            example:
              borrowers:
                # Collected by ACH from the bank account above — omit charge_instrument.
                - authorizer_unique_id: "1a2b3c4d"
                  first_name: "John"
                  last_name: "Doe"
                  phone_number: "5551234567"
                  date_of_birth:
                    day: 14
                    month: 3
                    year: 1988
                  address:
                    street_address_line1: "123 Main St"
                    city: "Anytown"
                    state: "CA"
                    zip_code: "12345"
                    country: "US"
                  ssn: "123456789"
                  account_number: "1234567890"
                  routing_number: "021000021"
                  collectable_amount: 42500
                  days_past_due: 47
                  merchant_ref: "acme-nv"
                # Two past-due installments collected separately from one account,
                # with this borrower's own reserve.
                - authorizer_unique_id: "3c4d5e6f"
                  first_name: "Sam"
                  last_name: "Lee"
                  phone_number: "5556667777"
                  date_of_birth:
                    day: 7
                    month: 1
                    year: 1990
                  address:
                    zip_code: "30301"
                  ssn: "234567891"
                  account_number: "2233445566"
                  routing_number: "061000104"
                  days_past_due: 31
                  obligations:
                    - id: "loan-123:inst-2"
                      amount: 10000
                      priority: 1
                      metadata:
                        installment: 2
                        type: "representment"
                    - id: "loan-123:inst-3"
                      amount: 10000
                      priority: 2
                      metadata:
                        installment: 3
                        type: "presentment"
                  policy_overrides:
                    reserve_cents: 5000
                # Card held by your processor. Preferred: no card details leave your
                # systems. token_type and last4 are required alongside the token.
                - authorizer_unique_id: "5e6f7g8h"
                  first_name: "Maria"
                  last_name: "Alvarez"
                  phone_number: "5559876543"
                  date_of_birth:
                    day: 2
                    month: 11
                    year: 1979
                  address:
                    street_address_line1: "88 Oak Ave"
                    street_address_line2: "Apt 4B"
                    city: "Springfield"
                    state: "IL"
                    zip_code: "62701"
                    country: "US"
                  ssn: "987654321"
                  account_number: "9876543210"
                  routing_number: "071000013"
                  collectable_amount: 18000
                  days_past_due: 22
                  charge_instrument:
                    token: "tok_9f3a21c7"
                    token_type: "card"
                    last4: "4412"
                # Bank account held by your processor. The routing and account number
                # above are still required — they are what the balance is read from,
                # whichever instrument collects.
                - authorizer_unique_id: "7g8h9i0j"
                  first_name: "Dana"
                  last_name: "Ruiz"
                  phone_number: "5554445555"
                  date_of_birth:
                    day: 19
                    month: 8
                    year: 1985
                  address:
                    street_address_line1: "41 Cedar Ln"
                    city: "Columbus"
                    state: "OH"
                    zip_code: "43215"
                    country: "US"
                  ssn: "321654987"
                  account_number: "4433221100"
                  routing_number: "121000248"
                  collectable_amount: 26000
                  days_past_due: 34
                  charge_instrument:
                    token: "pm_1QbK2xXyz"
                    token_type: "bank_account"
                    last4: "1100"
                    customer_ref: "cus_1QbK2x"
                # Raw card details, stored securely by Rightfoot. Use only when you
                # cannot produce a processor token.
                - authorizer_unique_id: "9i0j1k2l"
                  first_name: "Andre"
                  last_name: "Boateng"
                  phone_number: "5552223333"
                  date_of_birth:
                    day: 30
                    month: 6
                    year: 1991
                  address:
                    street_address_line1: "500 Pine Rd"
                    city: "Austin"
                    state: "TX"
                    zip_code: "78701"
                    country: "US"
                  ssn: "456789123"
                  account_number: "5544332211"
                  routing_number: "111000025"
                  collectable_amount: 7500
                  days_past_due: 9
                  charge_instrument:
                    card:
                      debit_card_number: "4111111111111111"
                      cvv: "123"
                      expiration_month: 4
                      expiration_year: 2030
      responses:
        '207':
          description: |
            Batch processed. Every borrower in the request has an entry in `results`;
            inspect each `outcome` rather than relying on the status code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentEnrollmentResponse'
              example:
                enrolled: 4
                updated: 1
                already_enrolled: 0
                held: 0
                failed: 0
                results:
                  - authorizer_unique_id: "1a2b3c4d"
                    outcome: "enrolled"
                    reason: null
                    message: null
                  - authorizer_unique_id: "3c4d5e6f"
                    outcome: "enrolled"
                    reason: null
                    message: null
                    obligations:
                      - id: "loan-123:inst-2"
                        outcome: "enrolled"
                        reason: null
                        message: null
                      - id: "loan-123:inst-3"
                        outcome: "enrolled"
                        reason: null
                        message: null
                  - authorizer_unique_id: "5e6f7g8h"
                    outcome: "updated"
                    reason: null
                    message: null
                  - authorizer_unique_id: "7g8h9i0j"
                    outcome: "enrolled"
                    reason: null
                    message: null
                  - authorizer_unique_id: "9i0j1k2l"
                    outcome: "enrolled"
                    reason: null
                    message: null
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 400
                error:
                  code: "BAD_REQUEST"
                  message: "borrowers must contain between 1 and 1000 entries."
                  timestamp: "2024-12-12T12:00:00Z"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 401
                error:
                  code: "UNAUTHORIZED"
                  message: "Invalid or missing API key."
                  timestamp: "2024-12-12T12:00:00Z"
    delete:
      tags:
        - In Development
      summary: Remove borrowers from monitoring
      operationId: delete_payment_enrollments
      description: |
        Stop monitoring a batch of borrowers. Nothing about them is deleted —
        their details and payment history are kept.

        Use it for cured accounts, payoffs, and any borrower who must not be
        contacted or debited. Takes effect immediately: no new payment is
        initiated for these borrowers after this call returns.

        **Per-borrower outcomes.** The response is `200` and every identifier you
        sent has an entry in `results`, in the order you sent them. Inspect each
        `outcome` — in particular, a non-zero `unknown` count usually means the
        wrong identifier is being sent, and those borrowers are **not** removed.

        **Removal is not sticky.** Sending a removed borrower in a later
        enrollment push is read as intent to resume, and starts fresh monitoring
        using the amount in that push. Nothing short of that re-push restarts
        monitoring.

        **Safe to re-send.** Removing a borrower twice is harmless — the second
        request reports them `not_monitored`.

        **Single obligations.** For a borrower enrolled with `obligations`, send
        `obligations` here to stop one and keep collecting the rest. Removing the
        borrower by `authorizer_unique_id` stops every obligation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentUnenrollmentRequest'
            example:
              authorizer_unique_ids:
                - "1a2b3c4d"
                - "5e6f7g8h"
                - "9i0j1k2l"
      responses:
        '200':
          description: |
            Batch processed. Every identifier in the request has an entry in
            `results`; inspect each `outcome` rather than relying on the status
            code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentUnenrollmentResponse'
              example:
                removed: 2
                not_monitored: 1
                unknown: 0
                results:
                  - authorizer_unique_id: "1a2b3c4d"
                    outcome: "removed"
                  - authorizer_unique_id: "5e6f7g8h"
                    outcome: "not_monitored"
                  - authorizer_unique_id: "9i0j1k2l"
                    outcome: "removed"
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 400
                error:
                  code: "BAD_REQUEST"
                  message: "send authorizer_unique_ids, obligations, or both"
                  timestamp: "2024-12-12T12:00:00Z"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 401
                error:
                  code: "UNAUTHORIZED"
                  message: "Invalid or missing API key."
                  timestamp: "2024-12-12T12:00:00Z"

  /v1/payment_events:
    get:
      tags:
        - In Development
      summary: Retrieve payment results for one borrower
      operationId: get_payment_events
      description: |
        Every payment event Rightfoot holds for one borrower, oldest first.

        One payment produces several events over its life — `processing`, then
        `settled` or `returned`. A borrower can have several payments: an original
        and, after a return, at most one reinitiation per recovery episode. It all
        comes back as one flat chronological list, with `reference` identifying
        which payment each event belongs to.

        **Posting to a ledger.** `settled` is money received. `returned` and
        `late_returned` reverse the settlement you booked for that `reference`, and
        otherwise need no entry — an ordinary `returned` never settled in the first
        place. Every other status moves no money. `event_id` is stable and never
        reissued, so use it as your idempotency key.

        **Only what was observed.** Nothing is inferred to fill a gap. If a payment
        settled and was clawed back between two of Rightfoot's reads of the payment
        processor, you receive the `late_returned` with no `settled` before it.

        Pull on whatever cadence suits you: nothing here expires, and re-reading is
        free. A borrower Rightfoot has no record of returns an empty list rather
        than a `404`, so an empty result is not confirmation that the identifier is
        right.
      parameters:
        - in: query
          name: authorizer_unique_id
          required: true
          schema:
            type: string
            minLength: 1
          description: The borrower to report on — the identifier you enrolled them with.
      responses:
        '200':
          description: |
            Every payment event held for this borrower, oldest first. An empty
            `events` array is a valid answer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentEventsResponse'
              example:
                authorizer_unique_id: "1a2b3c4d"
                events:
                  - event_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    reference: "rf-8f21c7a4"
                    status: "processing"
                    amount: 42500
                    occurred_at: null
                    return_code: null
                  - event_id: "b3d1f0aa-2c44-4f8e-9d21-6a0b7c5e1f33"
                    reference: "rf-8f21c7a4"
                    status: "returned"
                    amount: 42500
                    occurred_at: "2025-01-14T09:12:00Z"
                    return_code: "R01"
                  - event_id: "e1a77c02-55b9-4a1d-8f0c-2b9d4e6a7f10"
                    reference: "rf-2b6d90e1"
                    status: "processing"
                    amount: 42500
                    occurred_at: null
                    return_code: null
                  - event_id: "9f4c2d18-6e7a-4b03-91cd-5f8e2a0b3c77"
                    reference: "rf-2b6d90e1"
                    status: "settled"
                    amount: 42500
                    occurred_at: "2025-01-22T16:40:00Z"
                    return_code: null
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 400
                error:
                  code: "BAD_REQUEST"
                  message: "authorizer_unique_id is required."
                  timestamp: "2024-12-12T12:00:00Z"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 401
                error:
                  code: "UNAUTHORIZED"
                  message: "Invalid or missing API key."
                  timestamp: "2024-12-12T12:00:00Z"

  /v1/payment_results/uploads:
    post:
      tags:
        - In Development
      summary: Report the outcomes of your debits via CSV upload
      operationId: upload_payment_results
      description: |
        Upload a CSV of debit outcomes — settled, returned, and so on — for debits
        run outside Rightfoot, or from a payment processor Rightfoot does not read
        directly. Each row is matched to a borrower and recorded exactly as if
        Rightfoot had read it from your payment processor, so a return reported
        here feeds that borrower's monitoring.

        **File Size Limit:** 10 MB.

        **Content-Type:** `multipart/form-data` with the file in a part named `file`.

        **Encoding:** UTF-8 (a byte-order mark is fine).

        **Preview first.** `dryRun=true` parses and matches the whole file and
        returns the same report without writing anything.

        **Outcome dates are required.** A `settled` row needs `settled_at`. A
        `returned` or `late_return` row needs `returned_at` and a NACHA
        `return_code`. These dates are when the outcome happened, not when the debit
        was submitted.

        **Safe to re-send.** Rows are keyed on `reference`, so re-uploading a file
        never creates a payment twice. A row whose status differs from the last one
        recorded for its `reference` records the change; an identical row does
        nothing. A row whose outcome date is earlier than the latest recorded
        outcome never overwrites it.

        **Obligations.** For a borrower enrolled with `obligations`, an optional
        `obligation_id` column names the obligation each debit was for, so its
        settlement, or its return with `R01` or `R09`, applies to that obligation
        alone. A return with any other code still stops every obligation of the
        borrower, because the code is about the bank account they share.

        **Per-row results.** A file that parses always returns `200` with a row-level
        report. Invalid rows are rejected and listed in `problems`, one entry per
        issue, and the valid rows are still recorded.
      parameters:
        - in: query
          name: dryRun
          schema:
            type: boolean
            default: false
          description: Validate and match the file without writing anything.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV of debit outcomes, one row per debit. See the column reference on this page.
      responses:
        '200':
          description: |
            The file was processed. Check `needsAttention` and `problems`: a row can
            be stored but unmatched, or skipped entirely.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentResultsUploadResponse'
              example:
                format: "generic"
                rowCount: 2
                dataRowCount: 3
                matched: 1
                needsAttention: 1
                dryRun: false
                rows:
                  - row: 2
                    reference: "INV-90243"
                    authorizerUniqueId: "1a2b3c4d"
                    status: "settled"
                    amountCents: 42500
                    obligationId: null
                  - row: 3
                    reference: "INV-90244"
                    authorizerUniqueId: null
                    status: "returned"
                    amountCents: 18000
                    obligationId: null
                problems:
                  - row: 3
                    reason: "No matching authorizer for reference 'INV-90244'"
                    severity: "warning"
                  - row: 4
                    reason: "Missing returned_at — required when status is returned"
                    severity: "error"
                  - row: 4
                    reason: "Missing return_code — required when status is returned"
                    severity: "error"
        '400':
          description: |
            Bad Request — the upload was rejected before any row was read: the file
            is over 10 MB, or is not UTF-8 text.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 400
                error:
                  code: "BAD_REQUEST"
                  message: "File exceeds the 10 MB upload limit."
                  timestamp: "2025-01-02T17:32:21Z"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/payment_connections:
    get:
      tags:
        - In Development
      summary: List your processor keys
      operationId: list_payment_connections
      description: |
        Every processor key registered for your account, plus the payment types
        that currently have no default key.

        Always `200`. An account with no keys yet gets an empty `connections`
        list and both payment types in `missing_default_rails`.

        Keys are ordered defaults first, then oldest first. The response never
        contains a key — only the last four characters of each one.
      responses:
        '200':
          description: Your keys and default-key coverage.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentConnectionsResponse'
              example:
                connections:
                  - id: "59ed8c84-50e0-478b-a1cb-e4ff4f809582"
                    label: "Main key"
                    merchant_ref: null
                    rail_scope: "both"
                    is_default: true
                    key_last4: "b6a0"
                    last_poll_at: "2026-09-01T06:12:44Z"
                    access_scope: "lender_admins"
                    created_at: "2026-08-28T15:02:11Z"
                  - id: "02623289-b33a-4d42-9e08-4a9a849c9c84"
                    label: "Nevada ACH"
                    merchant_ref: "acme-nv"
                    rail_scope: "ach"
                    is_default: false
                    key_last4: "d2f4"
                    last_poll_at: "2026-09-01T06:12:44Z"
                    access_scope: "lender_admins"
                    created_at: "2026-08-28T15:04:37Z"
                missing_default_rails: []
                ingested: 128
                matched: 121
                last_poll_at: "2026-09-01T06:12:44Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags:
        - In Development
      summary: Add a processor key
      operationId: create_payment_connection
      description: |
        Register one API key from a supported payment processor (currently
        Payliance). Rightfoot stores the key in a vault the moment the request
        is accepted and never returns it — every later response shows only its
        last four characters.

        A key is either the **default** for the payment types in `rail_scope`
        (`is_default: true`), or **tagged** with a `merchant_ref` that you send as
        `merchant_ref` when you enroll a borrower. It cannot be both. A key with
        neither is stored but not used until you promote it.

        Conflicts are checked before the key is stored, so a rejected request
        leaves nothing behind.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentConnectionCreateRequest'
            examples:
              default_key:
                summary: Default key for both payment types
                value:
                  api_key: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4b6a0"
                  label: "Main key"
                  rail_scope: "both"
                  is_default: true
              tagged_key:
                summary: Tagged ACH key
                value:
                  api_key: "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d2f4"
                  label: "Nevada ACH"
                  rail_scope: "ach"
                  merchant_ref: "acme-nv"
      responses:
        '201':
          description: The key was stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentConnection'
              example:
                id: "02623289-b33a-4d42-9e08-4a9a849c9c84"
                label: "Nevada ACH"
                merchant_ref: "acme-nv"
                rail_scope: "ach"
                is_default: false
                key_last4: "d2f4"
                last_poll_at: null
                access_scope: "lender_admins"
                created_at: "2026-08-28T15:04:37Z"
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                missing_rail_scope:
                  summary: rail_scope not sent
                  value:
                    status: "error"
                    status_code: 400
                    error:
                      code: "BAD_REQUEST"
                      message: "Validation error: body.rail_scope: Field required"
                      timestamp: "2026-08-28T15:04:37Z"
                      suggestion: "Check request parameters and body."
                default_with_tag:
                  summary: is_default and merchant_ref sent together
                  value:
                    status: "error"
                    status_code: 400
                    error:
                      code: "BAD_REQUEST"
                      message: "Validation error: body: Value error, Default keys cannot carry a merchant_ref — a default key is the one used when an enrollment names no tag"
                      timestamp: "2026-08-28T15:04:37Z"
                      suggestion: "Check request parameters and body."
                invalid_tag:
                  summary: merchant_ref is not a valid tag
                  value:
                    status: "error"
                    status_code: 400
                    error:
                      code: "BAD_REQUEST"
                      message: "Validation error: body.merchant_ref: String should match pattern '^[a-z0-9](?:[a-z0-9_-]{0,126}[a-z0-9])?$'"
                      timestamp: "2026-08-28T15:04:37Z"
                      suggestion: "Check request parameters and body."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                default_exists:
                  summary: A default key for that rail_scope already exists
                  value:
                    status: "error"
                    status_code: 409
                    error:
                      code: "CONFLICT"
                      message: "A default key for ACH already exists"
                      timestamp: "2026-08-28T15:04:37Z"
                      suggestion: ""
                tag_and_rail_exist:
                  summary: That tag already has a key for that rail_scope
                  value:
                    status: "error"
                    status_code: 409
                    error:
                      code: "CONFLICT"
                      message: "Tag 'acme-nv' already has an ACH key"
                      timestamp: "2026-08-28T15:04:37Z"
                      suggestion: ""
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/payment_connections/{id}:
    patch:
      tags:
        - In Development
      summary: Update a processor key
      operationId: update_payment_connection
      description: |
        Rename a key, change the payment types it can take, make it the default
        or stop it being the default, or replace the key itself. Send only the
        fields you want to change; at least one is required.

        `merchant_ref` cannot be changed. Borrowers already enrolled under a tag
        would silently move to a different merchant account if it could, so a
        request that includes `merchant_ref` is rejected with `400`.

        **Promoting** (`is_default: true`) swaps the key with the current default
        for the same `rail_scope`, if there is one, which becomes an ordinary
        untagged key. Only untagged keys can be promoted. **Demoting**
        (`is_default: false`) needs no replacement — the payment type is left
        without a default.

        **Replacing the key** (`api_key`) keeps the label, tag and payment types.
        The previous key is revoked once the new one is stored.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The key's `id`, as returned when it was added or listed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentConnectionUpdateRequest'
            examples:
              rename:
                summary: Rename
                value:
                  label: "Nevada ACH (primary)"
              promote:
                summary: Make this key the default for its rail_scope
                value:
                  is_default: true
              replace_key:
                summary: Replace the key
                value:
                  api_key: "0f1e2d3c4b5a69788796a5b4c3d29b1d"
      responses:
        '200':
          description: The key as it is now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentConnection'
              example:
                id: "5da36538-d683-40c6-8cbe-330b90b373b0"
                label: "Debit card key"
                merchant_ref: null
                rail_scope: "card"
                is_default: true
                key_last4: "9b1d"
                last_poll_at: "2026-09-01T06:12:44Z"
                access_scope: "lender_admins"
                created_at: "2026-08-28T15:06:02Z"
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                merchant_ref_sent:
                  summary: merchant_ref cannot be changed
                  value:
                    status: "error"
                    status_code: 400
                    error:
                      code: "BAD_REQUEST"
                      message: "Validation error: body.merchant_ref: Extra inputs are not permitted"
                      timestamp: "2026-08-28T15:06:02Z"
                      suggestion: "Check request parameters and body."
                tagged_key_promoted:
                  summary: A tagged key cannot be the default
                  value:
                    status: "error"
                    status_code: 400
                    error:
                      code: "BAD_REQUEST"
                      message: "A tagged key cannot be made the default key"
                      timestamp: "2026-08-28T15:06:02Z"
                      suggestion: "Key 'acme-nv' is reached by naming its tag. Add or promote an untagged key to serve enrollments that name none."
                nothing_to_change:
                  summary: Empty body
                  value:
                    status: "error"
                    status_code: 400
                    error:
                      code: "BAD_REQUEST"
                      message: "Validation error: body: Value error, Send at least one of label, rail_scope, is_default or api_key"
                      timestamp: "2026-08-28T15:06:02Z"
                      suggestion: "Check request parameters and body."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No key with this id for your account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 404
                error:
                  code: "NOT_FOUND"
                  message: "No payment connection with that id for this requester"
                  timestamp: "2026-08-28T15:06:02Z"
                  suggestion: ""
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                rail_has_default:
                  summary: Moving a default key onto a rail_scope that already has one
                  value:
                    status: "error"
                    status_code: 409
                    error:
                      code: "CONFLICT"
                      message: "A default key for debit card already exists"
                      timestamp: "2026-08-28T15:06:02Z"
                      suggestion: ""
                tag_has_rail:
                  summary: Moving a tagged key onto a rail_scope its tag already covers
                  value:
                    status: "error"
                    status_code: 409
                    error:
                      code: "CONFLICT"
                      message: "Tag 'acme-nv' already has a debit card key"
                      timestamp: "2026-08-28T15:06:02Z"
                      suggestion: ""
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      tags:
        - In Development
      summary: Delete a processor key
      operationId: delete_payment_connection
      description: |
        Revoke the stored key and remove it. Responds `204` with no body.

        Deleting a default key is always allowed. Until you add or promote
        another default for the payment types it covered, borrowers enrolled
        without a `merchant_ref` are skipped on those types —
        `GET /v1/payment_connections` lists them in `missing_default_rails`.

        Payment results already read through this key are kept.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The key's `id`, as returned when it was added or listed.
      responses:
        '204':
          description: The key was revoked and removed.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No key with this id for your account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: "error"
                status_code: 404
                error:
                  code: "NOT_FOUND"
                  message: "No payment connection with that id for this requester"
                  timestamp: "2026-08-28T15:06:02Z"
                  suggestion: ""
        '500':
          $ref: '#/components/responses/InternalServerError'

webhooks:
  balance_check.run.completed:
    post:
      tags:
        - General Availability
      summary: Balance Check Run Completed
      operationId: balanceCheckRunCompletedWebhook
      description: |
        Fires when a balance check run completes. A notification will be sent to the `webhook_url` specified by the partner in the [`/balance_requests` endpoint](#operation/submit_balance_request).
          - Webhook delivery is not guaranteed.
          - The webhook will be retried for up to 30 minutes with exponential backoff. If delivery fails after 30 minutes, no further retries will occur.
          - Customers can poll the /balances endpoint at any time to retrieve processed balances for the batch, ensuring reliable access to data. This provides a dependable fallback if the webhook is not delivered.
          - The receiving server should respond with HTTP `2xx` to confirm receipt. If a `2xx` response is not received, the webhook will retry delivery up to 30 min.
      security: [ ]
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              event_uuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
              type: "BALANCE_BATCH_COMPLETED"
              batch_id: "5d3c6bbb-fc1a-46a4-93da-1ce4a54b0d83"

  balance_threshold_alert:
    post:
      tags:
        - General Availability
      summary: Balance Threshold Alert Event
      operationId: balanceThresholdAlertWebhook
      description: |
        When a scheduled balance check finds authorizers whose balance meets or exceeds their configured threshold, a notification is sent to the `webhook` URL specified when creating the monitoring schedule.
          - Webhook delivery is not guaranteed.
          - The webhook will be retried for up to 30 minutes with exponential backoff. If delivery fails after 30 minutes, no further retries will occur.
          - The receiving server should respond with HTTP `2xx` to confirm receipt. If a `2xx` response is not received, the webhook will retry delivery up to 30 min.
      security: [ ]
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BalanceThresholdAlertEvent'
            example:
              event_uuid: "550e8400-e29b-41d4-a716-446655440000"
              type: "BALANCE_THRESHOLD_ALERT"
              schedule_id: "123e4567-e89b-12d3-a456-426614174000"
              batch_id: "987fcdeb-51a2-3bc4-d567-890123456789"
              alerts:
                - authorizer_unique_id: "user-123"
                  institution_name: "First National Bank"
                  balance: 150.00
                  threshold_amount: 100.00
                - authorizer_unique_id: "user-456"
                  institution_name: "Community Credit Union"
                  balance: 250.50
                  threshold_amount: 200.00
              timestamp: "2024-01-15T10:30:00Z"

components:
  responses:
    BadRequest:
      description: Bad Request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            status: "error"
            status_code: 400
            error:
              code: "BAD_REQUEST"
              message: "Batch ID was not provided in the request."
              timestamp: "2024-09-16T12:00:00Z"
              suggestion: "Please provide a valid batch ID in the request URL."
            documentation_url: "https://api.rightfoot.com/docs"
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            status: "error"
            status_code: 401
            error:
              code: "UNAUTHORIZED"
              message: "Invalid API key provided."
              timestamp: "2024-09-16T12:01:00Z"
              suggestion: "Please check your API key."
            documentation_url: "https://api.rightfoot.com/docs"
    NotFound:
      description: Not Found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            status: "error"
            status_code: 404
            error:
              code: "NOT_FOUND"
              message: "The specified batch ID does not exist."
              timestamp: "2024-09-16T12:09:00Z"
              suggestion: "Please check the batch ID."
            documentation_url: "https://api.rightfoot.com/docs"
    InternalServerError:
      description: Internal Server Error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            status: "error"
            status_code: 500
            error:
              code: "INTERNAL_SERVER_ERROR"
              message: "An unexpected error occurred while retrieving the balances."
              timestamp: "2024-09-16T12:11:00Z"
              suggestion: "Please try again later. If the problem persists, contact our support team."
            documentation_url: "https://api.rightfoot.com/docs"
  schemas:
    PaymentEnrollmentRequest:
      type: object
      required:
        - borrowers
      properties:
        borrowers:
          type: array
          minItems: 1
          maxItems: 1000
          description: The borrowers to enroll. Up to 1000 per request.
          items:
            $ref: '#/components/schemas/PaymentEnrollmentBorrower'
    PaymentEnrollmentBorrower:
      type: object
      description: |
        One borrower to enroll. The identity and bank account fields match the
        `Authorizer` schema used by the balance endpoints, so an existing integration
        can reuse the same serialization.
      required:
        - authorizer_unique_id
        - first_name
        - last_name
        - phone_number
        - date_of_birth
        - address
        - account_number
        - routing_number
        - days_past_due
      allOf:
        - anyOf:
            - required:
                - ssn
            - required:
                - ein
        - oneOf:
            - required:
                - collectable_amount
            - required:
                - obligations
      properties:
        authorizer_unique_id:
          type: string
          description: |
            Your own identifier for this borrower. It must stay the same for this
            borrower for the life of the loan — monitoring history accumulates
            against it, so an identifier that changes between pushes resets that
            history and monitoring degrades without failing.
        first_name:
          type: string
          description: The borrower's first name.
        last_name:
          type: string
          description: The borrower's last name.
        phone_number:
          type: string
          description: The borrower's phone number.
        date_of_birth:
          type: object
          description: Object containing day, month, and year of birth.
          required:
            - day
            - month
            - year
          properties:
            day:
              type: integer
              description: The day of birth, between 1 and 31.
            month:
              type: integer
              description: The month of birth, between 1 and 12.
            year:
              type: integer
              description: The four-digit year of birth.
        address:
          type: object
          required:
            - zip_code
          properties:
            street_address_line1:
              type: string
              description: Address line 1 (e.g., street, PO Box, or company name).
            street_address_line2:
              type: string
              description: Address line 2 (e.g., apartment, suite, unit, or building).
            city:
              type: string
              description: City, district, suburb, town, or village.
            state:
              type: string
              description: State, county, province, or region.
            zip_code:
              type: string
              description: ZIP or postal code. Must be 5 digits or in 5+4 format (e.g., 94107 or 94107-1234).
              pattern: ^\d{5}(?:-\d{4})?$
            country:
              type: string
              description: Two-letter country code.
        ssn:
          type: string
          description: The borrower's Social Security Number. Either SSN or EIN is required.
        ein:
          type: string
          description: The borrower's Employer Identification Number. Either SSN or EIN is required.
        account_number:
          type: string
          description: |
            The borrower's bank account number. Always required, including when you
            supply a card as the charge instrument — this is what Rightfoot uses to
            read the balance, and no charge instrument substitutes for it.
        routing_number:
          type: string
          pattern: ^\d{9}$
          description: The routing number for the bank account. Always required.
        collectable_amount:
          type: integer
          minimum: 1
          description: |
            The most you will accept against this borrower today, in cents
            (`42500` is $425.00). Rightfoot treats this as a ceiling and will not
            attempt more than this in a single payment. Send exactly one of this or
            `obligations`.
        obligations:
          type: array
          minItems: 1
          maxItems: 50
          description: |
            The borrower's debt split into obligations collected separately — for
            example each past-due installment. Send exactly one of this or
            `collectable_amount`; `id` must be unique within the borrower.

            One balance reading decides every obligation, in `priority` order: an
            obligation is collected when the balance covers it, everything ahead of
            it and your reserve. Each is collected whole as its own payment, so a
            returned payment is always re-presented at its original amount.

            The push carries the full set. An obligation sent again is updated in
            place and keeps its retry history; one left out stops being collected.
          items:
            $ref: '#/components/schemas/PaymentObligation'
        policy_overrides:
          $ref: '#/components/schemas/PaymentPolicyOverrides'
        days_past_due:
          type: integer
          minimum: 0
          description: How many days past due this borrower is, as of this request.
        merchant_ref:
          type: string
          description: |
            Optional. The tag of the processor key this borrower should settle to, if you hold
            more than one. Omit it and Rightfoot uses your default key for the rail
            being charged.
        charge_instrument:
          $ref: '#/components/schemas/PaymentChargeInstrument'
    PaymentObligation:
      type: object
      additionalProperties: false
      description: One obligation of a borrower, collected on its own.
      required:
        - id
        - amount
      properties:
        id:
          type: string
          minLength: 1
          description: |
            Your identifier for this obligation, unique within the borrower — for
            example loan number plus installment. Keep it stable: it is how a later
            push updates or removes the obligation, and it is echoed on payment
            events as `obligation_id`.
        amount:
          type: integer
          minimum: 1
          description: The amount to collect for this obligation, in cents.
        priority:
          type: [integer, "null"]
          description: |
            Collection order when the balance cannot cover every obligation. Lower
            is collected first; obligations without a priority come after every
            prioritized one.
        metadata:
          type: [object, "null"]
          additionalProperties: true
          description: |
            Anything you want kept with the obligation, such as an installment
            number or payment type. Echoed back on payment events; never used to
            decide whether or how much to collect.
    PaymentPolicyOverrides:
      type: object
      additionalProperties: false
      description: |
        Optional. Exceptions to your collection policy for this borrower; anything
        left out follows your policy. Unknown fields are rejected. Replaced on every
        push — omit it and the borrower returns to your policy.
      properties:
        reserve_cents:
          type: [integer, "null"]
          minimum: 0
          description: Cents that must remain in the account after a payment.
        min_debit_cents:
          type: [integer, "null"]
          minimum: 0
          description: |
            The smallest payment Rightfoot submits for this borrower. An amount or
            obligation below it is held with `amount_below_minimum`.
    PaymentChargeInstrument:
      description: |
        Optional. How Rightfoot should take payment. Omit it entirely to be charged
        by ACH against the bank account you already supplied.

        Supply exactly one of `card` or `token`. The token on its own is enough to
        charge; `token_type` and `last4` are optional. Raw card details carry
        neither — Rightfoot derives the last four from the number it stores, and
        sending a token field alongside them is rejected.
      oneOf:
        - title: Processor token
          type: object
          additionalProperties: false
          required:
            - token
          properties:
            token:
              type: string
              minLength: 1
              description: |
                A token held by your payment processor that represents the instrument
                to charge. Preferred over sending raw card details — the card or bank
                details behind it stay with your processor.
            token_type:
              type: string
              enum:
                - card
                - bank_account
              description: |
                Optional, and worth sending. Processors issue one token format for
                both kinds — Stripe uses `pm_` for cards and bank accounts alike — so
                Rightfoot cannot classify the token from its value. Omitting it
                records that the kind is unknown rather than guessing one.
            last4:
              type: string
              pattern: ^\d{4}$
              description: |
                Optional. The last four digits of the card or bank account behind
                `token`.
            customer_ref:
              type: string
              minLength: 1
              description: |
                Optional. The processor-side customer the token is attached to
                (Stripe `cus_`). Some processors require it to charge a stored
                instrument while the borrower is not present.
        - title: Raw card
          type: object
          additionalProperties: false
          required:
            - card
          properties:
            card:
              type: object
              description: Raw card details, stored securely by Rightfoot.
              required:
                - debit_card_number
                - cvv
                - expiration_month
                - expiration_year
              properties:
                debit_card_number:
                  type: string
                  pattern: ^\d{16}$
                  description: A sixteen digit debit card number.
                cvv:
                  type: string
                  pattern: ^\d{3,4}$
                  description: 3 or 4 digit CVV for the debit card.
                expiration_month:
                  type: integer
                  minimum: 1
                  maximum: 12
                  description: Expiration month of the debit card (1-12).
                expiration_year:
                  type: integer
                  description: Four-digit expiration year of the debit card.
    PaymentEnrollmentResponse:
      type: object
      description: Counts plus one result per borrower, in the order you sent them.
      properties:
        enrolled:
          type: integer
          description: Borrowers now stored, with monitoring newly started.
        updated:
          type: integer
          description: |
            Borrowers who were already being monitored. Their details were refreshed
            and the amount in this request applied to their existing monitor.
        already_enrolled:
          type: integer
          description: |
            Always `0`. Kept so the response shape stays stable for integrations
            parsing it; a re-sent borrower is reported under `updated` instead.
        held:
          type: integer
          description: Borrowers stored but not monitored.
        failed:
          type: integer
          description: Borrowers whose details from this request were not stored.
        results:
          type: array
          items:
            $ref: '#/components/schemas/PaymentEnrollmentResult'
    PaymentEnrollmentResult:
      type: object
      description: The outcome for a single borrower.
      properties:
        authorizer_unique_id:
          type: string
          description: The identifier you sent for this borrower.
        outcome:
          type: string
          enum:
            - enrolled
            - updated
            - already_enrolled
            - held
            - failed
          description: |
            `enrolled` — stored, and monitoring started.

            `updated` — monitoring was already running. The borrower's details were
            refreshed and the amount in this request applied to that monitor. An
            unchanged re-send is reported this way too.

            `already_enrolled` — no longer returned. Retained in the enum so that
            integrations parsing the old shape keep working.

            `held` — the borrower's details were stored, but monitoring did not
            start or has stopped. See `reason`.

            With `obligations`, the borrower is `enrolled` if any obligation started
            being collected, otherwise `updated` if any was applied, otherwise
            `held`. Each obligation's own outcome is in `obligations`.

            `failed` — the details in this request were not stored. See `reason`.
        reason:
          type: [string, "null"]
          enum:
            - null
            - institution_not_supported
            - unresolvable_routing_number
            - amount_below_minimum
            - authorization_revoked
            - bank_account_invalid
            - retry_limit_reached
            - balance_unreadable
            - duplicate_in_request
            - tokenization_failed
            - card_tokenization_failed
            - chunk_failed
            - monitor_open_failed
          description: |
            Why, for `held` and `failed` outcomes. `null` otherwise.

            `institution_not_supported` — Rightfoot does not currently support this
            bank. Your data is fine; no action available.

            `unresolvable_routing_number` — the routing number did not match a known
            bank. Check it.

            `amount_below_minimum` — the amount is below the minimum payment. Send
            it again at or above the minimum.

            `authorization_revoked` — a payment on this borrower came back as
            unauthorized. Monitoring stays stopped until authorization is
            re-established and monitoring is deliberately resumed.

            `bank_account_invalid` — a payment on this bank account came back as
            closed or invalid. Send the borrower with a different bank account.

            `retry_limit_reached` — payments for this borrower or obligation have
            already been returned the maximum number of times.

            `balance_unreadable` — the balance could not be retrieved on repeated
            attempts. Monitoring stays stopped until it is deliberately resumed.

            `duplicate_in_request` — this identifier appeared more than once in the
            request. Every copy is rejected rather than guessing which you meant.

            `tokenization_failed` — the borrower's details were rejected. See
            `message` for the specific problem, correct it, and send again.

            `card_tokenization_failed` — the card details were rejected. See `message`.

            `chunk_failed` — a temporary failure affecting part of the batch. Send
            these borrowers again.

            `monitor_open_failed` — the borrower was stored but monitoring did not
            start. Send them again.
        message:
          type: [string, "null"]
          description: A human-readable explanation, when one is available.
        obligations:
          type: [array, "null"]
          description: |
            Present when the request sent `obligations` for this borrower: one entry
            per obligation, in the order you sent them. Null for a borrower sent
            with `collectable_amount`.
          items:
            $ref: '#/components/schemas/PaymentObligationResult'
    PaymentObligationResult:
      type: object
      description: The outcome for one obligation of a borrower.
      properties:
        id:
          type: string
          description: The obligation `id` you sent.
        outcome:
          type: string
          enum:
            - enrolled
            - updated
            - held
          description: |
            `enrolled` — collection of this obligation started.

            `updated` — it was already being collected; the amount sent applies
            from the next payment on.

            `held` — not collected. See `reason`.
        reason:
          type: [string, "null"]
          description: Why, for `held`. Uses the same held reasons as a borrower.
        message:
          type: [string, "null"]
          description: A human-readable explanation, when one is available.
    PaymentUnenrollmentRequest:
      type: object
      description: Send `authorizer_unique_ids`, `obligations`, or both; at least one entry in total.
      properties:
        authorizer_unique_ids:
          type: array
          maxItems: 1000
          items:
            type: string
          description: |
            The identifiers of the borrowers to stop monitoring — the same
            `authorizer_unique_id` values you sent on enrollment. Stops every
            obligation the borrower has.
        obligations:
          type: array
          maxItems: 1000
          description: |
            Single obligations to stop collecting, leaving the borrower's other
            obligations monitored.
          items:
            type: object
            additionalProperties: false
            required:
              - authorizer_unique_id
              - obligation_id
            properties:
              authorizer_unique_id:
                type: string
                description: The borrower the obligation belongs to.
              obligation_id:
                type: string
                minLength: 1
                description: The obligation `id` you sent on enrollment.
    PaymentUnenrollmentResponse:
      type: object
      description: |
        Counts plus one result per identifier or obligation sent. The counts sum
        to the number of entries in `results`.
      properties:
        removed:
          type: integer
          description: Borrowers whose live monitoring was stopped by this request.
        not_monitored:
          type: integer
          description: |
            Borrowers Rightfoot knows but was not monitoring — monitoring had
            already expired or been stopped, or never started. Re-sending a
            removal lands every borrower here, so a repeated request is safe.
        unknown:
          type: integer
          description: |
            Identifiers Rightfoot has no record of. A consistently non-zero
            count usually means the wrong identifier is being sent — those
            borrowers are **not** removed from monitoring.
        results:
          type: array
          items:
            $ref: '#/components/schemas/PaymentUnenrollmentResult'
    PaymentUnenrollmentResult:
      type: object
      description: |
        The outcome for a single identifier. Borrower entries come first, in the
        order you sent them, then obligation entries.
      properties:
        authorizer_unique_id:
          type: string
          description: The identifier you sent.
        obligation_id:
          type: [string, "null"]
          description: Set when this entry is for one obligation rather than the whole borrower.
        outcome:
          type: string
          enum:
            - removed
            - not_monitored
            - unknown
          description: |
            `removed` — live monitoring was stopped by this request.

            `not_monitored` — Rightfoot knows this borrower but was not
            monitoring them; there was nothing to stop.

            `unknown` — Rightfoot has no record of this identifier. The borrower
            was **not** removed; check the identifier.
    PaymentResultsUploadResponse:
      type: object
      properties:
        format:
          type: string
          enum: [generic, annotated_export]
          description: |
            How the file was read. `annotated_export` is a Rightfoot balance export
            with your outcome columns added; anything else is `generic`.
        rowCount:
          type: integer
          description: Rows accepted for matching and recording.
        dataRowCount:
          type: integer
          description: Data rows found in the file, excluding blank and summary rows.
        matched:
          type: integer
          description: Accepted rows matched to a borrower.
        needsAttention:
          type: integer
          description: |
            Accepted rows Rightfoot could not match to a borrower. They are stored,
            and re-uploading them with an `authorizer_unique_id` matches them.
        dryRun:
          type: boolean
          description: Echoes the `dryRun` parameter. When true, nothing was written.
        rows:
          type: array
          description: One entry per accepted row.
          items:
            type: object
            properties:
              row:
                type: integer
                description: The row's line in your file (the header is row 1).
              reference:
                type: string
                description: |
                  The row's `reference`. For a row sent without one, the key
                  Rightfoot assigned it.
              authorizerUniqueId:
                type: [string, "null"]
                description: The borrower the row was matched to, or null when unmatched.
              status:
                type: string
                description: The status recorded, in Rightfoot's vocabulary.
              amountCents:
                type: integer
                description: The debit amount in cents.
              obligationId:
                type: [string, "null"]
                description: The `obligation_id` the row named, or null when it named none.
        problems:
          type: array
          description: |
            Everything that needs your attention. An `error` row was not recorded; a
            `warning` row was recorded, but something was ignored or it is unmatched.
            `row` is `0` for a problem with the file as a whole.
          items:
            type: object
            properties:
              row:
                type: integer
              reason:
                type: string
              severity:
                type: string
                enum: [error, warning]
    PaymentEventsResponse:
      type: object
      properties:
        authorizer_unique_id:
          type: string
          description: The borrower these events belong to, echoed from the request.
        events:
          type: array
          description: |
            Oldest first. An empty array means Rightfoot holds no payment events for
            this borrower — which is also the answer for a borrower Rightfoot has
            never heard of, so it is not by itself confirmation that the identifier
            is correct.
          items:
            $ref: '#/components/schemas/PaymentEvent'
    PaymentEvent:
      type: object
      description: One thing that happened to one payment.
      properties:
        event_id:
          type: string
          format: uuid
          description: |
            Stable, unique identifier for this event. Safe to use as an idempotency
            key when posting to a ledger — it never changes and is never reissued.
        reference:
          type: string
          description: |
            The payment this event belongs to. Groups events into attempts: a
            borrower's original payment and each subsequent reinitiation carry
            different references.
        status:
          type: string
          enum:
            - processing
            - settled
            - returned
            - late_returned
            - invalid
            - voided
            - refunded
            - not_attempted
          description: |
            `processing` — submitted to the payment processor and in flight.

            `settled` — cleared. This is money received.

            `returned` — bounced, and never settled. See `return_code`.

            `late_returned` — settled, then clawed back. Reverse the settlement.

            `invalid` — the payment processor refused it: the account details
            themselves are bad, and re-sending the same ones will not help.

            `voided` — cancelled before it went out.

            `refunded` — returned to the borrower.

            `not_attempted` — never sent.
        amount:
          type: integer
          description: The amount of the payment, in cents. `42500` is $425.00.
        occurred_at:
          type: [string, "null"]
          format: date-time
          description: |
            When this happened at the payment processor, where the processor reports
            it. Null where no feed dates the event — notably a `processing` event,
            which nothing at the processor timestamps.
        return_code:
          type: [string, "null"]
          description: |
            The payment processor's return code on a `returned` or `late_returned`
            event — a NACHA R-code such as `R01` (insufficient funds). Null on every
            other status.
        obligation_id:
          type: [string, "null"]
          description: |
            The obligation this payment collects, as the `id` you sent on
            enrollment. For a payment of your own, the `obligation_id` you reported
            it with on a payment-results upload, even one that is not among the
            borrower's obligations. Null for a payment Rightfoot submitted for a
            borrower enrolled with a single `collectable_amount`, and for a payment
            of your own reported without an `obligation_id`.
        obligation_metadata:
          type: [object, "null"]
          additionalProperties: true
          description: |
            The `metadata` your latest push sent with that obligation. Null when
            `obligation_id` is null, once the obligation has been left out of a
            push, and when the `obligation_id` you reported is not one of the
            borrower's obligations.
    PaymentConnectionCreateRequest:
      type: object
      required:
        - api_key
        - label
        - rail_scope
      properties:
        api_key:
          type: string
          minLength: 1
          description: |
            The API key your processor issued for one of your merchant accounts. Stored
            in a vault as soon as the request is accepted and never returned —
            responses carry only `key_last4`.
        label:
          type: string
          minLength: 1
          maxLength: 80
          description: |
            A name you will recognize, shown in the Rightfoot portal. Cosmetic —
            change it any time.
        rail_scope:
          type: string
          enum:
            - ach
            - card
            - both
          description: |
            The payment types this key can take: `ach`, `card` (debit card), or
            `both` when one merchant account covers both. Required — there
            is no default, because a key used for a payment type its merchant
            account was not set up for fails at the processor rather than here.
        is_default:
          type: boolean
          default: false
          description: |
            Make this the default key for the payment types in `rail_scope` — the
            key used for every borrower enrolled without a `merchant_ref`. One
            default per `rail_scope`. Cannot be combined with `merchant_ref`.
        merchant_ref:
          type: string
          pattern: ^[a-z0-9](?:[a-z0-9_-]{0,126}[a-z0-9])?$
          description: |
            A tag that enrollments name to settle to this key: send the same value
            as `merchant_ref` on `POST /v1/payment_enrollments`. Lowercase letters,
            digits, `-` and `_`; 1–128 characters; must start and end with a letter
            or digit. Fixed once saved. One tag can hold one key per `rail_scope`,
            so an ACH key and a debit-card key may share a tag.
    PaymentConnectionUpdateRequest:
      type: object
      minProperties: 1
      additionalProperties: false
      description: |
        Send only the fields to change. At least one is required. `merchant_ref` is
        not accepted — a tag is fixed when the key is added.
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 80
          description: A new name for the key.
        rail_scope:
          type: string
          enum:
            - ach
            - card
            - both
          description: |
            The payment types this key can take. A default key cannot move onto a
            `rail_scope` that already has a default, and a tagged key cannot move
            onto one its tag already has a key for.
        is_default:
          type: boolean
          description: |
            `true` promotes an untagged key to default for its `rail_scope`, swapping
            with the current default for that scope if there is one. `false`
            demotes it and leaves the scope without a default.
        api_key:
          type: string
          minLength: 1
          description: |
            A replacement key from your processor. The previous key is revoked once this
            one is stored; if it cannot be stored, the previous key stays in place.
    PaymentConnection:
      type: object
      description: One registered key. Never includes the key itself.
      properties:
        id:
          type: string
          format: uuid
          description: Use this to update or delete the key.
        label:
          type: string
          description: The name you gave the key.
        merchant_ref:
          type:
            - string
            - "null"
          description: |
            The tag enrollments name to settle to this key, or `null` for a
            default or unused key.
        rail_scope:
          type: string
          enum:
            - ach
            - card
            - both
          description: The payment types this key can take.
        is_default:
          type: boolean
          description: Whether this is the default key for its `rail_scope`.
        key_last4:
          type:
            - string
            - "null"
          description: The last four characters of the stored key, to tell keys apart.
        last_poll_at:
          type:
            - string
            - "null"
          format: date-time
          description: |
            When Rightfoot last read payment results through this key. `null`
            until the first read.
        access_scope:
          type: string
          description: Reserved for a future permissions setting. Always `lender_admins` today.
        created_at:
          type: string
          format: date-time
          description: When the key was added.
    PaymentConnectionsResponse:
      type: object
      properties:
        connections:
          type: array
          description: Your keys — defaults first, then oldest first.
          items:
            $ref: '#/components/schemas/PaymentConnection'
        missing_default_rails:
          type: array
          items:
            type: string
            enum:
              - card
              - ach
          description: |
            Payment types with no default key. Borrowers enrolled without a
            `merchant_ref` are skipped on these until you add or promote one.
            Empty when every type is covered — a `both` key covers both.
        ingested:
          type: integer
          description: Payments Rightfoot has read from your processor through your keys.
        matched:
          type: integer
          description: Of those, the number matched to a borrower you enrolled.
        last_poll_at:
          type:
            - string
            - "null"
          format: date-time
          description: |
            The most recent read across all your keys. `null` until the first
            read.
    Authorizer:
      type: object
      required:
        - authorizer_unique_id
        - address
        - phone_number
        - date_of_birth
      properties:
        authorizer_unique_id:
          type: string
          description: Alphanumeric unique ID for an authorizer, provided by the lender.
        first_name:
          type: string
          description: The authorizer's first name.
        last_name:
          type: string
          description: The authorizer's last name.
        address:
          type: object
          required:
            - zip_code
          properties:
            street_address_line1:
              type: string
              description: Address line 1 (e.g., street, PO Box, or company name).
            street_address_line2:
              type: string
              description: Address line 2 (e.g., apartment, suite, unit, or building).
            city:
              type: string
              description: City, district, suburb, town, or village.
            state:
              type: string
              description: State, county, province, or region.
            zip_code:
              type: string
              description: ZIP or postal code. Must be 5 digits or in 5+4 format (e.g., 94107 or 94107-1234).
              pattern: '^\d{5}(?:-\d{4})?$'
            country:
              type: string
              description: Two-letter country code.
        phone_number:
          type: string
          description: The authorizer's phone number
        date_of_birth:
          type: object
          description: Object containing day, month, and year of birth
          required:
            - day
            - month
            - year
          properties:
            day:
              type: integer
              description: The day of birth, between 1 and 31.
            month:
              type: integer
              description: The month of birth, between 1 and 12.
            year:
              type: integer
              description: The four-digit year of birth.
        ssn:
          type: string
          description: The authorizer's Social Security Number, in string form
        ein:
          type: string
          description: The authorizer's Employer Identification Number, in string form

      anyOf:
        - title: "Bank Account Information"
          description: "Payment details using a bank account. Requires account number, routing number, and either SSN or EIN."
          type: object
          properties:
            account_number:
              type: string
              description: The account number for the bank account, in string form.
            routing_number:
              type: string
              pattern: '^\\d{9}$'
              description: The routing number for the bank account.
            ssn:
              type: string
              description: Either SSN or EIN is required.
            ein:
              type: string
              description: Either SSN or EIN is required.
          required:
            - account_number
            - routing_number
        - title: "Debit Card Information"
          description: "Payment details using a debit card. Requires debit card number and either SSN or EIN. CVV, expiration month, expiration year, and routing number are optional. Account number is not applicable and will not appear."
          type: object
          properties:
            debit_card_number:
              type: string
              description: A sixteen digit debit card number.
              pattern: '^\\d{16}$'
            cvv:
              type: string
              description: 3 or 4 digit CVV for the debit card.
              pattern: '^\\d{3,4}$'
            expiration_year:
              type: integer
              description: Four-digit expiration year of the debit card.
            expiration_month:
              type: integer
              description: Expiration month of the debit card (1-12).
              minimum: 1
              maximum: 12
            routing_number:
              type: string
              pattern: '^\\d{9}$'
              description: (Optional) The routing number for the bank account, if debit card is linked to one.
            ssn:
              type: string
              description: Either SSN or EIN is required.
            ein:
              type: string
              description: Either SSN or EIN is required.
          required:
            - debit_card_number
    ApiErrorResponse:
      type: object
      properties:
        status:
          type: string
          description: Status of the error, either "error" or "success"
        status_code:
          type: integer
          description: HTTP status code of the error
        error:
          type: object
          properties:
            code:
              type: string
              description: Error code (e.g., "RESOURCE_NOT_FOUND")
            message:
              type: string
              description: Detailed error message.
            details:
              type: string
              description: Additional details about the error
            timestamp:
              type: string
              format: date-time
              description: Time the error occurred
            suggestion:
              type: string
              description: Suggested action to resolve the error
        documentation_url:
          type: string
          format: uri
          description: URL to the documentation for the error
    BatchBalancesResponse:
      type: object
      properties:
        batch_id:
          type: string
          description: Unique identifier of the batch.
        total_processed:
          type: integer
          description: Total number of authorizers processed in the batch.
        balances:
          type: array
          description: List of balance records for each authorizer.
          items:
            $ref: '#/components/schemas/BalanceRecord'
        has_more:
          type: boolean
          description: Indicates if more records are available after this page.
        next_page_token:
          type: string
          description: Token to pass into the next request to fetch more balances.
      example:
        batch_id: "batch_12345ABCDE"
        #        total_processed: 3
        #        total_count: 10
        balances:
          - authorizer_unique_id: "1286672"
            balance: -83.68
            timestamp: "2024-08-09T18:10:44Z"
            balance_status_code: 0
            status_code: 0
          - authorizer_unique_id: "1258580"
            balance: 3851.09
            timestamp: "2024-08-09T17:51:34Z"
            balance_status_code: 0
            status_code: 0
          - authorizer_unique_id: "1258831"
            balance: null
            timestamp: "2024-08-09T17:55:25Z"
            balance_status_code: 3300
            status_code: 3300
        has_more: true
        next_page_token: "eyJ0ZXN0IjogImN1cnNvciJ9"
    BalanceRecord:
      type: object
      properties:
        authorizer_unique_id:
          type: string
          description: Unique identifier for the authorizer.
        balance:
          type: number
          format: float
          nullable: true
          description: The balance amount. `null` if the balance couldn't be retrieved.
        balance_status_code:
          $ref: '#/components/schemas/RequestStatusCode'
        status_code:
          $ref: '#/components/schemas/RequestStatusCode'
          deprecated: true
          description: Deprecated in favor of `balance_status_code` to avoid confusion with HTTP status codes.
        timestamp:
          type: string
          format: date-time
          description: Timestamp of when the balance was retrieved.
    BatchCreatedResponse:
      type: object
      required:
        - batch_id
        - submitted_at
        - message
      properties:
        batch_id:
          type: string
          description: Unique identifier for the submitted batch.
        #        status:
        #          type: string
        #          enum:
        #            - submitted
        #            - processing
        #            - completed
        #            - failed
        #          description: Current status of the batch submission.
        submitted_at:
          type: string
          format: date-time
          description: Timestamp indicating when the batch was submitted.
        message:
          type: string
          description: Confirmation message or additional information.
        failed_authorizers:
          type: array
          description: List of authorizers that failed validation or processing.
          items:
            type: object
            properties:
              authorizer_unique_id:
                type: string
                description: Unique identifier for the authorizer that failed.
              error:
                $ref: '#/components/schemas/AuthorizerValidationError'
              account_type:
                type: string
                description: The payment information type for the failed row (e.g., `ACCOUNT_NUMBER`, `DEBIT_CARD`).
              row_number:
                type: integer
                description: 1-indexed source row number from the uploaded CSV (header is row 1, first data row is row 2). Only set for CSV uploads.
        warnings:
          type: array
          description: Non-fatal warnings raised while parsing the input (e.g., non-UTF-8 CSV files decoded as Latin-1). Only set for CSV uploads.
          items:
            type: string
    BatchStatusResponse:
      type: object
      properties:
        batch_id:
          type: string
          description: Unique identifier of the batch.
        status:
          type: string
          description: Current status of the batch.
          enum:
            - processing
            - completed
        #        processed_count:
        #          type: integer
        #          description: Number of authorizers processed so far.
        #        total_count:
        #          type: integer
        #          description: Total number of authorizers in the batch.
        submitted_at:
          type: string
          format: date-time
          description: Timestamp when the batch was submitted.
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the batch processing was completed. `null` if not completed yet.
        message:
          type: string
          description: Additional information on status of batch.
    RequestStatusCode:
      type: integer
      enum:
        - 0
        - 3000
        - 3100
        - 3200
        - 3300
        - 3310
        - 3400
        - 9000
      description: |
        Status code representing the outcome of the request.

        | Code  | Description                                       |
        |-------|---------------------------------------------------|
        | 0     | Success.                                          |
        | 3000  | No tax ID match.                                  |
        | 3100  | No date of birth match.                           |
        | 3200  | Account closed.                                   |
        | 3300  | Institution not supported.                        |
        | 3310  | Institution currently in testing.                 |
        | 3400  | No match with provided authorizer information.    |
        | 9000  | Other error.                                      |
    WebhookEvent:
      type: object
      properties:
        event_uuid:
          type: string
          description: Unique identifier for the webhook event.
          example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
        type:
          type: string
          description: The type of the webhook event, typically `BALANCE_BATCH_COMPLETED`.
          example: "BALANCE_BATCH_COMPLETED"
        batch_id:
          type: string
          description: The ID of the batch that was processed.
          example: "5d3c6bbb-fc1a-46a4-93da-1ce4a54b0d83"
      required:
        - event_uuid
        - type
        - batch_id
    BalanceThresholdAlertEvent:
      type: object
      properties:
        event_uuid:
          type: string
          description: Unique identifier for the webhook event.
          example: "550e8400-e29b-41d4-a716-446655440000"
        type:
          type: string
          description: The type of the webhook event, always `BALANCE_THRESHOLD_ALERT` for this event.
          example: "BALANCE_THRESHOLD_ALERT"
        schedule_id:
          type: string
          description: The ID of the monitoring schedule that triggered the alert.
          example: "123e4567-e89b-12d3-a456-426614174000"
        batch_id:
          type: string
          description: The batch ID for this balance check run.
          example: "987fcdeb-51a2-3bc4-d567-890123456789"
        alerts:
          type: array
          description: List of authorizers that met or exceeded their configured threshold.
          items:
            $ref: '#/components/schemas/BalanceThresholdAlert'
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the event was created.
          example: "2024-01-15T10:30:00Z"
      required:
        - event_uuid
        - type
        - schedule_id
        - batch_id
        - alerts
        - timestamp
    BalanceThresholdAlert:
      type: object
      properties:
        authorizer_unique_id:
          type: string
          description: Your unique identifier for the authorizer.
          example: "user-123"
        institution_name:
          type: string
          description: The name of the financial institution this authorizer is tied to.
          example: "First National Bank"
        balance:
          type: number
          format: float
          description: The authorizer's current balance in dollars.
          example: 150.00
        threshold_amount:
          type: number
          format: float
          description: The configured threshold amount in dollars.
          example: 100.00
      required:
        - authorizer_unique_id
        - institution_name
        - balance
        - threshold_amount
    AuthorizerValidationError:
      type: string
      enum:
        - "Missing authorizer unique ID for authorizer(s)."
        - "Missing zip code for authorizer(s)."
        - "Invalid ZIP code for authorizer(s). Must be 5 digits or in 5+4 format (e.g., 94107 or 94107-1234)."
        - "Missing day of birth for authorizer(s)."
        - "Missing month of birth for authorizer(s)."
        - "Missing year of birth for authorizer(s)."
        - "Invalid phone number for authorizer(s). Phone number must be 10 digits long without dashes and parentheses."
        - "Missing phone number for authorizer(s)."
        - "Missing date of birth for authorizer(s)."
        - "SSN must be of type string for authorizer(s)."
        - "Invalid SSN for authorizer(s)."
        - "EIN must be of type string for authorizer(s)."
        - "Invalid EIN for authorizer(s)."
        - "Number must be of type string for authorizer(s)."
        - "Missing Account Number or Debit Card Number for authorizer(s)."
        - "Account number must be of type string for authorizer(s)."
        - "Debit card number must be of type string for authorizer(s)."
        - "Invalid debit card number for authorizer(s). Must be 16 digits."
        - "CVV must be of type string for authorizer(s)."
        - "Invalid CVV for authorizer(s). Must be 3 or 4 digits."
        - "Invalid expiration month for authorizer(s). Must be between 1 and 12."
        - "Invalid expiration year for authorizer(s). Must be 4 digits."
        - "Invalid routing number for authorizer(s). Routing number must be 9 digits long."
        - "Missing routing number for authorizer(s)."
        - "Missing SSN or EIN for authorizer(s)."
        - "Provide either Account Number or Debit Card Number for authorizer(s), not both."
        - "Duplicate authorizer unique ID not allowed."
      description: |
        Validation error messages for authorizer data submitted in balance requests.

        | Error | Description |
        |-------|-------------|
        | Missing authorizer unique ID for authorizer(s). | The authorizer_unique_id field was not provided. |
        | Missing zip code for authorizer(s). | ZIP code is required for certain institutions (e.g., Discover). |
        | Invalid ZIP code for authorizer(s). Must be 5 digits or in 5+4 format (e.g., 94107 or 94107-1234). | ZIP code format is invalid. |
        | Missing day of birth for authorizer(s). | The day field in date_of_birth was not provided. |
        | Missing month of birth for authorizer(s). | The month field in date_of_birth was not provided. |
        | Missing year of birth for authorizer(s). | The year field in date_of_birth was not provided. |
        | Invalid phone number for authorizer(s). Phone number must be 10 digits long without dashes and parentheses. | Phone number format is invalid. |
        | Missing phone number for authorizer(s). | The phone_number field was not provided. |
        | Missing date of birth for authorizer(s). | The date_of_birth object was not provided. |
        | SSN must be of type string for authorizer(s). | SSN was provided as a non-string type. |
        | Invalid SSN for authorizer(s). | SSN format is invalid. Must be 9 digits or XXX-XX-XXXX format. |
        | EIN must be of type string for authorizer(s). | EIN was provided as a non-string type. |
        | Invalid EIN for authorizer(s). | EIN format is invalid. Must be 9-10 digits or XX-XXXXXXX format. |
        | Number must be of type string for authorizer(s). | Card number was provided as a non-string type. |
        | Missing Account Number or Debit Card Number for authorizer(s). | Neither account_number nor debit_card_number was provided. |
        | Account number must be of type string for authorizer(s). | Account number was provided as a non-string type. |
        | Debit card number must be of type string for authorizer(s). | Debit card number was provided as a non-string type. |
        | Invalid debit card number for authorizer(s). Must be 16 digits. | Debit card number format is invalid. |
        | CVV must be of type string for authorizer(s). | CVV was provided as a non-string type. |
        | Invalid CVV for authorizer(s). Must be 3 or 4 digits. | CVV format is invalid. |
        | Invalid expiration month for authorizer(s). Must be between 1 and 12. | Expiration month is out of range. |
        | Invalid expiration year for authorizer(s). Must be 4 digits. | Expiration year format is invalid. |
        | Invalid routing number for authorizer(s). Routing number must be 9 digits long. | Routing number format is invalid. |
        | Missing routing number for authorizer(s). | Routing number is required when using account_number. |
        | Missing SSN or EIN for authorizer(s). | Neither SSN nor EIN was provided. |
        | Provide either Account Number or Debit Card Number for authorizer(s), not both. | Both account_number and debit_card_number were provided. |
        | Duplicate authorizer unique ID not allowed. | The same authorizer_unique_id appears multiple times in the batch. |
    ScheduledBalanceRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
          description: Name for the schedule. Must be unique. Max 255 characters.
        authorizers:
          type: array
          description: List of authorizers to include in the scheduled balance checks.
          items:
            $ref: '#/components/schemas/Authorizer'
        webhook:
          type: string
          format: uri
          description: Webhook URL to receive notifications when each scheduled run completes.
        interval_days:
          type: integer
          minimum: 1
          maximum: 30
          description: |
            Run the schedule every N days. Used together with `time_of_day` for interval-based scheduling.
            Value must be between 1 and 30.
        time_of_day:
          type: string
          pattern: '^([01]?[0-9]|2[0-3]):[0-5][0-9]$'
          description: |
            Time of day to run the schedule in HH:mm format (24-hour, UTC). Used together with `interval_days`.
            If not provided, defaults to a random time in the 8:00 AM - 11:00 AM UTC window.
            Example: "14:30" for 2:30 PM UTC.
        start_at:
          type: string
          format: date
          description: Date at which the schedule should start in YYYY-MM-DD format. If none is provided, the schedule will start at the next `time_of_day`.
        expires_at:
          type: string
          format: date
          description: Date when the schedule expires in YYYY-MM-DD format. Default is 30 days from creation, maximum is 365 days.
      required:
        - name
        - authorizers
        - webhook
    ScheduledBalanceResponse:
      type: object
      properties:
        schedule_id:
          type: string
          format: uuid
          description: Unique identifier for the created schedule.
        name:
          type: string
          description: The name of the schedule.
        message:
          type: string
          description: Confirmation message.
        expires_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the schedule will expire.
        next_run_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp of the next scheduled run.
      required:
        - schedule_id
        - name
        - message
        - expires_at
        - next_run_at
    ScheduleDetailsResponse:
      type: object
      properties:
        schedule_id:
          type: string
          format: uuid
          description: Unique identifier for the schedule.
        name:
          type: string
          description: The name of the schedule.
        last_run_at:
          type: string
          format: date-time
          nullable: true
          description: ISO 8601 timestamp of the last completed run. Null if the schedule has not run yet.
        next_run_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp of the next scheduled run.
        total_authorizers:
          type: integer
          description: Total number of authorizers in this schedule.
      required:
        - schedule_id
        - name
        - next_run_at
        - total_authorizers
    AuthorizerHistoryResponse:
      type: object
      properties:
        authorizer_unique_id:
          type: string
          description: The unique identifier for the authorizer.
        total_count:
          type: integer
          description: Total number of history records for this authorizer.
        history:
          type: array
          description: List of balance check history entries in reverse chronological order.
          items:
            $ref: '#/components/schemas/AuthorizerHistoryRecord'
        next_page_token:
          type: string
          nullable: true
          description: Token for fetching the next page of results. Null if no more results.
        has_more:
          type: boolean
          description: Indicates whether more results are available beyond this page.
      required:
        - authorizer_unique_id
        - total_count
        - history
        - has_more
    AuthorizerHistoryRecord:
      type: object
      properties:
        run_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the balance check was performed.
        batch_id:
          type: string
          description: The associated batch request ID.
        balance_amount:
          type: number
          format: float
          nullable: true
          description: The balance amount retrieved. Null if the check failed.
        status:
          type: string
          enum:
            - success
            - failed
          description: Status of the balance check.
        error_message:
          type: string
          nullable: true
          description: Error message if the check failed. Null on success.
      required:
        - run_at
        - batch_id
        - status
    RemoveScheduleAuthorizersResponse:
      type: object
      properties:
        removed_count:
          type: integer
          description: Number of authorizers successfully removed from the schedule.
        remaining_count:
          type: integer
          description: Number of authorizers still in the schedule after removal.
      required:
        - removed_count
        - remaining_count
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Authentication to the API is performed via Bearer Token Authentication. Provide your API key as the bearer token in the Authorization header.

        All API requests must be made over HTTPS. Calls made over plain HTTP will fail. API requests without authentication will also fail.
security:
  - BearerAuth: [ ]
