{
  "openapi": "3.1.0",
  "info": {
    "title": "End Close API",
    "description": "REST API is used to interact with the End Close platform.",
    "license": {
      "name": "MIT"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.endclose.com/v1"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-KEY"
      }
    },
    "schemas": {
      "RelayConfig": {
        "type": "object",
        "required": ["routes"],
        "properties": {
          "environment": {
            "type": "string",
            "description": "Name of the environment the API key belongs to, and that owns this document. Informational: the relay logs it and writes it into a comment in its stored copy.",
            "example": "Sandbox"
          },
          "routes": {
            "type": "array",
            "minItems": 1,
            "description": "The relay's routes document — the same content as the `routes:` block of `relay.yaml`. Never empty: an environment with no document gets a 404 instead.",
            "items": {
              "$ref": "#/components/schemas/RelayRoute"
            }
          }
        }
      },
      "RelayRoute": {
        "type": "object",
        "description": "One webhook route on the relay. Validated on save against the relay's own rules (schema in End-Close/endclose-relay, packages/core/src/config/schema.ts): route ids are unique lowercase slugs, `source` names a known adapter, `auth.secret_env` is the NAME of an environment variable on the relay (never a secret value), metadata output names are snake_case and may not be on the relay's hard denylist (cvv, ssn, account/routing number, password, api key, ...) unless hashed, and `enrich:` references are not accepted.",
        "required": ["id", "source", "auth", "map"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-_]*$",
            "description": "Unique lowercase slug for the route.",
            "example": "payabli-settlements"
          },
          "source": {
            "type": "string",
            "enum": ["payabli", "generic_hmac"],
            "description": "The processor adapter that verifies and parses the webhook."
          },
          "auth": {
            "type": "object",
            "description": "How inbound webhooks are verified. `mode: static_header` (header, secret_env, allowed_ips) or `mode: hmac` (header, algorithm, secret_env, signed_content, timestamp_header, tolerance_seconds, event_id, event_type). `secret_env` names an environment variable on the relay.",
            "required": ["mode", "secret_env"],
            "properties": {
              "mode": {
                "type": "string",
                "enum": ["static_header", "hmac"]
              },
              "header": {
                "type": "string"
              },
              "secret_env": {
                "type": "string",
                "pattern": "^[A-Z][A-Z0-9_]*$",
                "example": "PAYABLI_WEBHOOK_SECRET"
              },
              "allowed_ips": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "events": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "Payload event types accepted by this route (glob `*` allowed). Others are dropped locally and never forwarded."
          },
          "map": {
            "type": "object",
            "description": "How a webhook event becomes an End Close record: only fields named here are forwarded. Field references are dot-notation paths into the payload (`batchId`, `batch.id`) or `{ source, transform }` objects (transforms: trim, lowercase, hash).",
            "required": ["data_stream_key", "external_id", "amount", "direction"],
            "properties": {
              "data_stream_key": {
                "type": "string"
              },
              "external_id": {
                "description": "Field reference"
              },
              "amount": {
                "description": "Field reference to a string amount like \"3,762.87\"; converted to integer cents."
              },
              "direction": {
                "type": "string",
                "enum": ["credit", "debit"]
              },
              "date": {
                "description": "Optional payload timestamp: a path, or `{ source, format }` with format `iso8601` (default) or `mdy_hms`. Absent means the record is dated by receive time."
              },
              "description": {
                "description": "Optional field reference"
              },
              "currency": {
                "type": "string",
                "pattern": "^[A-Za-z]{3}$"
              },
              "metadata": {
                "type": "object",
                "description": "Extra fields to forward, as snake_case output name → field reference.",
                "additionalProperties": true
              }
            }
          },
          "max_body_bytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10485760,
            "default": 1048576
          }
        }
      },
      "RelayInstanceManifest": {
        "type": "object",
        "required": ["schema", "reason", "host", "engine_version", "config_schema", "config_source", "capabilities"],
        "properties": {
          "schema": {
            "type": "integer",
            "enum": [1],
            "description": "Manifest format version. Unknown values are rejected with 400."
          },
          "reason": {
            "type": "string",
            "example": "heartbeat",
            "description": "Why the relay is checking in. One lowercase word: `boot`, `heartbeat`, `config_applied`, `error` and `shutdown` are the ones End Close knows about, and any other lowercase word is accepted and stored, so a new relay version is never blocked from checking in. Only `shutdown` carries meaning: it is recorded but does not refresh the instance's liveness, so an instance that says goodbye stops constraining what a document may name instead of counting as alive for another 24 hours. Send no other detail with it — extra keys are stored verbatim and never read, which is the wrong home for stack traces or metrics."
          },
          "host": {
            "type": "string",
            "enum": ["application", "embedded"],
            "description": "The shipped Docker image, or `createRelay()` embedded in the customer's own backend."
          },
          "engine_version": {
            "type": "string",
            "description": "Semver of `@end-close/relay` in use.",
            "example": "0.12.2"
          },
          "config_schema": {
            "type": "integer",
            "minimum": 1,
            "description": "The routes-document schema version this engine understands. Version 1 is current. Gates which document features may be saved: nothing newer than the oldest reporting instance understands."
          },
          "config_source": {
            "type": "string",
            "enum": ["remote", "local"],
            "description": "`remote` fetches from End Close; `local` has routes supplied in code or by file. Informational."
          },
          "capabilities": {
            "type": "object",
            "required": ["adapters"],
            "properties": {
              "adapters": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Lowercase adapter slugs. Always includes the built-ins `payabli` and `generic_hmac`.",
                "example": ["payabli", "generic_hmac", "stripe"]
              },
              "enrichments": {
                "type": "object",
                "description": "Registered enrichment functions keyed by lowercase snake_case name. Each value is a descriptor that may be empty; recognised keys are `description` (string) and `output` (`string` | `number` | `boolean`). Unknown keys are stored as sent.",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "description": {
                      "type": "string"
                    },
                    "output": {
                      "type": "string",
                      "enum": ["string", "number", "boolean"]
                    }
                  },
                  "additionalProperties": true
                },
                "example": {
                  "resident_name": { "output": "string", "description": "Resident full name from the Payabli payor id" },
                  "resident_unit": {}
                }
              }
            }
          }
        }
      },
      "DataStream": {
        "type": "object",
        "required": ["key", "name"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier"
          },
          "key": {
            "type": "string",
            "description": "Unique immutable identifier"
          },
          "name": {
            "type": "string",
            "description": "Human readable name"
          },
          "description": {
            "type": "string",
            "description": "Optional description of the data stream"
          },
          "type": {
            "type": "string",
            "description": "The type of data stream (e.g. api, csv, stripe, bank_feed). `adjustments` is a stream End Close writes itself: a reconciliation's adjustment records for one side, bound to that reconciliation alone."
          },
          "program_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "records_count": {
            "type": "integer",
            "description": "Record count from the ClickHouse mirror"
          }
        }
      },
      "RecordPropertyDefinition": {
        "type": "object",
        "required": ["key", "name", "type", "data_stream_key"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": ["number", "date", "datetime", "string", "boolean"]
          },
          "optional": {
            "type": "boolean",
            "default": false
          },
          "data_stream_key": {
            "type": "string",
            "description": "Must match an existing DataStream.key"
          }
        }
      },
      "BulkRequestSummary": {
        "type": "object",
        "description": "Summary returned when a bulk request is created or retrieved via idempotency key",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier for the bulk request"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled"
            ],
            "description": "Current processing status"
          },
          "total_items": {
            "type": "integer",
            "description": "Number of records submitted"
          },
          "processed_items": {
            "type": "integer",
            "description": "Number of records processed so far"
          },
          "successful_items": {
            "type": "integer",
            "description": "Number of records successfully created"
          },
          "failed_items": {
            "type": "integer",
            "description": "Number of records that failed"
          },
          "skipped_items": {
            "type": "integer",
            "description": "Number of records skipped due to `on_conflict: skip` matching an existing `external_id`"
          },
          "url": {
            "type": "string",
            "description": "Polling URL to check progress (e.g. /v1/bulk_requests/123)"
          }
        }
      },
      "BulkRequest": {
        "type": "object",
        "description": "Full bulk request payload, returned by show, list, cancel, and retry endpoints.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled"
            ],
            "description": "pending = queued, processing = records being inserted, completed = all rows processed, failed = job errored, cancelled = cancellation honored on a heartbeat"
          },
          "source": {
            "type": "string",
            "nullable": true,
            "description": "Origin of the request (e.g. `bulk_api`, `csv`, `nacha`)"
          },
          "total_items": {
            "type": "integer"
          },
          "processed_items": {
            "type": "integer"
          },
          "successful_items": {
            "type": "integer"
          },
          "failed_items": {
            "type": "integer"
          },
          "skipped_items": {
            "type": "integer",
            "description": "Records skipped due to `on_conflict: skip` matching an existing `external_id`"
          },
          "progress": {
            "type": "integer",
            "description": "processed_items / total_items as a percentage (0-100)"
          },
          "filename": {
            "type": "string",
            "nullable": true,
            "description": "Original filename for CSV/NACHA uploads. Null for API-submitted requests."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_progress_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Heartbeat timestamp from the worker; used to detect stuck jobs"
          },
          "cancellation_requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Set by POST /bulk_requests/{id}/cancel; the worker exits at its next heartbeat"
          },
          "parsing": {
            "type": "boolean",
            "description": "True while a CSV/NACHA upload is still being parsed (before rows are enqueued)"
          },
          "stuck": {
            "type": "boolean",
            "description": "True if the request has been `processing` for more than 5 minutes without a heartbeat"
          },
          "error_message": {
            "type": "string",
            "nullable": true
          },
          "error_details": {
            "type": "object",
            "additionalProperties": true,
            "description": "Free-form structured error context (e.g. row indices, validation errors)"
          },
          "parent_bulk_request_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "If this request is a retry, the ID of the original (parent) bulk request"
          },
          "on_conflict": {
            "type": "string",
            "enum": ["skip", "error"],
            "description": "Conflict policy applied when an `external_id` already exists in the stream"
          },
          "data_stream_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Data stream the request targets, when applicable"
          }
        }
      },
      "DataStreamOperation": {
        "type": "object",
        "description": "Async operation against a data stream (delete or reset). Poll via GET /data_streams/{key}/operations/{id}. An operation only reaches `completed` once the stream is verified empty in the primary database; if records outlive the wipe (for example, ingestion landing mid-reset), it ends `failed` with `records_remaining` > 0 and an `error_message` asking you to retry.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "data_stream_id": {
            "type": "string",
            "format": "uuid"
          },
          "data_stream_key": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": ["delete", "reset"]
          },
          "status": {
            "type": "string",
            "enum": ["pending", "running", "completed", "failed"]
          },
          "progress": {
            "type": "integer",
            "description": "Percentage 0-100"
          },
          "error_message": {
            "type": "string",
            "nullable": true
          },
          "records_remaining": {
            "type": "integer",
            "nullable": true,
            "description": "Number of records still on the stream when the operation reached a terminal state, counted in the primary database (not the analytics mirror behind `records_count`, which can lag). `0` on `completed`; greater than `0` on a `failed` operation that left records behind; `null` while `pending` or `running`. After a reset, trust this field rather than re-listing `/records`."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "requested_by_user_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          }
        }
      },
      "DataStreamHealth": {
        "type": "object",
        "description": "Ingestion health snapshot for a data stream. All counts are indexed lookups; safe to poll.",
        "properties": {
          "records_count": {
            "type": "integer"
          },
          "imports_count": {
            "type": "integer",
            "description": "Total file imports on this stream, all statuses"
          },
          "imports": {
            "type": "object",
            "description": "File imports on this stream, counted by status",
            "properties": {
              "pending": {
                "type": "integer"
              },
              "processing": {
                "type": "integer"
              },
              "replicating": {
                "type": "integer"
              },
              "completed": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "cancelled": {
                "type": "integer"
              },
              "stuck": {
                "type": "integer",
                "description": "Currently `processing` with no heartbeat for >5 minutes"
              }
            }
          },
          "last_ingestion_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Most recent record `created_at` on this stream"
          }
        }
      },
      "Record": {
        "type": "object",
        "required": ["date", "data_stream_key", "amount", "direction"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "amount": {
            "type": "integer",
            "description": "The monetary amount in cents (e.g., 1234 = $12.34)"
          },
          "currency": {
            "type": "string",
            "description": "Three-letter ISO 4217 currency code (e.g. USD, EUR). **Optional on create — defaults to `USD` when omitted.** Case-insensitive on input; always returned uppercase.",
            "pattern": "^[A-Za-z]{3}$",
            "default": "USD"
          },
          "decimal_places": {
            "type": "integer",
            "description": "Number of decimal places for this currency (2 for USD)",
            "default": 2
          },
          "direction": {
            "type": "string",
            "enum": ["credit", "debit"]
          },
          "description": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "default": {}
          },
          "external_id": {
            "type": "string"
          },
          "data_stream_key": {
            "type": "string"
          }
        }
      },
      "ReconciliationEntry": {
        "description": "A record's entry in one reconciliation. Match status, escalation, and matched records are all scoped to that reconciliation; a record has one entry per reconciliation it participates in. `id` is the record id.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Record"
          },
          {
            "type": "object",
            "properties": {
              "reconciliation_id": {
                "type": "string",
                "format": "uuid"
              },
              "reconciliation_name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": ["reconciled", "unreconciled"],
                "description": "Whether the record is matched in this reconciliation. Read-only."
              },
              "escalated": {
                "type": "boolean",
                "description": "Unmatched and past its `overdue_at`."
              },
              "overdue_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "matched_records": {
                "type": "array",
                "description": "Records matched with this record in this reconciliation",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "amount": {
                      "type": "integer",
                      "description": "Amount in cents"
                    },
                    "currency": {
                      "type": "string",
                      "description": "Three-letter ISO 4217 currency code (e.g. USD, EUR)",
                      "default": "USD"
                    },
                    "decimal_places": {
                      "type": "integer",
                      "default": 2
                    },
                    "direction": {
                      "type": "string",
                      "enum": ["credit", "debit"]
                    },
                    "description": {
                      "type": "string"
                    },
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "external_id": {
                      "type": "string"
                    },
                    "data_stream_key": {
                      "type": "string"
                    },
                    "reconciliation_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The reconciliation in which this match was made"
                    },
                    "reconciliation_name": {
                      "type": "string",
                      "description": "Name of the reconciliation in which this match was made"
                    }
                  }
                }
              },
              "days_unreconciled": {
                "type": "integer",
                "nullable": true,
                "description": "Number of days since the record's date. Null when the record is reconciled."
              },
              "exception_id": {
                "type": "string",
                "format": "uuid",
                "nullable": true,
                "description": "Open exception covering this entry's data stream when the entry is escalated. Null otherwise."
              }
            }
          }
        ]
      },
      "Reconciliation": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Human readable name for the reconciliation"
          },
          "run_type": {
            "type": "string",
            "enum": ["continuous", "batch"],
            "default": "continuous"
          },
          "side_a_streams": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataStream"
            },
            "description": "Data streams on side A of the reconciliation"
          },
          "side_b_streams": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataStream"
            },
            "description": "Data streams on side B of the reconciliation"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "current_run": {
            "$ref": "#/components/schemas/ReconciliationRun",
            "nullable": true,
            "description": "The currently pending or running batch run, if any"
          },
          "last_run": {
            "$ref": "#/components/schemas/ReconciliationRun",
            "nullable": true,
            "description": "The most recent completed or failed batch run, if any"
          }
        }
      },
      "ReconciliationRun": {
        "type": "object",
        "description": "A single execution of a reconciliation's batch or continuous matching pipeline.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": ["batch", "continuous"]
          },
          "status": {
            "type": "string",
            "enum": ["pending", "running", "completed", "failed", "cancelled"]
          },
          "trigger": {
            "type": "string",
            "enum": ["manual", "rule_activated", "agent", "record_created", "ingestion"],
            "description": "What initiated this run. `ingestion` runs are queued automatically once an import lands, so rollup (many_to_one) rules apply to the new records."
          },
          "total_records": {
            "type": "integer",
            "description": "Total records considered by the run"
          },
          "processed_records": {
            "type": "integer",
            "description": "Records processed so far"
          },
          "matched_records": {
            "type": "integer",
            "description": "Records that were matched during the run"
          },
          "rule_summary": {
            "type": "object",
            "nullable": true,
            "description": "Per-rule match counts, populated as the run progresses"
          },
          "progress": {
            "type": "integer",
            "description": "Processing progress as a percentage (0-100)"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "ReconciliationRule": {
        "type": "object",
        "required": ["filters", "reconciliation_id"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "priority": {
            "type": "integer",
            "description": "Lower values run first"
          },
          "filters": {
            "type": "object",
            "description": "A rule tree with group nodes (AND/OR) and condition leaf nodes. Conditions compare a field on one side to a field on the other side. On a `many_to_one` rule only AND-combined `eq` conditions without tolerances are accepted: `amount eq amount` is satisfied by the rollup itself (the group total is compared to the Side B amount), and every other condition segments the Side A groups by its `key` and requires the Side B record to carry the same value in its `value` field (for example `metadata.operator eq metadata.operator` keeps each operator's batches with that operator's payout). OR groups, other operands and tolerances are rejected on `many_to_one` rules; to accept a group total that differs by a small amount, set `rollup.tolerance`.",
            "properties": {
              "operand": {
                "type": "string",
                "enum": ["and", "or"],
                "description": "Logical operator for combining child rules"
              },
              "rules": {
                "type": "array",
                "description": "Child rules (groups or conditions)",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "description": "Source field to compare (e.g. \"amount\", \"date\", \"metadata.invoice_id\")"
                    },
                    "operand": {
                      "type": "string",
                      "enum": [
                        "eq",
                        "neq",
                        "gt",
                        "gte",
                        "lt",
                        "lte",
                        "contains",
                        "in",
                        "not_in"
                      ],
                      "description": "Comparison operator"
                    },
                    "value": {
                      "type": "string",
                      "description": "Target field to compare against (e.g. \"amount\", \"external_id\")"
                    },
                    "tolerance_amount": {
                      "type": "number",
                      "description": "Optional tolerance for fuzzy matching with the 'eq' operand. Must be a positive number."
                    },
                    "tolerance_unit": {
                      "type": "string",
                      "enum": ["dollar", "percent", "day", "month"],
                      "description": "Unit for tolerance. 'dollar' and 'percent' for number fields, 'day' and 'month' for date fields. 'month' matches records in the same calendar month (tolerance_amount is ignored)."
                    },
                    "transforms": {
                      "type": "object",
                      "description": "Optional transforms applied to field values before comparison. Each side (key/value) can have a single transform string, a parameterized transform object, or an array of transforms applied in order (first = innermost). Available transforms: lowercase, uppercase, alphanumeric_only, digits_only, first_n_chars (requires {\"name\": \"first_n_chars\", \"args\": {\"n\": 8}}).",
                      "properties": {
                        "key": {
                          "description": "Transform(s) for the Side A (key) field."
                        },
                        "value": {
                          "description": "Transform(s) for the Side B (value) field."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "group_by": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Side A field keys to group records by before matching. The first key is the primary axis: in `rollup.group_by_kind` 'date' mode it must be a date field; in 'identifier' mode it can be any field (e.g. a batch id in metadata). Additional keys further segment Side A — in date mode each segment matches only Side B records with the same value for those fields; in identifier mode they form a composite cohort key and do not constrain the Side B record. To require the Side B record to share a value in either mode, add an `eq` condition to `filters`."
          },
          "cardinality": {
            "type": "string",
            "enum": ["one_to_one", "many_to_one"],
            "default": "one_to_one",
            "description": "Match cardinality. 'one_to_one' matches single records on each side. 'many_to_one' rolls up multiple records on one side to match a single record on the other side — requires the `rollup` field to be set."
          },
          "rollup": {
            "type": "object",
            "nullable": true,
            "description": "Required when cardinality is 'many_to_one'. Configures how records on the many side are aggregated before matching.",
            "properties": {
              "lookback_days": {
                "type": "integer",
                "minimum": 1,
                "description": "Optional. Number of days of history to include when rolling up records. Must be set together with `date_field`. Omit both to match without a time bound."
              },
              "date_field": {
                "type": "string",
                "description": "Optional. The date field on the rolled-up records used to anchor the lookback window. Must be set together with `lookback_days`."
              },
              "group_by_kind": {
                "type": "string",
                "enum": ["date", "identifier"],
                "default": "date",
                "description": "How `group_by` is interpreted. 'date' (default) buckets records per day and walks a date-window running sum. 'identifier' buckets records by exact value (e.g. a batch id) and matches the bucket total to one record on the other side, optionally bounded by the lookback window."
              },
              "tolerance": {
                "type": "integer",
                "minimum": 0,
                "description": "Optional. How far a group's total may be from the other side's amount and still match, in cents (1 = one cent), like every amount in this API. The difference is booked as an adjustment record on the match so it nets to zero; the record lives on the reconciliation's own adjustments stream (type `adjustments`), not on a feed. Applies to same-currency groups only. A match is still made only when the record has exactly one candidate group within the variance, and that group has exactly one candidate record. Omit for an exact match."
              }
            },
            "dependentRequired": {
              "lookback_days": ["date_field"],
              "date_field": ["lookback_days"]
            }
          },
          "description": {
            "type": "string",
            "description": "Optional human-readable description of what this rule matches"
          },
          "source": {
            "type": "string",
            "enum": ["user", "ai", "default"],
            "description": "Origin of the rule. 'user' = created manually via the API or UI. 'ai' = created by the AI rules engine. 'default' = built-in rule seeded on older reconciliations (no longer created)."
          },
          "status": {
            "type": "string",
            "enum": ["active", "proposed"],
            "description": "Active rules are used by the reconciliation engine. Proposed rules are awaiting review from a cold start."
          },
          "confidence": {
            "type": "string",
            "enum": ["high", "medium", "low"],
            "description": "AI confidence rating. Only present on AI-generated rules from cold start."
          },
          "sample_match_rate": {
            "type": "string",
            "description": "How many sample records this rule matched during dry-run evaluation (e.g. '42/50'). Only present on AI-generated rules from cold start."
          },
          "reconciliation_id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ReconciliationMatch": {
        "type": "object",
        "description": "A reconciliation match represents a group of records that have been matched together. Endclose supports many-to-one matching, so a single match can contain a large number of records — to retrieve them, use `GET /v1/records?reconciliation_match_id={id}`.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "group_key": {
            "type": "string"
          },
          "reconciliation_id": {
            "type": "string",
            "format": "uuid"
          },
          "reconciliation_rule_id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "record_count": {
            "type": "integer",
            "description": "Total number of records in this match"
          }
        }
      },
      "ReconciliationStory": {
        "type": "object",
        "required": ["name"],
        "description": "A reconciliation story groups related reconciliations into an ordered chain so end-to-end flow can be tracked. Each link points to a reconciliation; non-origin links must specify the data stream that connects them to the previous reconciliation in the chain.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Human readable name for the story"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "links": {
            "type": "array",
            "description": "Ordered list of reconciliations participating in the story",
            "items": {
              "$ref": "#/components/schemas/ReconciliationStoryLink"
            }
          }
        }
      },
      "ReconciliationStoryLink": {
        "type": "object",
        "required": ["reconciliation_id", "position", "role"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "position": {
            "type": "integer",
            "description": "1-indexed position in the chain. Must be unique within a story."
          },
          "role": {
            "type": "string",
            "enum": ["origin", "continuation"],
            "description": "The first link is the 'origin'. All subsequent links are 'continuation' and must set shared_data_stream_id."
          },
          "reconciliation_id": {
            "type": "string",
            "format": "uuid",
            "description": "The reconciliation this link points to"
          },
          "reconciliation_name": {
            "type": "string"
          },
          "shared_data_stream_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The data stream that connects this link to the previous link in the chain. Required for continuation links, null for origin."
          },
          "shared_data_stream_name": {
            "type": "string",
            "nullable": true
          },
          "side_a_streams": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "side_b_streams": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ReconciliationException": {
        "type": "object",
        "required": ["title", "reconciliation_id", "data_stream_id"],
        "description": "A reconciliation exception represents unmatched overdue records that need human attention. Exceptions are scoped to a (reconciliation, data_stream) tuple and managed by the sweeper job — record membership is computed on read from the set of unmatched records whose `overdue_at` has passed.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "reconciliation_id": {
            "type": "string",
            "format": "uuid",
            "description": "The reconciliation this exception belongs to"
          },
          "reconciliation_name": {
            "type": "string",
            "description": "Name of the parent reconciliation"
          },
          "data_stream_id": {
            "type": "string",
            "format": "uuid",
            "description": "The data stream this exception is scoped to"
          },
          "data_stream_name": {
            "type": "string",
            "description": "Human-readable name of the scoped data stream"
          },
          "data_stream_key": {
            "type": "string",
            "description": "Immutable key of the scoped data stream"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": ["open", "investigating", "dismissed", "resolved"],
            "default": "open"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "opened_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the exception most recently transitioned into an open state. Distinct from `created_at` so reopening is tracked cleanly."
          },
          "closed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the exception most recently transitioned into a resolved or dismissed state. Cleared on reopen."
          },
          "assignees": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "workos_user_id": {
                  "type": "string"
                }
              }
            }
          },
          "decimal_places": {
            "type": "integer",
            "description": "Number of decimal places (2 for USD)",
            "default": 2
          },
          "aggregates": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ExceptionAggregates"
              }
            ],
            "description": "Whole-exception aggregates over the current unmatched overdue slice."
          },
          "filtered_aggregates": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ExceptionAggregates"
              }
            ],
            "description": "Aggregates restricted to the filter query parameters. Equals `aggregates` when no filter is supplied. Only present on the show response."
          },
          "comments": {
            "type": "array",
            "description": "Comments on this exception, oldest first",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "body": {
                  "type": "string"
                },
                "author_workos_id": {
                  "type": "string"
                },
                "mentions": {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  },
                  "description": "User IDs mentioned in the comment"
                },
                "created_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "investigation": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AgentInvestigationSession"
              }
            ],
            "nullable": true,
            "description": "The most recent agent investigation session for this exception, if any."
          },
          "investigation_sessions": {
            "type": "array",
            "description": "Up to the five most recent investigation sessions, newest first. Exceptions may accumulate multiple sessions as the overdue set evolves.",
            "items": {
              "$ref": "#/components/schemas/AgentInvestigationSession"
            }
          },
          "review_counts": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ReviewCounts"
              }
            ],
            "description": "How many lines of the review are in each status. Zero everywhere until findings are written."
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Adjustment reasons already used on this reconciliation, most common first (at most 20). Offered as choices when a reviewer plugs a gap by hand. Only present on the show response."
          }
        }
      },
      "ReviewCounts": {
        "type": "object",
        "description": "Counts of review lines by status. A line is `done` once its record is matched, `looks_right` when its finding says so and it is not yet matched, and `to_look_at` otherwise.",
        "properties": {
          "all": {
            "type": "integer"
          },
          "to_look_at": {
            "type": "integer"
          },
          "looks_right": {
            "type": "integer"
          },
          "done": {
            "type": "integer"
          }
        }
      },
      "FindingInput": {
        "type": "object",
        "required": ["note"],
        "description": "One line of the review as your engine sees it. Identify the record by `record_id` or by `external_id` (resolved within the exception's data stream). `suggestion.record_ids` may be record ids or external ids from either side of the reconciliation; unknown ids reject the finding.",
        "properties": {
          "record_id": {
            "type": "string",
            "description": "The record's id. Required unless `external_id` is given."
          },
          "external_id": {
            "type": "string",
            "description": "The record's external id within the exception's data stream. Rejected when it matches more than one record."
          },
          "looks_right": {
            "type": "boolean",
            "default": false,
            "description": "`true` when the line is correct as it stands and only needs the note. These lines can be confirmed in bulk."
          },
          "note": {
            "type": "string",
            "description": "One or two plain sentences: what happened to this line and what was checked."
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "A short label for the kind of line, in your own words (e.g. `back-billing`, `monthly minimum`)."
          },
          "suggestion": {
            "type": "object",
            "description": "What Approve applies. Omit, or use kind `none`, when there is nothing to apply.",
            "properties": {
              "kind": {
                "type": "string",
                "enum": ["match", "adjust", "none"],
                "default": "none",
                "description": "`match`: pair the line with `record_ids` (must balance). `adjust`: pair them and plug the remaining gap with an adjustment carrying `reason`. `none`: nothing to apply."
              },
              "record_ids": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Every other record of the proposed match, either side, as record ids or external ids."
              },
              "reason": {
                "type": "string",
                "description": "For `adjust`: why a gap remains. Becomes the adjustment's description."
              },
              "text": {
                "type": "string",
                "description": "The sentence shown next to the Approve button."
              },
              "difference_cents": {
                "type": "integer",
                "description": "Informational. Recomputed from the records as |side A| − |side B| whenever `record_ids` is given; only kept as sent for a one-sided `adjust`."
              }
            }
          },
          "evidence": {
            "type": "array",
            "description": "Quotes that back the note. A quote with a `url` marks the line as checked against a document in the review.",
            "items": {
              "type": "object",
              "required": ["quote"],
              "properties": {
                "quote": {
                  "type": "string"
                },
                "source": {
                  "type": "string",
                  "nullable": true,
                  "description": "Where the quote is from, e.g. `Services Agreement, Exhibit B p.19`."
                },
                "url": {
                  "type": "string",
                  "nullable": true,
                  "description": "An http(s) link to the document — for a file in End Close, the file's `url`. End `#page=N` to open a PDF at that page: the review drawer labels the link “Open p.N” and shows the document inline. Other schemes are dropped."
                }
              }
            }
          },
          "precedent": {
            "type": "string",
            "nullable": true,
            "description": "How the same line was handled before, in one sentence."
          },
          "labels": {
            "type": "object",
            "additionalProperties": true,
            "description": "Free-form markers your reviewers care about, e.g. `{\"carried_forward\": true, \"colour\": \"peach\"}`. The review page reads `colour`, `colour_cell` and `carried_forward`; anything else is stored and returned untouched."
          }
        }
      },
      "Finding": {
        "type": "object",
        "description": "A stored finding: the `FindingInput` fields as saved, plus provenance.",
        "properties": {
          "id": {
            "type": "string"
          },
          "record_id": {
            "type": "string"
          },
          "looks_right": {
            "type": "boolean"
          },
          "note": {
            "type": "string"
          },
          "suggestion": {
            "type": "object",
            "description": "As `FindingInput.suggestion`, with `record_ids` resolved to record ids and `difference_cents` recomputed."
          },
          "evidence": {
            "type": "array",
            "description": "As `FindingInput.evidence`, each quote with the `check` End Close ran against its document.",
            "items": {
              "type": "object",
              "properties": {
                "quote": {
                  "type": "string"
                },
                "source": {
                  "type": "string",
                  "nullable": true
                },
                "url": {
                  "type": "string",
                  "nullable": true
                },
                "check": {
                  "type": "object",
                  "description": "Whether the document says what the quote says. Written by End Close shortly after the finding, for quotes whose `url` is a PDF in Files.",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": ["pending", "found", "found_elsewhere", "not_found", "no_text", "no_document", "external", "unsupported", "unreadable"],
                      "description": "`pending`: not checked yet. `found`: on the page the `url` names (or anywhere, if it names none). `found_elsewhere`: in the document, on `page` rather than the cited one. `not_found`: not in the document as quoted. `no_text`: the PDF has no text to search, such as a scan. `no_document`: the quote has no `url`. `external`: the `url` isn't a file in Files. `unsupported`: the file isn't a PDF. `unreadable`: the PDF can't be read."
                    },
                    "page": {
                      "type": "integer",
                      "description": "The page the quote is on, from 1. Present when `found` or `found_elsewhere`."
                    },
                    "boxes": {
                      "type": "array",
                      "description": "Where the quote is on `page`: one box per line, each as fractions of the page's width and height from its top-left corner.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "left": { "type": "number" },
                          "top": { "type": "number" },
                          "width": { "type": "number" },
                          "height": { "type": "number" }
                        }
                      }
                    },
                    "checked_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "precedent": {
            "type": "string",
            "nullable": true
          },
          "other_side": {
            "type": "object",
            "description": "What the other column shows: `{ amount_cents, count, record_ids }` derived from the suggested records on the opposite side, or `{}`. `count` is the sum of those records' counts (`metadata.quantity`, `qty`, `count`, `event_qty` or `units`, first present) and is only there when one of them carries one. You may send `amount_cents` and `count` yourself; a `count` you leave out is still derived."
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "labels": {
            "type": "object"
          },
          "source": {
            "type": "string",
            "enum": ["api", "agent"],
            "description": "`api` when written through this API; `agent` when written by End Close's own review."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FindingsUpsertRequest": {
        "type": "object",
        "required": ["findings"],
        "properties": {
          "findings": {
            "type": "array",
            "maxItems": 1000,
            "items": {
              "$ref": "#/components/schemas/FindingInput"
            }
          }
        }
      },
      "FindingsUpsertResult": {
        "type": "object",
        "description": "Each finding is validated on its own, so a batch succeeds partially: `upserted` counts the rows written and `errors` names the rejected ones by position.",
        "properties": {
          "upserted": {
            "type": "integer"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer",
                  "description": "Position of the rejected finding in the request's `findings`."
                },
                "record_id": {
                  "type": "string"
                },
                "external_id": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ReviewRow": {
        "type": "object",
        "properties": {
          "record": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ReconciliationEntry"
              }
            ],
            "description": "The record as it stands in the exception's reconciliation."
          },
          "finding": {
            "$ref": "#/components/schemas/Finding"
          },
          "status": {
            "type": "string",
            "enum": ["to_look_at", "looks_right", "done"],
            "description": "Derived on read: `done` once the record is matched, else `looks_right` or `to_look_at` from the finding."
          },
          "difference_cents": {
            "type": "integer",
            "description": "|this line's amount| − |finding.other_side.amount_cents|, in cents. Zero when nothing was found on the other side."
          },
          "quantity": {
            "type": "number",
            "nullable": true,
            "description": "The line's count, read from the record's `metadata.quantity`, `qty`, `count`, `event_qty` or `units` (first present). Null when the record carries none."
          },
          "other_quantity": {
            "type": "number",
            "nullable": true,
            "description": "`finding.other_side.count`, or null."
          },
          "quantity_difference": {
            "type": "number",
            "nullable": true,
            "description": "`quantity` − `other_quantity`; null unless both sides carry a count."
          }
        }
      },
      "ReviewPage": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReviewRow"
            }
          },
          "counts": {
            "$ref": "#/components/schemas/ReviewCounts"
          },
          "categories": {
            "type": "array",
            "description": "Every finding category on the exception with how many lines carry it, most common first — counted over the whole exception, not the current filter.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total": {
            "type": "integer",
            "description": "Rows matching the current `status`, `category` and `q` filters."
          },
          "sides": {
            "type": "object",
            "description": "`{ a: { data_stream_id, name }, b: { data_stream_id, name } }`"
          },
          "row_side": {
            "type": "string",
            "enum": ["a", "b"],
            "description": "Which side the exception's lines are on."
          },
          "pending_notes": {
            "type": "integer",
            "description": "Overdue lines of this exception that have no finding yet."
          },
          "notes_state": {
            "type": "string",
            "enum": ["writing", "failed", "off", "idle"],
            "description": "Why `pending_notes` lines have no finding. `writing`: a `row_review` session is queued or running. `failed`: the latest `row_review` session failed or died mid-run. `off`: End Close's row review isn't enabled for this account (this wins over any earlier run), so only findings you post through `PUT /reconciliation_exceptions/{id}/findings` appear. `idle`: no run is in flight."
          }
        }
      },
      "ExceptionAggregates": {
        "type": "object",
        "description": "Summary statistics for the (filtered) set of unmatched overdue records behind an exception.",
        "properties": {
          "record_count": {
            "type": "integer",
            "description": "Number of matching overdue unmatched records"
          },
          "total_amount": {
            "type": "integer",
            "description": "Sum of signed amounts in cents (debits are negative)"
          },
          "debit_count": {
            "type": "integer",
            "description": "Count of records with amount < 0"
          },
          "debit_sum": {
            "type": "integer",
            "description": "Sum of debit amounts in cents (non-positive)"
          },
          "credit_count": {
            "type": "integer",
            "description": "Count of records with amount > 0"
          },
          "credit_sum": {
            "type": "integer",
            "description": "Sum of credit amounts in cents (non-negative)"
          },
          "avg_age_days": {
            "type": "number",
            "description": "Average age in days since each record's `overdue_at`"
          },
          "oldest_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Transaction date of the oldest record in the slice"
          },
          "newest_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Transaction date of the newest record in the slice"
          }
        }
      },
      "AgentInvestigationSession": {
        "type": "object",
        "description": "An agent run against an exception: an `investigation` (one recommendation for the overdue slice) or a `row_review` (one finding per overdue line, read back through `GET /reconciliation_exceptions/{id}/review`). `investigation` and `investigation_sessions` on an exception only ever carry investigations.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": ["investigation", "row_review"]
          },
          "status": {
            "type": "string",
            "description": "Investigation status (e.g. pending, running, completed, failed)"
          },
          "outcome": {
            "type": "string",
            "nullable": true,
            "enum": ["match_found", "no_match", "needs_review", null],
            "description": "Standardized outcome of a completed investigation. `match_found` = confident candidate matches; `no_match` = thorough search returned nothing; `needs_review` = candidates found but signals are weak. Null when the session is not yet completed or failed, and always null for a `row_review` session."
          },
          "recommendation_text": {
            "type": "string",
            "nullable": true,
            "description": "Agent's recommendation once the investigation completes"
          },
          "confidence_score": {
            "type": "number",
            "nullable": true,
            "description": "Agent confidence in its recommendation (0.0-1.0)"
          },
          "candidate_records_searched": {
            "type": "integer",
            "nullable": true,
            "description": "Number of candidate records the agent evaluated"
          },
          "suggested_record_ids": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "integer"
            },
            "description": "Records the agent suggests linking to this exception"
          },
          "filter_params": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Filter parameters the session was scoped to, if any. Matches the filter shape on `GET /reconciliation_exceptions/{id}`."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ExceptionFilter": {
        "type": "object",
        "description": "Optional filter parameters that narrow an exception's record slice. All fields are optional; omit or leave blank to match everything.",
        "properties": {
          "amount_min": {
            "type": "integer",
            "description": "Include records with signed amount >= amount_min (cents)"
          },
          "amount_max": {
            "type": "integer",
            "description": "Include records with signed amount <= amount_max (cents)"
          },
          "date_from": {
            "type": "string",
            "format": "date",
            "description": "Include records with transaction date >= date_from"
          },
          "date_to": {
            "type": "string",
            "format": "date",
            "description": "Include records with transaction date <= date_to"
          },
          "direction": {
            "type": "string",
            "enum": ["debit", "credit"],
            "description": "Include only debits (amount < 0) or credits (amount > 0). Derived from the sign of `amount`, not from the record's reconciliation side."
          },
          "counterparty": {
            "type": "string",
            "description": "Case-insensitive substring match on the record description"
          },
          "age_bucket": {
            "type": "string",
            "enum": ["lt_7d", "7_30d", "30_90d", "gt_90d"],
            "description": "Convenience filter on time since `overdue_at` (less-than-7-days, 7-to-30-days, 30-to-90-days, greater-than-90-days)"
          }
        }
      },
      "FileImport": {
        "type": "object",
        "description": "A file import (CSV/NACHA upload or bulk API submission) and its ingestion lifecycle. Aggregate fields (record_count, total_credits, total_debits, net_amount, date_range_start, date_range_end, currency) are merged in only on the stream-scoped index GET /data_streams/{key}/file_imports, and are trustworthy once status is `completed`.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "replicating",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "source": {
            "type": "string",
            "nullable": true,
            "description": "Origin of the import (e.g. `csv`, `nacha`, `bulk_api`)"
          },
          "filename": {
            "type": "string",
            "nullable": true
          },
          "total_items": {
            "type": "integer"
          },
          "processed_items": {
            "type": "integer"
          },
          "successful_items": {
            "type": "integer"
          },
          "failed_items": {
            "type": "integer"
          },
          "skipped_items": {
            "type": "integer",
            "description": "Records skipped due to `on_conflict: skip` matching an existing `external_id`"
          },
          "expected_records_count": {
            "type": "integer",
            "nullable": true
          },
          "progress": {
            "type": "integer",
            "description": "processed_items / total_items as a percentage (0-100)"
          },
          "parsing": {
            "type": "boolean",
            "description": "True until rows start landing; file totals are unknown while parsing"
          },
          "decimal_places": {
            "type": "integer",
            "description": "Number of decimal places (2 for USD)"
          },
          "stuck": {
            "type": "boolean",
            "description": "Currently processing with no heartbeat for >5 minutes"
          },
          "on_conflict": {
            "type": "string",
            "nullable": true
          },
          "error_message": {
            "type": "string",
            "nullable": true
          },
          "error_details": {
            "type": "object",
            "nullable": true
          },
          "data_stream_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "records_written_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_progress_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "cancellation_requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "url": {
            "type": "string",
            "description": "e.g. /v1/file_imports/123"
          },
          "records_url": {
            "type": "string",
            "description": "e.g. /v1/file_imports/123/records"
          },
          "record_count": {
            "type": "integer",
            "description": "Aggregate; stream-scoped index only"
          },
          "total_credits": {
            "type": "integer",
            "description": "Aggregate, in cents; stream-scoped index only"
          },
          "total_debits": {
            "type": "integer",
            "description": "Aggregate, in cents; stream-scoped index only"
          },
          "net_amount": {
            "type": "integer",
            "description": "Aggregate: credits minus debits, in cents; stream-scoped index only"
          },
          "date_range_start": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Aggregate; stream-scoped index only"
          },
          "date_range_end": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Aggregate; stream-scoped index only"
          },
          "currency": {
            "type": "string",
            "nullable": true,
            "description": "Aggregate; stream-scoped index only"
          }
        }
      },
      "PaginatedResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {}
          },
          "has_more": {
            "type": "boolean"
          },
          "offset": {
            "type": "number"
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": ["id", "event_type", "data"],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for the event"
          },
          "event_type": {
            "type": "string",
            "description": "Description of the event (e.g. record.reconciled or record.reconciliation_overdue)"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "description": "Payload data for the event"
          }
        }
      },
      "RecordWebhookEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEvent"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/Record"
              }
            }
          }
        ]
      },
      "BankAccount": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier"
          },
          "name": {
            "type": "string",
            "description": "Human readable name for the bank account"
          },
          "mask": { "type": "string", "nullable": true, "description": "Last four digits of the account number, as it appears on statements. Populated for Plaid-linked accounts." },
          "display_name": { "type": "string", "description": "Human label for the account: \"Name (mask)\" when a mask is known, otherwise just the name." },
          "currency": {
            "type": "string",
            "description": "Three-letter ISO 4217 currency code (e.g. USD, EUR). Must be uppercase. Optional — bank accounts may be created without a currency.",
            "pattern": "^[A-Z]{3}$",
            "nullable": true
          },
          "role": {
            "type": "string",
            "description": "Canonical account role within a program. Null when the account has no declared role.",
            "enum": [
              "fbo",
              "ach_suspense",
              "network_settlement",
              "secondary_network",
              "revenue",
              "cardholder_adjustment",
              "cash_load",
              "unload_settlement",
              "reserve",
              "escheat",
              "gl",
              "operating"
            ],
            "nullable": true,
            "readOnly": true
          },
          "program": {
            "type": "object",
            "description": "The program this account belongs to, if any.",
            "nullable": true,
            "readOnly": true,
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BankAccountBalance": {
        "type": "object",
        "required": ["bank_account_id", "date", "balance_type", "amount_cents"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier"
          },
          "bank_account_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID of the bank account this balance belongs to"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The date of the balance"
          },
          "balance_type": {
            "type": "string",
            "enum": [
              "opening_ledger",
              "closing_ledger",
              "current_ledger",
              "opening_available",
              "opening_available_next_business_day",
              "closing_available",
              "current_available",
              "previously_closed_book",
              "other"
            ],
            "description": "The type of balance"
          },
          "amount_cents": {
            "type": "integer",
            "description": "The balance amount in cents (e.g., 1234 = $12.34)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AgentFile": {
        "type": "object",
        "description": "A file attached to an agent (input) or produced by a run (output). `url` is an absolute, signed download link on the API host; input files can also be fetched with `GET /agents/{id}/files/{file_id}`.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "filename": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "content_type": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string",
            "description": "Absolute signed download URL on the API host (e.g. `https://api.endclose.com/rails/active_storage/blobs/redirect/...`). Fetching it needs no credentials; it 302s to a short-lived object-storage URL."
          }
        }
      },
      "Agent": {
        "type": "object",
        "required": ["name", "instructions"],
        "description": "An agent: standing instructions executed in an isolated sandbox with access to the environment's data, on a schedule or on demand. The same objects the app's Agents page manages.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": ["llm", "browser"],
            "readOnly": true,
            "description": "What executes the agent's runs. `llm` agents run their instructions in a sandbox. `browser` agents run a fixed automation against a web portal and import what it exports; they have no instructions, are set up by End Close, and cannot be created or converted through the API."
          },
          "browser": {
            "type": "object",
            "readOnly": true,
            "description": "Present on `browser` agents only. The portal login itself is never returned.",
            "properties": {
              "automation": {
                "type": "string",
                "description": "The automation the agent runs, e.g. `snb_activity_export_v1`"
              },
              "portal_url": {
                "type": "string"
              },
              "has_credentials": {
                "type": "boolean",
                "description": "Whether a portal login is saved"
              },
              "has_browser_context": {
                "type": "boolean",
                "description": "Whether the agent has signed in to the portal before"
              },
              "settings": {
                "type": "object",
                "description": "Automation settings: `lookback_days` and `streams`, which maps each export type to the key of the data stream it is imported into, and `oldest_pending_on` (set by the agent, absent when there is none): the created date of the oldest transaction the last run found that the bank had not settled yet (not sent, cancelled or failed — this includes one that is approved and already imported), which the next run looks back to",
                "additionalProperties": true
              },
              "needs_verification": {
                "type": "boolean",
                "description": "Whether the agent is paused until a person signs it in to the portal. Runs fail while this is true."
              },
              "verification_reason": {
                "type": "string",
                "enum": ["code_requested", "never_signed_in"],
                "nullable": true,
                "description": "Why the agent needs verification: `code_requested` when the portal asked for a verification code, `never_signed_in` when the agent has not signed in yet. Null when it does not."
              },
              "verification_required_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Set when the portal asked for a verification code, which a run cannot enter. Runs fail until someone signs the agent in again."
              }
            }
          },
          "name": {
            "type": "string",
            "description": "Unique within the environment"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "instructions": {
            "type": "string",
            "description": "The standing prompt every run executes. Empty for `browser` agents"
          },
          "mentions": {
            "type": "array",
            "description": "Reconciliations, data streams, users, and other agents whose context is resolved into every run's prompt. Mentioning an agent also teaches the run how to trigger it (see POST /agents/{id}/runs)",
            "items": {
              "type": "object",
              "required": ["type", "id", "label"],
              "properties": {
                "type": {
                  "type": "string",
                  "enum": ["reconciliation", "data_stream", "user", "agent"]
                },
                "id": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              }
            }
          },
          "model": {
            "type": "string",
            "nullable": true,
            "description": "The model the agent runs on — resolved to the platform default when not set explicitly. Null for `browser` agents, which run on no model"
          },
          "cadence": {
            "type": "string",
            "enum": ["manual", "hourly", "daily", "weekdays", "weekly"],
            "description": "`manual` agents only run when triggered (POST /agents/{id}/runs)"
          },
          "hour": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 23,
            "description": "Required for daily, weekdays, and weekly cadences"
          },
          "minute": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 59,
            "description": "Required for every cadence except manual"
          },
          "day_of_week": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 6,
            "description": "0 = Sunday. Required for weekly cadence"
          },
          "timezone": {
            "type": "string",
            "description": "IANA timezone the schedule is evaluated in",
            "default": "UTC"
          },
          "active": {
            "type": "boolean",
            "description": "Inactive agents are never scheduled but can still be run on demand"
          },
          "handles_file_drops": {
            "type": "boolean",
            "description": "Whether this agent processes files dropped in the app. At most one active agent per environment can have this enabled."
          },
          "delivery_enabled": {
            "type": "boolean",
            "description": "Email output files matching delivery_file_pattern to delivery_recipients after each completed run"
          },
          "delivery_recipients": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            }
          },
          "delivery_file_pattern": {
            "type": "string",
            "description": "Case-insensitive glob selecting which output files are delivered",
            "default": "*.csv"
          },
          "input_files": {
            "type": "array",
            "description": "Files attached for every run to use — managed with `POST /agents/{id}/files` and `DELETE /agents/{id}/files/{file_id}`",
            "items": {
              "$ref": "#/components/schemas/AgentFile"
            }
          },
          "next_run_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_run_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AgentRun": {
        "type": "object",
        "description": "One execution of an agent. Unset fields (e.g. error_message on a successful run) are omitted from responses.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "scheduled_agent_id": {
            "type": "string",
            "format": "uuid",
            "description": "The agent this run belongs to"
          },
          "triggered_by": {
            "type": "string",
            "enum": ["manual", "schedule", "resume", "drop", "api", "agent"],
            "description": "`api` for runs started through this API with an API key; `agent` for runs started by another agent's run using its session key"
          },
          "triggered_by_user_id": {
            "type": "string",
            "nullable": true,
            "description": "The user who started a manual run; null for API-triggered runs"
          },
          "triggered_by_run_id": {
            "type": "string",
            "nullable": true,
            "description": "For agent-triggered runs: the run whose agent started this one"
          },
          "agent_review_task_id": {
            "type": "string",
            "nullable": true,
            "description": "For resume runs: the review task whose resolution this run executes"
          },
          "status": {
            "type": "string",
            "enum": ["pending", "running", "waiting", "completed", "failed", "cancelled"]
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "duration_ms": {
            "type": "integer",
            "nullable": true
          },
          "error_message": {
            "type": "string",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "nullable": true,
            "description": "The agent's markdown summary of what it did"
          },
          "model": {
            "type": "string",
            "nullable": true,
            "description": "The model this run executed on"
          },
          "total_input_tokens": {
            "type": "integer",
            "nullable": true
          },
          "total_output_tokens": {
            "type": "integer",
            "nullable": true
          },
          "output_files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentFile"
            },
            "description": "Files the agent produced for the operator"
          },
          "dropped_files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentFile"
            },
            "description": "For drop runs: the file(s) this run was triggered to process"
          },
          "delivery": {
            "type": "object",
            "nullable": true,
            "description": "The email delivery record for this run's output files, when delivery ran"
          },
          "transcript": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Full event transcript. Only included when fetching a single run"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  },
  "webhooks": {
    "record.reconciled": {
      "post": {
        "description": "When a record is reconciled",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return a 200 status to indicate that the data was received successfully"
          }
        }
      }
    },
    "record.reconciliation_overdue": {
      "post": {
        "description": "When a record has gone beyond its expected reconciliation date",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return a 200 status to indicate that the data was received successfully"
          }
        }
      }
    }
  },
  "paths": {
    "/data_streams": {
      "get": {
        "summary": "List data streams",
        "responses": {
          "200": {
            "description": "List of data streams",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/DataStream"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a data stream",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["key", "name"],
                "properties": {
                  "key": {
                    "type": "string",
                    "description": "Unique immutable identifier"
                  },
                  "name": {
                    "type": "string",
                    "description": "Human readable name"
                  },
                  "type": {
                    "type": "string",
                    "enum": ["api", "bank_feed"],
                    "default": "api",
                    "description": "The type of data stream to create"
                  },
                  "bank_account_id": {
                    "type": "string",
                    "description": "Required when type is bank_feed. The ID of the bank account to link."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Data stream created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataStream"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (e.g. missing required fields, duplicate key, bank_account_id not found)"
          }
        }
      }
    },
    "/data_streams/{key}": {
      "get": {
        "summary": "Retrieve a data stream",
        "description": "Retrieve a data stream using its unique immutable key.",
        "parameters": [
          {
            "in": "path",
            "name": "key",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The immutable data stream key"
          }
        ],
        "responses": {
          "200": {
            "description": "Data stream retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataStream"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Data stream not found"
          }
        }
      },
      "patch": {
        "summary": "Update a data stream",
        "description": "Update the name of a data stream.",
        "parameters": [
          {
            "in": "path",
            "name": "key",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The immutable data stream key"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New human readable name"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Data stream updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataStream"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Data stream not found"
          }
        }
      },
      "delete": {
        "summary": "Delete a data stream",
        "description": "Permanently deletes a data stream and all associated records and property definitions.\n\n**Sync vs async:** Streams with at most 10,000 records are deleted synchronously and return 204. Streams above that threshold *must* be deleted asynchronously by passing `?async=true`; otherwise the request fails with 409. Callers may also opt into the async path explicitly for any size. The async path returns 202 with a `DataStreamOperation` payload; poll `GET /data_streams/{key}/operations/{id}` for progress.",
        "parameters": [
          {
            "in": "path",
            "name": "key",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The immutable data stream key"
          },
          {
            "in": "query",
            "name": "async",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["true", "false"]
            },
            "description": "When `\"true\"`, run the delete asynchronously and return 202 + a `DataStreamOperation`. Required for streams with more than 10,000 records."
          }
        ],
        "responses": {
          "204": {
            "description": "Data stream deleted synchronously (record count ≤ 10,000 and `async` not requested)"
          },
          "202": {
            "description": "Async delete accepted. Poll `GET /data_streams/{key}/operations/{id}` (also set in the `Location` header) until `status` is `completed` or `failed`.",
            "headers": {
              "Location": {
                "description": "URL of the operation to poll",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataStreamOperation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Data stream not found"
          },
          "409": {
            "description": "Either (a) the stream is linked to reconciliations and cannot be deleted, or (b) it has more than 10,000 records and `?async=true` was not provided.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/data_streams/{key}/reset": {
      "post": {
        "summary": "Reset a data stream",
        "description": "Asynchronously wipes all records on the stream while preserving stream config, property definitions, and reconciliation links. Always async. Poll the returned operation.\n\n**Completion is verified.** The operation reaches `completed` only after the stream is confirmed empty in the primary database, with `records_remaining` set to `0`. Records that land while the reset is running are swept up a bounded number of times; if any survive, the operation ends `failed` with `records_remaining` greater than `0` and an `error_message` asking you to stop ingestion and retry. Do not reload data onto the stream until you observe `completed`.",
        "parameters": [
          {
            "in": "path",
            "name": "key",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The immutable data stream key"
          }
        ],
        "responses": {
          "202": {
            "description": "Reset accepted. Poll `GET /data_streams/{key}/operations/{id}` (also set in the `Location` header).",
            "headers": {
              "Location": {
                "description": "URL of the operation to poll",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataStreamOperation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Data stream not found"
          }
        }
      }
    },
    "/data_streams/{key}/health": {
      "get": {
        "summary": "Get data stream ingestion health",
        "description": "Cheap, indexed snapshot of ingestion health for the stream — record count, file import totals, orphan records, bulk request counters by status, and last ingestion time. Safe to poll from the UI.",
        "parameters": [
          {
            "in": "path",
            "name": "key",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The immutable data stream key"
          }
        ],
        "responses": {
          "200": {
            "description": "Health snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataStreamHealth"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Data stream not found"
          }
        }
      }
    },
    "/data_streams/{key}/operations/{id}": {
      "get": {
        "summary": "Get a data stream operation",
        "description": "Poll a delete or reset operation kicked off against this data stream. Returns the serialized `DataStreamOperation`. On a terminal status, `records_remaining` is the primary-database count of records left on the stream (`0` on `completed`), so callers can trust the operation instead of re-listing records.",
        "parameters": [
          {
            "in": "path",
            "name": "key",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The immutable data stream key"
          },
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The operation ID returned by DELETE /data_streams/{key} or POST /data_streams/{key}/reset"
          }
        ],
        "responses": {
          "200": {
            "description": "The operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataStreamOperation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Operation not found or does not belong to this data stream"
          }
        }
      }
    },
    "/data_stream_property_definitions": {
      "get": {
        "summary": "List property definitions",
        "parameters": [
          {
            "in": "query",
            "name": "data_stream_key",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of property definitions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RecordPropertyDefinition"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a property definition",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordPropertyDefinition"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Property definition created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordPropertyDefinition"
                }
              }
            }
          }
        }
      }
    },
    "/data_stream_property_definitions/{id}": {
      "get": {
        "summary": "Retrieve a property definition",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Property definition",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordPropertyDefinition"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a property definition",
        "description": "Update the name or optional flag of a property definition.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "optional": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Property definition updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordPropertyDefinition"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a property definition",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Property definition deleted successfully"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Property definition not found"
          }
        }
      }
    },
    "/records": {
      "get": {
        "summary": "List records",
        "description": "Returns a paginated list of records, newest first. At least one of `data_stream_key`, `reconciliation_match_id`, `reconciliation_id`, `external_id`, or `import_id` is required.\n\nPagination is cursor-based: pass each response's `next_cursor` back as `cursor` until `next_cursor` is null. The legacy `offset` parameter and its `has_more`/`offset`/`total_count` response shape have been removed.",
        "parameters": [
          {
            "in": "query",
            "name": "data_stream_key",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by data stream key"
          },
          {
            "in": "query",
            "name": "reconciliation_match_id",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter to records that belong to the given match"
          },
          {
            "in": "query",
            "name": "reconciliation_id",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter to records that have a status row in the given reconciliation"
          },
          {
            "in": "query",
            "name": "external_id",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter to records with this exact `external_id`"
          },
          {
            "in": "query",
            "name": "import_id",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Filter to records created by this import: a file import's id, or a bulk request's id"
          },
          {
            "in": "query",
            "name": "created_after",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO8601 lower bound on `created_at`"
          },
          {
            "in": "query",
            "name": "created_before",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO8601 upper bound on `created_at`"
          },
          {
            "in": "query",
            "name": "source",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["single_api", "csv_upload", "nacha_upload", "bulk_api", "plaid", "airwallex", "flows"]
            },
            "description": "Filter via the joined file import's `source`"
          },
          {
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["matched", "unmatched"]
            },
            "description": "Filter by match status"
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from a previous response's `next_cursor`. Omit for the first page."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of records",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Record"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Opaque cursor for the next page. Null when no more pages."
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "No scoping filter provided, or invalid `status`, `created_after`, or `created_before`"
          }
        }
      },
      "post": {
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "summary": "Create a record",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Record"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Record created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Record"
                }
              }
            }
          }
        }
      }
    },
    "/records/bulk": {
      "post": {
        "summary": "Bulk create records",
        "description": "Submit up to 1,000 records for asynchronous processing. Returns immediately with a bulk request ID. Each record is validated and inserted independently, so some records can succeed while others fail. Use the `GET /bulk_requests/{id}` endpoint to poll for results.",
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string"
            },
            "description": "Optional idempotency key. If a bulk request with this key already exists for your environment, the existing request is returned instead of creating a new one."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["records"],
                "properties": {
                  "on_conflict": {
                    "type": "string",
                    "enum": ["skip", "error"],
                    "default": "skip",
                    "description": "How to handle records whose `external_id` already exists in the target stream. `skip` (default) silently skips duplicates and counts them in `skipped_items`. `error` surfaces duplicates as per-row failures. Only applies to records with `external_id` set."
                  },
                  "records": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1000,
                    "items": {
                      "type": "object",
                      "required": [
                        "date",
                        "data_stream_key",
                        "amount",
                        "direction"
                      ],
                      "properties": {
                        "date": {
                          "type": "string",
                          "format": "date",
                          "description": "The date of the record"
                        },
                        "data_stream_key": {
                          "type": "string",
                          "description": "Key of the data stream this record belongs to"
                        },
                        "amount": {
                          "type": "integer",
                          "description": "The monetary amount in cents (e.g., 1234 = $12.34)"
                        },
                        "currency": {
                          "type": "string",
                          "description": "Three-letter ISO 4217 currency code (e.g. USD, EUR). **Optional — defaults to `USD` when omitted.** Case-insensitive on input.",
                          "pattern": "^[A-Za-z]{3}$",
                          "default": "USD"
                        },
                        "direction": {
                          "type": "string",
                          "enum": ["credit", "debit"],
                          "description": "Whether this is a credit or debit"
                        },
                        "description": {
                          "type": "string",
                          "description": "Optional description"
                        },
                        "external_id": {
                          "type": "string",
                          "description": "Optional external identifier"
                        },
                        "metadata": {
                          "type": "object",
                          "additionalProperties": true,
                          "default": {},
                          "description": "Arbitrary key-value metadata"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Bulk request accepted for processing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkRequestSummary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid `on_conflict` value",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "422": {
            "description": "Invalid request (e.g. records is not an array, empty, or exceeds 1,000 items)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/bulk_requests/{id}": {
      "get": {
        "summary": "Get bulk request status",
        "description": "Poll the status of an asynchronous bulk request. Returns the bulk request payload plus paginated per-row results. Results grow as processing continues, so only non-pending items are included.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The bulk request ID returned from POST /records/bulk"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "description": "Number of result items to return"
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Pagination offset for result items"
          }
        ],
        "responses": {
          "200": {
            "description": "Bulk request payload with paginated results",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/BulkRequest"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "results": {
                          "type": "object",
                          "properties": {
                            "data": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "index": {
                                    "type": "integer",
                                    "description": "Zero-based position in the original request array"
                                  },
                                  "status": {
                                    "type": "string",
                                    "enum": ["successful", "failed", "skipped"]
                                  },
                                  "record_id": {
                                    "type": "string",
                                    "format": "uuid",
                                    "description": "ID of the created record. Only present when status is successful"
                                  },
                                  "error": {
                                    "type": "string",
                                    "description": "Error message. Only present when status is failed"
                                  }
                                }
                              }
                            },
                            "has_more": {
                              "type": "boolean"
                            },
                            "offset": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Bulk request not found"
          }
        }
      }
    },
    "/bulk_requests/{id}/cancel": {
      "post": {
        "summary": "Cancel a bulk request",
        "description": "Sets `cancellation_requested_at` so the running job exits cleanly to `cancelled` on its next heartbeat. Idempotent — re-cancelling just bumps the timestamp.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Cancellation requested",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkRequest"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Bulk request not found"
          },
          "409": {
            "description": "Bulk request is not in a cancellable status (must be `pending` or `processing`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/bulk_requests/{id}/retry": {
      "post": {
        "summary": "Retry a bulk request",
        "description": "Creates a new bulk request that re-runs only the parent's failed and skipped items. The new request's `parent_bulk_request_id` points at the original.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The ID of the failed or cancelled bulk request to retry"
          }
        ],
        "responses": {
          "202": {
            "description": "Retry created and enqueued. Returns the **new** bulk request, not the parent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkRequest"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Bulk request not found"
          },
          "409": {
            "description": "Bulk request is not in a retryable status (must be `failed` or `cancelled`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/relays/config": {
      "get": {
        "summary": "Get relay configuration",
        "description": "The routes document End Close is managing for the environment this API key belongs to: which processor webhook routes the relay accepts, how each is verified, and the field map that decides what leaves your network.\n\nThis is the only thing End Close tells a relay, and the status code is the whole message. A 200 is itself the statement that End Close owns this configuration, so the relay runs the document and hides its own config editor — there is no local copy that can drift. A 304 says the same and that nothing moved. A 404 means End Close is not managing this environment (nothing provisioned, or the switch turned off in Settings → Relay). It is a state, not a terminal answer: keep the document you already have, fall back to local configuration if you have one, and keep polling at your normal cadence — this is the only way you will hear that management was switched back on, and stopping forwarding on a 404 would turn a settings toggle into an outage.\n\nPoll it with `If-None-Match`. The 200 carries a strong `ETag` identifying the document, which changes only when the document itself changes — a save that alters nothing keeps it — so the ordinary answer on a heartbeat is a 304 with no body. The API key alone resolves the environment: no parameters are needed, and rotating the key does not change the document. `routes` has the same content as the `routes:` block of the relay's `relay.yaml`; `environment` is informational.",
        "parameters": [
          {
            "in": "header",
            "name": "If-None-Match",
            "required": false,
            "schema": { "type": "string" },
            "description": "The ETag from the last 200. Send it back and an unchanged document answers 304."
          }
        ],
        "responses": {
          "200": {
            "description": "The provisioned routes document. The ETag header identifies it.",
            "headers": {
              "ETag": {
                "description": "Strong validator for this document. Opaque to the relay: store it and send it back as If-None-Match.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelayConfig"
                }
              }
            }
          },
          "304": {
            "description": "Not Modified — End Close still owns this configuration and the document has not changed. No body; keep running what you have."
          },
          "401": {
            "description": "Unauthorized — missing, invalid, or revoked API key"
          },
          "403": {
            "description": "Forbidden — the key is a run-scoped agent session key, not a relay key"
          },
          "404": {
            "description": "End Close is not managing the relay configuration for this environment. Keep the document you have and keep polling — a 404 is a state, not a terminal answer."
          }
        }
      }
    },
    "/relays/events": {
      "post": {
        "summary": "Accept a legacy relay check-in",
        "description": "Accepted for legacy relay clients (v0.11.0–v0.12.4) that still call home with `{ \"name\", \"properties\" }` (`relay_boot`, `relay_heartbeat`, `relay_error`, `relay_shutdown`, `relay_killswitch`, `relay_config_applied`, `relay_batch_parked`, or whatever an older build sends). The body is logged and discarded: nothing is stored, and this is not a configuration channel. It does not return `{ config: { managed, fingerprint } }`. Current relays report themselves with PUT /relays/instances/{instance_id} and read configuration from GET /relays/config. A 204 with an empty body means the event was accepted. Same API-key auth as the other /relays endpoints.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Whatever the legacy client sends. `name` and `properties` are logged; other keys are logged too. Names are not validated.",
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "relay_heartbeat"
                  },
                  "properties": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Event fields the client allowlists (version, config YAML, queue, routes, stack, …). Logged in full. `config` may be the relay's YAML; `stack` is treated as sensitive."
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Accepted and logged. Empty body. Not a configuration response."
          },
          "400": {
            "description": "Body is not JSON, or exceeds 64 KB"
          },
          "401": {
            "description": "Unauthorized — missing, invalid, or revoked API key"
          },
          "403": {
            "description": "Forbidden — the key is a run-scoped agent session key, not a relay key"
          }
        }
      }
    },
    "/relays/instances/{instance_id}": {
      "put": {
        "summary": "Report a relay instance's manifest",
        "description": "A running relay instance describing itself: host kind, engine version, the routes-document schema version it understands, where its configuration comes from, and the adapters and enrichments it has registered. Idempotent replace keyed by the environment the API key belongs to plus a host-chosen instance id (up to 128 characters; embedded hosts without a configured id use a random UUID per process). Send it on boot and on every heartbeat. End Close checks any document saved for the environment against the instances seen in the last 24 hours: every route `source` must be an adapter every live instance has, every `enrich:` an enrichment every live instance has registered (none at all while any live instance is the standard application), and no document feature newer than the oldest instance's `config_schema`. Instances unseen for 30 days are deleted. Nothing is required of a relay that never sends a manifest: authoring then falls back to the built-in adapters and no enrichments.",
        "parameters": [
          {
            "in": "path",
            "name": "instance_id",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 128
            },
            "description": "Host-chosen identifier for this running instance"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RelayInstanceManifest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Manifest stored"
          },
          "400": {
            "description": "Malformed manifest; the message names the field"
          },
          "401": {
            "description": "Unauthorized — missing, invalid, or revoked API key"
          },
          "403": {
            "description": "Forbidden — the key is a run-scoped agent session key, not a relay key"
          },
          "413": {
            "description": "Body exceeds 64 KB"
          }
        }
      }
    },
    "/file_imports": {
      "get": {
        "summary": "List file imports",
        "description": "Returns a cursor-paginated list of file imports across all data streams, newest first. Filter to one stream with `data_stream_key` (equivalent to GET /data_streams/{key}/file_imports, which also returns per-import aggregates).",
        "parameters": [
          {
            "in": "query",
            "name": "data_stream_key",
            "schema": {
              "type": "string"
            },
            "description": "Restrict to one data stream"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from a previous response's `next_cursor`. Pass it back unchanged; `next_cursor: null` is the only terminator."
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "replicating",
                "completed",
                "failed",
                "cancelled"
              ]
            }
          },
          {
            "in": "query",
            "name": "source",
            "schema": {
              "type": "string"
            },
            "description": "Filter by import source (e.g. `csv`, `nacha`, `bulk_api`)"
          },
          {
            "in": "query",
            "name": "created_after",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO8601; inclusive"
          },
          {
            "in": "query",
            "name": "created_before",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO8601; inclusive"
          }
        ],
        "responses": {
          "200": {
            "description": "Cursor-paginated list of file imports",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FileImport"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Pass as `cursor` for the next page; null on the last page"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/data_streams/{key}/file_imports": {
      "get": {
        "summary": "List file imports",
        "description": "Returns a cursor-paginated list of file imports for a data stream, newest first. Rows on this stream-scoped route additionally carry the ClickHouse aggregates (record_count, total_credits, total_debits, net_amount, date_range_start, date_range_end, currency).",
        "parameters": [
          {
            "in": "path",
            "name": "key",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The immutable data stream key"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from a previous response's `next_cursor`. Pass it back unchanged; `next_cursor: null` is the only terminator."
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "replicating",
                "completed",
                "failed",
                "cancelled"
              ]
            }
          },
          {
            "in": "query",
            "name": "source",
            "schema": {
              "type": "string"
            },
            "description": "Filter by import source (e.g. `csv`, `nacha`, `bulk_api`)"
          },
          {
            "in": "query",
            "name": "created_after",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO8601; inclusive"
          },
          {
            "in": "query",
            "name": "created_before",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO8601; inclusive"
          }
        ],
        "responses": {
          "200": {
            "description": "Cursor-paginated list of file imports",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FileImport"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Pass as `cursor` for the next page; null on the last page"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Data stream not found"
          }
        }
      }
    },
    "/file_imports/{id}": {
      "get": {
        "summary": "Retrieve a file import",
        "description": "Retrieve a single file import by its unique ID.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "outcome",
            "schema": {
              "type": "string"
            },
            "description": "Filter the embedded row errors by outcome"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "description": "Page size for the embedded row errors"
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Offset for the embedded row errors"
          }
        ],
        "responses": {
          "200": {
            "description": "The file import, plus an offset-paginated page of its row errors. Successful rows are not listed here — they are real records.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/FileImport"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "errors": {
                          "type": "object",
                          "properties": {
                            "data": {
                              "type": "array",
                              "items": {
                                "type": "object"
                              }
                            },
                            "has_more": {
                              "type": "boolean"
                            },
                            "offset": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "File import not found"
          }
        }
      }
    },
    "/file_imports/{id}/records": {
      "get": {
        "summary": "List records for a file import",
        "description": "Returns a paginated list of records belonging to a specific file import.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the file import"
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "in": "query",
            "name": "offset",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Pagination offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of records",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Record"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "offset": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Import batch not found"
          }
        }
      }
    },
    "/file_imports/{id}/cancel": {
      "post": {
        "summary": "Cancel a file import",
        "description": "Sets `cancellation_requested_at` so the running job exits cleanly to `cancelled` on its next heartbeat. Idempotent — re-cancelling just bumps the timestamp.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Cancel accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileImport"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "File import not found"
          },
          "409": {
            "description": "File import is not in a cancellable status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/file_imports/{id}/retry": {
      "post": {
        "summary": "Retry a file import",
        "description": "In-place retry: resumes an unfinished main pass from `processed_offset` and re-runs the queued row errors. No child import is created.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Retry accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileImport"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "File import not found"
          },
          "409": {
            "description": "File import is not in a retryable status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/records/{id}": {
      "get": {
        "summary": "Retrieve a record",
        "description": "Retrieve a single record by its unique ID.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the record"
          }
        ],
        "responses": {
          "200": {
            "description": "Record retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Record"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Record not found"
          }
        }
      },
      "patch": {
        "summary": "Update a record",
        "description": "Update a record's fields. The record **must have a status of `unreconciled`**. Records that are partially or fully reconciled cannot be modified.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the record"
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "integer",
                    "description": "The monetary amount in cents (e.g., 1234 = $12.34)"
                  },
                  "direction": {
                    "type": "string",
                    "enum": ["credit", "debit"]
                  },
                  "description": {
                    "type": "string"
                  },
                  "date": {
                    "type": "string",
                    "format": "date"
                  },
                  "external_id": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Record updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Record"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Record not found"
          },
          "409": {
            "description": "Record cannot be modified because it has already been reconciled"
          }
        }
      },
      "delete": {
        "summary": "Delete a record",
        "description": "Deletes a single record by its ID. The record **must have a status of `unreconciled`**. Records that are partially or fully reconciled cannot be deleted.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the record"
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "responses": {
          "204": {
            "description": "Record deleted successfully"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Record not found"
          },
          "409": {
            "description": "Record cannot be deleted because it is already reconciled or partially reconciled"
          }
        }
      }
    },
    "/reconciliations/{reconciliation_id}/records/{record_id}/mark_overdue": {
      "post": {
        "summary": "Mark a record as overdue",
        "description": "Flags a record as overdue within a specific reconciliation by setting the `overdue_at` timestamp on its reconciliation entry. The reconciliation and record are both named in the path — the subject is the (reconciliation, record) entry, so the same record can be overdue in one reconciliation and matched in another. The record's data stream must be part of the reconciliation. If no entry exists yet for the record and reconciliation, one is created so unmatched records can be flagged as overdue. Records already matched in this reconciliation cannot be marked overdue. Once overdue, the record is picked up by the exceptions sweeper and rolled into the data-stream-scoped exception for that reconciliation.",
        "parameters": [
          {
            "in": "path",
            "name": "reconciliation_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the reconciliation the record should be marked overdue in"
          },
          {
            "in": "path",
            "name": "record_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the record"
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "overdue_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the record should be considered overdue. Defaults to the current time."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The record's entry in the given reconciliation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationEntry"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Record or reconciliation not found"
          },
          "409": {
            "description": "Record is already matched in this reconciliation and cannot be marked overdue"
          },
          "422": {
            "description": "Record's data stream is not part of the reconciliation, or invalid `overdue_at`"
          }
        }
      }
    },
    "/reconciliations": {
      "get": {
        "summary": "List reconciliations",
        "description": "Returns a paginated list of reconciliations with their associated data streams, newest first.",
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from a previous response's `next_cursor`. Omit for the first page."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of reconciliations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Reconciliation"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Opaque cursor for the next page. Null when no more pages."
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a reconciliation",
        "description": "Create a new reconciliation with optional side A and side B data stream associations.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "run_type": {
                    "type": "string",
                    "enum": ["continuous", "batch"],
                    "default": "continuous"
                  },
                  "side_a_stream_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "IDs of data streams to assign to side A"
                  },
                  "side_b_stream_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "IDs of data streams to assign to side B"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Reconciliation created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Reconciliation"
                }
              }
            }
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/reconciliations/{id}": {
      "get": {
        "summary": "Retrieve a reconciliation",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reconciliation retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Reconciliation"
                }
              }
            }
          },
          "404": {
            "description": "Reconciliation not found"
          }
        }
      },
      "patch": {
        "summary": "Update a reconciliation",
        "description": "Update reconciliation name, run type, or reassign data streams. If stream IDs are provided, all existing stream associations are replaced.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "run_type": {
                    "type": "string",
                    "enum": ["continuous", "batch"]
                  },
                  "side_a_stream_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "side_b_stream_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reconciliation updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Reconciliation"
                }
              }
            }
          },
          "404": {
            "description": "Reconciliation not found"
          }
        }
      },
      "delete": {
        "summary": "Delete a reconciliation",
        "description": "Permanently deletes a reconciliation and all associated rules, matches, and data stream associations.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Reconciliation deleted successfully"
          },
          "404": {
            "description": "Reconciliation not found"
          }
        }
      }
    },
    "/reconciliation_rules": {
      "get": {
        "summary": "List reconciliation rules",
        "description": "Returns all rules for a reconciliation, ordered by priority.",
        "parameters": [
          {
            "in": "query",
            "name": "reconciliation_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The reconciliation to list rules for"
          }
        ],
        "responses": {
          "200": {
            "description": "List of rules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ReconciliationRule"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "summary": "Create a reconciliation rule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["reconciliation_id", "filters"],
                "properties": {
                  "reconciliation_id": {
                    "type": "string"
                  },
                  "filters": {
                    "$ref": "#/components/schemas/ReconciliationRule/properties/filters"
                  },
                  "priority": {
                    "type": "integer",
                    "default": 0
                  },
                  "group_by": {
                    "$ref": "#/components/schemas/ReconciliationRule/properties/group_by"
                  },
                  "cardinality": {
                    "$ref": "#/components/schemas/ReconciliationRule/properties/cardinality"
                  },
                  "rollup": {
                    "$ref": "#/components/schemas/ReconciliationRule/properties/rollup"
                  },
                  "description": {
                    "type": "string",
                    "description": "Optional human-readable description of what this rule matches"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Rule created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationRule"
                }
              }
            }
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/reconciliation_rules/{id}": {
      "get": {
        "summary": "Retrieve a reconciliation rule",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rule retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationRule"
                }
              }
            }
          },
          "404": {
            "description": "Rule not found"
          }
        }
      },
      "patch": {
        "summary": "Update a reconciliation rule",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "filters": {
                    "type": "object"
                  },
                  "priority": {
                    "type": "integer"
                  },
                  "group_by": {
                    "$ref": "#/components/schemas/ReconciliationRule/properties/group_by"
                  },
                  "cardinality": {
                    "$ref": "#/components/schemas/ReconciliationRule/properties/cardinality"
                  },
                  "rollup": {
                    "$ref": "#/components/schemas/ReconciliationRule/properties/rollup"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rule updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationRule"
                }
              }
            }
          },
          "404": {
            "description": "Rule not found"
          }
        }
      },
      "delete": {
        "summary": "Delete a reconciliation rule",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "responses": {
          "204": {
            "description": "Rule deleted successfully"
          },
          "404": {
            "description": "Rule not found"
          }
        }
      }
    },
    "/reconciliations/{id}/agent_status": {
      "get": {
        "summary": "Get reconciliation agent status",
        "description": "Returns the current status of the reconciliation agent, which automatically creates and refines matching rules. The agent runs automatically when records are uploaded or manually matched — no manual triggering is needed.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The reconciliation ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Agent status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": ["idle", "running", "failed"],
                      "description": "Current agent status"
                    },
                    "last_run_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "When the agent last ran"
                    },
                    "progress": {
                      "type": "array",
                      "description": "Ordered list of progress steps from the last run",
                      "items": {
                        "type": "object",
                        "properties": {
                          "step": {
                            "type": "string"
                          },
                          "message": {
                            "type": "string"
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "reasoning": {
                      "type": "string",
                      "nullable": true,
                      "description": "AI reasoning from the last rule analysis"
                    },
                    "rules_count": {
                      "type": "integer",
                      "description": "Number of active rules on this reconciliation"
                    },
                    "unmatched_count": {
                      "type": "integer",
                      "description": "Number of unreconciled records across both sides"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Reconciliation not found"
          }
        }
      }
    },
    "/reconciliations/{id}/run": {
      "post": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The reconciliation ID"
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "summary": "Run a reconciliation",
        "description": "Starts a batch run of the matching engine over the reconciliation's unmatched records and active rules, the same run as Run in the app. The run works in the background: poll `GET /reconciliations/{id}`, whose `current_run` is this run while it is pending or running and whose `last_run` is it once it has completed or failed. Only batch reconciliations can be run on demand; continuous reconciliations match each record as it arrives. A reconciliation has at most one run in progress.",
        "responses": {
          "202": {
            "description": "Run queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reconciliation_id": {
                      "type": "string"
                    },
                    "run": {
                      "$ref": "#/components/schemas/ReconciliationRun"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Reconciliation not found"
          },
          "409": {
            "description": "A run of this reconciliation is already pending or running. The body's `run` is that run, which you can poll instead.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "run": {
                      "$ref": "#/components/schemas/ReconciliationRun"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The reconciliation is continuous, so it has no on-demand run"
          }
        }
      }
    },
    "/reconciliation_stories": {
      "get": {
        "summary": "List reconciliation stories",
        "description": "Returns all reconciliation stories in the environment, newest first.",
        "responses": {
          "200": {
            "description": "List of stories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReconciliationStory"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a reconciliation story",
        "description": "Create a story and attach a chain of reconciliations. The first link must use role `origin` and have no shared_data_stream_id; subsequent links must use role `continuation` and specify the shared data stream linking them to the previous reconciliation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "links": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["reconciliation_id", "position"],
                      "properties": {
                        "reconciliation_id": {
                          "type": "string"
                        },
                        "position": {
                          "type": "integer",
                          "description": "1-indexed position in the chain"
                        },
                        "role": {
                          "type": "string",
                          "enum": ["origin", "continuation"],
                          "default": "origin"
                        },
                        "shared_data_stream_id": {
                          "type": "string",
                          "nullable": true,
                          "description": "Required for continuation links"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Story created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationStory"
                }
              }
            }
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/reconciliation_stories/{id}": {
      "get": {
        "summary": "Retrieve a reconciliation story",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Story retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationStory"
                }
              }
            }
          },
          "404": {
            "description": "Story not found"
          }
        }
      },
      "patch": {
        "summary": "Update a reconciliation story",
        "description": "Update the story name and/or replace its links. If `links` is provided, all existing links are replaced with the new chain. If `links` is omitted, only the name is updated.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "links": {
                    "type": "array",
                    "description": "Replace the entire chain. Omit to leave links unchanged.",
                    "items": {
                      "type": "object",
                      "required": ["reconciliation_id", "position"],
                      "properties": {
                        "reconciliation_id": {
                          "type": "string"
                        },
                        "position": {
                          "type": "integer"
                        },
                        "role": {
                          "type": "string",
                          "enum": ["origin", "continuation"]
                        },
                        "shared_data_stream_id": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Story updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationStory"
                }
              }
            }
          },
          "404": {
            "description": "Story not found"
          },
          "422": {
            "description": "Validation error"
          }
        }
      },
      "delete": {
        "summary": "Delete a reconciliation story",
        "description": "Permanently deletes a story and unlinks it from all reconciliations. The reconciliations themselves are not deleted.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Story deleted successfully"
          },
          "404": {
            "description": "Story not found"
          }
        }
      }
    },
    "/reconciliation_matches": {
      "get": {
        "summary": "List reconciliation matches",
        "description": "Returns a paginated list of matches, optionally filtered by reconciliation or status. Results are ordered by `created_at` descending, then `id` descending \u2014 a stable total order, so paging through the full list with `offset` returns every row exactly once even when many rows share a `created_at`.",
        "parameters": [
          {
            "in": "query",
            "name": "reconciliation_id",
            "schema": {
              "type": "string"
            },
            "description": "Filter by reconciliation"
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string"
            },
            "description": "Filter by match status"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer"
            },
            "description": "Pagination offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of matches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReconciliationMatch"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "offset": {
                      "type": "number"
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Total number of matches matching the filters, across all pages"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "summary": "Create a reconciliation match",
        "description": "Manually match a set of records. All records must be unreconciled in the specified reconciliation (a record matched in a different reconciliation is fine) and belong to its data streams.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["reconciliation_id", "record_ids"],
                "properties": {
                  "reconciliation_id": {
                    "type": "string"
                  },
                  "record_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "IDs of records to match together"
                  },
                  "create_adjustment": {
                    "type": "boolean",
                    "description": "If true and the records do not balance, creates an adjustment record so the match nets to zero. With records on both sides the adjustment is a credit on the side with the lower total. With records on one side only it is an offsetting entry on the other side, mirroring the selection's amount and direction. The record lives on the reconciliation's own adjustments stream for that side (a data stream of type `adjustments`, bound to this reconciliation alone), not on one of its feeds."
                  },
                  "adjustment_reason": {
                    "type": "string",
                    "description": "Description for the adjustment record"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Match created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationMatch"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (records not found, already reconciled, or not in reconciliation)"
          }
        }
      }
    },
    "/reconciliation_matches/{id}": {
      "get": {
        "summary": "Retrieve a reconciliation match",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Match retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationMatch"
                }
              }
            }
          },
          "404": {
            "description": "Match not found"
          }
        }
      },
      "delete": {
        "summary": "Delete a reconciliation match",
        "description": "Deletes a reconciliation match and resets all associated records to unreconciled status.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "responses": {
          "204": {
            "description": "Match deleted and records unreconciled"
          },
          "404": {
            "description": "Match not found"
          }
        }
      }
    },
    "/reconciliation_exceptions": {
      "get": {
        "summary": "List reconciliation exceptions",
        "description": "Returns a paginated list of exceptions, optionally filtered by reconciliation or status. Each row includes data-stream metadata and an aggregates summary so you can render a queue without a follow-up request per row. Results are ordered by `created_at` descending, then `id` descending \u2014 a stable total order, so paging through the full list with `offset` returns every row exactly once even when many rows share a `created_at`.",
        "parameters": [
          {
            "in": "query",
            "name": "reconciliation_id",
            "schema": {
              "type": "string"
            },
            "description": "Filter by reconciliation"
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": ["open", "investigating", "dismissed", "resolved"]
            },
            "description": "Filter by exception status"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer"
            },
            "description": "Pagination offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of exceptions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReconciliationException"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "offset": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      },
      "post": {
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "summary": "Create a reconciliation exception",
        "description": "Create an exception for a reconciliation and data stream. Only one `open` or `investigating` exception can exist per (data stream, reconciliation) pair at a time. `title` defaults to `\"{data_stream_name} — overdue records\"`, and `status` defaults to `open`. The system automatically creates exceptions via its overdue sweeper — use this endpoint when you want to open one manually.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["reconciliation_id", "data_stream_id"],
                "properties": {
                  "reconciliation_id": {
                    "type": "string",
                    "description": "Reconciliation the exception belongs to"
                  },
                  "data_stream_id": {
                    "type": "string",
                    "description": "Data stream the exception is scoped to. Must belong to the reconciliation."
                  },
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": ["open", "investigating", "dismissed", "resolved"],
                    "default": "open"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Exception created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationException"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Reconciliation or data stream not found"
          },
          "422": {
            "description": "Validation failed, or a duplicate open/investigating exception already exists for this (data stream, reconciliation)"
          }
        }
      }
    },
    "/reconciliation_exceptions/{id}": {
      "get": {
        "summary": "Retrieve a reconciliation exception",
        "description": "Returns the exception with assignees, comments, recent investigation sessions, `aggregates` for the entire overdue slice, and `filtered_aggregates` restricted to the supplied filter query parameters. All filter parameters are optional.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "amount_min",
            "schema": {
              "type": "integer"
            },
            "description": "Include records with signed amount >= amount_min (cents)"
          },
          {
            "in": "query",
            "name": "amount_max",
            "schema": {
              "type": "integer"
            },
            "description": "Include records with signed amount <= amount_max (cents)"
          },
          {
            "in": "query",
            "name": "date_from",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Include records with transaction date >= date_from"
          },
          {
            "in": "query",
            "name": "date_to",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Include records with transaction date <= date_to"
          },
          {
            "in": "query",
            "name": "direction",
            "schema": {
              "type": "string",
              "enum": ["debit", "credit"]
            },
            "description": "Include only debits (amount < 0) or credits (amount > 0)"
          },
          {
            "in": "query",
            "name": "counterparty",
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring match on record description"
          },
          {
            "in": "query",
            "name": "age_bucket",
            "schema": {
              "type": "string",
              "enum": ["lt_7d", "7_30d", "30_90d", "gt_90d"]
            },
            "description": "Convenience age filter on time since `overdue_at`"
          }
        ],
        "responses": {
          "200": {
            "description": "Exception retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationException"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Exception not found"
          }
        }
      },
      "patch": {
        "summary": "Update a reconciliation exception",
        "description": "Update the title, description, or status of an exception.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": ["open", "investigating", "dismissed", "resolved"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Exception updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationException"
                }
              }
            }
          },
          "404": {
            "description": "Exception not found"
          }
        }
      },
      "delete": {
        "summary": "Delete a reconciliation exception",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "responses": {
          "204": {
            "description": "Exception deleted successfully"
          },
          "404": {
            "description": "Exception not found"
          }
        }
      }
    },
    "/reconciliation_exceptions/{id}/findings": {
      "put": {
        "summary": "Write findings for an exception's lines",
        "description": "Upserts one finding per record of the exception — the note, status and suggested action your reviewers see on each open line. Findings are keyed on (exception, record), so sending the same record again replaces its finding. Up to 1,000 findings per request; each is validated independently and the response lists the rejected ones, so a partial batch still writes. Records already matched by rules do not need findings — write them for the lines that are left.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FindingsUpsertRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Findings written (possibly with per-finding errors)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FindingsUpsertResult"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Exception not found"
          },
          "422": {
            "description": "`findings` is not a list, or has more than 1,000 items"
          }
        }
      }
    },
    "/reconciliation_exceptions/{id}/review": {
      "get": {
        "summary": "Read the review of an exception",
        "description": "Returns the exception's findings with each line's derived status, filtered and paged. Lines sort `to_look_at` first, then `looks_right`, then `done`; within a status, newest record date first. `sort` puts a column in front of that order.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": ["all", "to_look_at", "looks_right", "done"],
              "default": "to_look_at"
            }
          },
          {
            "in": "query",
            "name": "q",
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring match on the record's description, external id, `metadata.client_name` or `metadata.line_num`, or the finding's note or category"
          },
          {
            "in": "query",
            "name": "category",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Only lines whose finding is in one of these categories (exact match). Repeat the parameter for several."
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": ["date", "description", "category", "amount", "other_amount", "difference", "quantity", "other_quantity", "quantity_difference", "status"]
            },
            "description": "Column to sort by. Amounts sort on magnitude; `difference` and `quantity_difference` on the signed gap; lines with nothing in the column come last. Unknown values keep the review order."
          },
          {
            "in": "query",
            "name": "dir",
            "schema": {
              "type": "string",
              "enum": ["asc", "desc"],
              "default": "asc"
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "in": "query",
            "name": "per_page",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the review",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewPage"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Exception not found"
          },
          "422": {
            "description": "The exception's data stream is no longer on the reconciliation"
          }
        }
      }
    },
    "/reconciliations/{reconciliation_id}/data_streams/{data_stream_id}/findings": {
      "put": {
        "summary": "Write findings for a data stream's lines",
        "description": "The same upsert as `PUT /reconciliation_exceptions/{id}/findings`, for an engine that runs before the sweeper has opened an exception. Opens the exception for the (reconciliation, data stream) pair if none is open, reuses it otherwise, and returns its id with the result.",
        "parameters": [
          {
            "in": "path",
            "name": "reconciliation_id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "data_stream_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The data stream whose records the findings are about. Must be part of the reconciliation."
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Send the same key when retrying a write you are not sure landed: the first response is returned again (with Idempotent-Replayed: true) instead of writing twice. A key used for a different request is refused with 422; one whose first request is still running gets 409. A refused or failed request does not use up its key. Keys are per environment and kept for 7 days."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FindingsUpsertRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Findings written against the open exception",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/FindingsUpsertResult"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "exception_id": {
                          "type": "string",
                          "description": "The open exception the findings were written to"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Reconciliation or data stream not found"
          },
          "422": {
            "description": "Data stream is not part of the reconciliation, or `findings` is invalid"
          }
        }
      }
    },
    "/bank_accounts": {
      "get": {
        "summary": "List bank accounts",
        "description": "Returns all bank accounts for the current environment, ordered by name.",
        "responses": {
          "200": {
            "description": "List of bank accounts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BankAccount"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create a bank account",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Human readable name for the bank account"
                  },
                  "currency": {
                    "type": "string",
                    "description": "Three-letter ISO 4217 currency code (e.g. USD, EUR). Must be uppercase. Optional.",
                    "pattern": "^[A-Z]{3}$"
                  },
                  "mask": {
                    "type": "string",
                    "description": "Last four digits of the account number, as it appears on statements. Set automatically for Plaid-linked accounts."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Bank account created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BankAccount"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/bank_accounts/{id}": {
      "get": {
        "summary": "Retrieve a bank account",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the bank account"
          }
        ],
        "responses": {
          "200": {
            "description": "Bank account retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BankAccount"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Bank account not found"
          }
        }
      },
      "patch": {
        "summary": "Update a bank account",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the bank account"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Human readable name for the bank account"
                  },
                  "currency": {
                    "type": "string",
                    "description": "Three-letter ISO 4217 currency code (e.g. USD, EUR). Must be uppercase. Optional.",
                    "pattern": "^[A-Z]{3}$"
                  },
                  "mask": {
                    "type": "string",
                    "description": "Last four digits of the account number, as it appears on statements. Set automatically for Plaid-linked accounts."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bank account updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BankAccount"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Bank account not found"
          }
        }
      },
      "delete": {
        "summary": "Delete a bank account",
        "description": "Permanently deletes a bank account. Returns 409 Conflict if the bank account has linked data streams.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the bank account"
          }
        ],
        "responses": {
          "204": {
            "description": "Bank account deleted successfully"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Bank account not found"
          },
          "409": {
            "description": "Cannot delete bank account because it has linked data streams"
          }
        }
      }
    },
    "/bank_account_balances": {
      "get": {
        "summary": "List bank account balances",
        "description": "Returns all balances for a bank account, ordered by date descending.",
        "parameters": [
          {
            "in": "query",
            "name": "bank_account_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The bank account to list balances for"
          }
        ],
        "responses": {
          "200": {
            "description": "List of bank account balances",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BankAccountBalance"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "422": {
            "description": "bank_account_id is required"
          }
        }
      },
      "post": {
        "summary": "Create or update a bank account balance",
        "description": "Creates a balance for the specified bank account. If a balance already exists for the same date and balance type, it will be updated with the new amount (upsert behavior).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bank_account_id",
                  "date",
                  "balance_type",
                  "amount_cents"
                ],
                "properties": {
                  "bank_account_id": {
                    "type": "string",
                    "description": "ID of the bank account"
                  },
                  "date": {
                    "type": "string",
                    "format": "date",
                    "description": "The date of the balance"
                  },
                  "balance_type": {
                    "type": "string",
                    "enum": [
                      "opening_ledger",
                      "closing_ledger",
                      "current_ledger",
                      "opening_available",
                      "opening_available_next_business_day",
                      "closing_available",
                      "current_available",
                      "previously_closed_book",
                      "other"
                    ],
                    "description": "The type of balance"
                  },
                  "amount_cents": {
                    "type": "integer",
                    "description": "The balance amount in cents (e.g., 1234 = $12.34)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Balance created or updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BankAccountBalance"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/bank_account_balances/bulk": {
      "post": {
        "summary": "Bulk create or update bank account balances",
        "description": "Submit up to 1,000 balances for a bank account in a single request. Each balance is upserted — if a balance already exists for the same date and balance type, it will be updated with the new amount. All balances are processed in a single transaction.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["bank_account_id", "balances"],
                "properties": {
                  "bank_account_id": {
                    "type": "string",
                    "description": "ID of the bank account"
                  },
                  "balances": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1000,
                    "items": {
                      "type": "object",
                      "required": ["date", "balance_type", "amount_cents"],
                      "properties": {
                        "date": {
                          "type": "string",
                          "format": "date",
                          "description": "The date of the balance"
                        },
                        "balance_type": {
                          "type": "string",
                          "enum": [
                            "opening_ledger",
                            "closing_ledger",
                            "current_ledger",
                            "opening_available",
                            "opening_available_next_business_day",
                            "closing_available",
                            "current_available",
                            "previously_closed_book",
                            "other"
                          ],
                          "description": "The type of balance"
                        },
                        "amount_cents": {
                          "type": "integer",
                          "description": "The balance amount in cents (e.g., 1234 = $12.34)"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Balances created or updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "description": "Number of balances processed"
                    },
                    "balances": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BankAccountBalance"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "422": {
            "description": "Validation error (e.g. exceeds 1,000 balances)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/agents": {
      "get": {
        "summary": "List agents",
        "description": "Returns a paginated list of the environment's agents, newest first.",
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from a previous response's `next_cursor`. Omit for the first page."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of agents",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Agent"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Opaque cursor for the next page. Null when no more pages."
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create an agent",
        "description": "Create an agent in the environment. Only `name` and `instructions` are required — `cadence` defaults to `manual`, which runs only when triggered via `POST /agents/{id}/runs`. Scheduled cadences require their time fields: `minute` for hourly, plus `hour` for daily/weekdays, plus `day_of_week` for weekly.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "instructions"],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Unique within the environment"
                  },
                  "description": {
                    "type": "string"
                  },
                  "instructions": {
                    "type": "string",
                    "description": "The standing prompt every run executes"
                  },
                  "model": {
                    "type": "string",
                    "description": "Explicit model override. Omit to follow the platform default."
                  },
                  "cadence": {
                    "type": "string",
                    "enum": ["manual", "hourly", "daily", "weekdays", "weekly"],
                    "default": "manual"
                  },
                  "hour": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 23
                  },
                  "minute": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 59
                  },
                  "day_of_week": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 6,
                    "description": "0 = Sunday"
                  },
                  "timezone": {
                    "type": "string",
                    "default": "UTC",
                    "description": "IANA timezone the schedule is evaluated in"
                  },
                  "active": {
                    "type": "boolean",
                    "default": true
                  },
                  "handles_file_drops": {
                    "type": "boolean",
                    "default": false,
                    "description": "At most one active agent per environment can have this enabled"
                  },
                  "delivery_enabled": {
                    "type": "boolean",
                    "default": false
                  },
                  "delivery_recipients": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "email"
                    }
                  },
                  "delivery_file_pattern": {
                    "type": "string",
                    "default": "*.csv"
                  },
                  "mentions": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["type", "id", "label"],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": ["reconciliation", "data_stream", "user", "agent"]
                        },
                        "id": {
                          "type": "string"
                        },
                        "label": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Agent created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agent"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — agent management requires a standard API key"
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/agents/{id}": {
      "get": {
        "summary": "Retrieve an agent",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          }
        ],
        "responses": {
          "200": {
            "description": "Agent retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agent"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Agent not found"
          }
        }
      },
      "patch": {
        "summary": "Update an agent",
        "description": "Update any subset of the agent's attributes. Accepts the same fields as create; omitted fields are left unchanged.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "instructions": {
                    "type": "string"
                  },
                  "model": {
                    "type": "string"
                  },
                  "cadence": {
                    "type": "string",
                    "enum": ["manual", "hourly", "daily", "weekdays", "weekly"]
                  },
                  "hour": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 23
                  },
                  "minute": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 59
                  },
                  "day_of_week": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 6
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "active": {
                    "type": "boolean"
                  },
                  "handles_file_drops": {
                    "type": "boolean"
                  },
                  "delivery_enabled": {
                    "type": "boolean"
                  },
                  "delivery_recipients": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "email"
                    }
                  },
                  "delivery_file_pattern": {
                    "type": "string"
                  },
                  "mentions": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["type", "id", "label"],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": ["reconciliation", "data_stream", "user", "agent"]
                        },
                        "id": {
                          "type": "string"
                        },
                        "label": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Agent updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agent"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Agent not found"
          },
          "422": {
            "description": "Validation error"
          }
        }
      },
      "delete": {
        "summary": "Delete an agent",
        "description": "Deletes the agent and all of its runs, including their transcripts and files.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          }
        ],
        "responses": {
          "204": {
            "description": "Agent deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Agent not found"
          }
        }
      }
    },
    "/agents/{id}/runs": {
      "get": {
        "summary": "List an agent's runs",
        "description": "Returns a paginated list of the agent's runs, newest first. Transcripts are omitted — fetch a single run to get its transcript.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from a previous response's `next_cursor`. Omit for the first page."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentRun"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Opaque cursor for the next page. Null when no more pages."
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Agent not found"
          }
        }
      },
      "post": {
        "summary": "Run an agent",
        "description": "Triggers a run of the agent immediately, regardless of its cadence or active flag. The run is created `pending` and executed asynchronously — poll `GET /agents/{id}/runs/{run_id}` for status, summary, and output files. Unlike the rest of the agent-management surface, this endpoint also accepts a run's session key, letting one agent kick off another (`triggered_by: agent`); those requests are refused with 422 when they would loop — the target agent already ran in the trigger chain — or chain more than 5 runs deep.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          }
        ],
        "responses": {
          "201": {
            "description": "Run created and queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentRun"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Agent not found"
          },
          "422": {
            "description": "Refused agent-to-agent trigger — the run would create a loop or chain more than 5 runs deep"
          }
        }
      }
    },
    "/agents/{id}/runs/{run_id}": {
      "get": {
        "summary": "Retrieve a run",
        "description": "Returns one run including its full event transcript.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          },
          {
            "in": "path",
            "name": "run_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the run"
          }
        ],
        "responses": {
          "200": {
            "description": "Run retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentRun"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Agent or run not found"
          }
        }
      }
    },
    "/agents/{id}/files": {
      "post": {
        "summary": "Attach an input file",
        "description": "Attaches one file to the agent's input files — the same files the app's agent form manages under \"Files\". Every run lists the agent's input files in its prompt and can download them, so this is how to hand an agent a mapping table, a prior statement, or any reference material. Send `multipart/form-data` with a single `file` part; call once per file. Limits match the app: 50 MB per file, 20 files per agent, any content type (JSON is fine as an input). A file whose name matches an existing input file is added as a second file, not a replacement — delete the old one first to replace it.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file to attach (at most 50 MB)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File attached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentFile"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — agent management requires a standard API key"
          },
          "404": {
            "description": "Agent not found"
          },
          "422": {
            "description": "No `file` part, file over 50 MB, or the agent already has 20 input files"
          }
        }
      }
    },
    "/agents/{id}/files/{file_id}": {
      "get": {
        "summary": "Download an input file",
        "description": "Redirects (302) to a short-lived signed URL for the file's bytes; follow the redirect to download. This is the API-key-authenticated way to fetch an input file. The `url` on the file object reaches the same bytes through a signed link that needs no credentials.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          },
          {
            "in": "path",
            "name": "file_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `id` of the file from the agent's `input_files`"
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the signed download URL"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — agent management requires a standard API key"
          },
          "404": {
            "description": "Agent not found, or the file is not one of this agent's input files"
          }
        }
      },
      "delete": {
        "summary": "Remove an input file",
        "description": "Detaches and deletes one of the agent's input files. Runs already in progress keep the copy they downloaded; later runs no longer see the file.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The unique identifier of the agent"
          },
          {
            "in": "path",
            "name": "file_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `id` of the file from the agent's `input_files`"
          }
        ],
        "responses": {
          "204": {
            "description": "File removed"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — agent management requires a standard API key"
          },
          "404": {
            "description": "Agent not found, or the file is not one of this agent's input files"
          }
        }
      }
    }
  }
}
