{
  "openapi": "3.1.0",
  "info": {
    "title": "Obol Ledger API",
    "version": "1.0.0",
    "description": "A double-entry ledger. Every entry is balanced, append-only, and enforced by the database as well as the application. Amounts are exchanged as exact decimal strings, never as JSON numbers.",
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    }
  },
  "servers": [
    {
      "url": "/api/v1"
    }
  ],
  "tags": [
    {
      "name": "Accounts"
    },
    {
      "name": "Journal"
    },
    {
      "name": "Reports"
    },
    {
      "name": "Webhooks"
    },
    {
      "name": "Periods"
    },
    {
      "name": "Credentials"
    },
    {
      "name": "Operations"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Required for all writes."
      }
    },
    "schemas": {
      "Money": {
        "type": "object",
        "required": [
          "amount",
          "minorUnits",
          "currency"
        ],
        "properties": {
          "amount": {
            "type": "string",
            "description": "Exact decimal, e.g. \"1234.56\".",
            "examples": [
              "1234.56"
            ]
          },
          "minorUnits": {
            "type": "string",
            "description": "The same value as an integer count of minor units, sent as a string so that values beyond 2^53 survive JSON.",
            "examples": [
              "123456"
            ]
          },
          "currency": {
            "type": "string",
            "examples": [
              "USD"
            ]
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 Problem Details. Extension members carry the specifics of the failure.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "id",
          "name",
          "type",
          "status",
          "overdraftAllowed",
          "balance",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "acct_01JBQZ8Q2N7K3F5M9R1T4V6X8Z"
            ]
          },
          "name": {
            "type": "string"
          },
          "type": {
            "enum": [
              "asset",
              "liability",
              "equity",
              "revenue",
              "expense"
            ]
          },
          "status": {
            "enum": [
              "open",
              "closed"
            ]
          },
          "overdraftAllowed": {
            "type": "boolean"
          },
          "balance": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "The settled balance. Only posted entries count."
          },
          "pendingBalance": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Settled plus in-flight: what the balance becomes if every pending entry settles."
          },
          "availableBalance": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Settled minus in-flight outflows: what can still be spent. This is the balance the overdraft rule consults, so an authorisation reserves funds the moment it is made."
          },
          "version": {
            "type": "integer",
            "description": "Optimistic-concurrency token, incremented on every balance change. Pass it back in `expectedVersions` to apply a write only if the account has not moved."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Transaction": {
        "type": "object",
        "required": [
          "id",
          "description",
          "currency",
          "occurredAt",
          "createdAt",
          "postings"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "txn_01JBQZ8Q2N7K3F5M9R1T4V6X8Z"
            ]
          },
          "description": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "status": {
            "enum": [
              "pending",
              "posted",
              "archived"
            ],
            "description": "pending reserves funds without moving them; posted has settled; archived was cancelled before settling. Posted and archived are immutable."
          },
          "postedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "archivedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "postings": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "accountId",
                "accountName",
                "direction",
                "amount",
                "sequence"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "accountId": {
                  "type": "string"
                },
                "accountName": {
                  "type": "string"
                },
                "direction": {
                  "enum": [
                    "debit",
                    "credit"
                  ]
                },
                "amount": {
                  "$ref": "#/components/schemas/Money"
                },
                "sequence": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "CreateAccount": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "type": {
            "type": "string",
            "enum": [
              "asset",
              "liability",
              "equity",
              "revenue",
              "expense"
            ]
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "EUR",
              "GBP",
              "AUD",
              "NZD",
              "SGD",
              "JPY",
              "VND",
              "KRW",
              "BHD",
              "KWD"
            ]
          },
          "overdraftAllowed": {
            "default": false,
            "type": "boolean"
          },
          "metadata": {
            "default": {},
            "type": "object",
            "propertyNames": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            }
          }
        },
        "required": [
          "name",
          "type",
          "currency"
        ]
      },
      "CreateEntry": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 280
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "EUR",
              "GBP",
              "AUD",
              "NZD",
              "SGD",
              "JPY",
              "VND",
              "KRW",
              "BHD",
              "KWD"
            ]
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "postings": {
            "minItems": 2,
            "maxItems": 64,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "accountId": {
                  "type": "string",
                  "pattern": "^acct_[0-9A-HJKMNP-TV-Z]{26}$"
                },
                "direction": {
                  "type": "string",
                  "enum": [
                    "debit",
                    "credit"
                  ]
                },
                "amount": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 32,
                  "pattern": "^\\d+(\\.\\d+)?$"
                },
                "baseAmount": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 32,
                  "pattern": "^\\d+(\\.\\d+)?$"
                },
                "fxRate": {
                  "type": "string",
                  "pattern": "^\\d+(\\.\\d{1,10})?$"
                }
              },
              "required": [
                "accountId",
                "direction",
                "amount"
              ]
            }
          },
          "fxAdjustment": {
            "default": false,
            "type": "boolean"
          },
          "status": {
            "default": "posted",
            "type": "string",
            "enum": [
              "pending",
              "posted"
            ]
          },
          "expectedVersions": {
            "type": "object",
            "propertyNames": {
              "type": "string",
              "pattern": "^acct_[0-9A-HJKMNP-TV-Z]{26}$"
            },
            "additionalProperties": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            }
          },
          "metadata": {
            "default": {},
            "type": "object",
            "propertyNames": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            }
          }
        },
        "required": [
          "description",
          "currency",
          "postings"
        ]
      },
      "CreateTransfer": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 280
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "EUR",
              "GBP",
              "AUD",
              "NZD",
              "SGD",
              "JPY",
              "VND",
              "KRW",
              "BHD",
              "KWD"
            ]
          },
          "fromAccountId": {
            "type": "string",
            "pattern": "^acct_[0-9A-HJKMNP-TV-Z]{26}$"
          },
          "toAccountId": {
            "type": "string",
            "pattern": "^acct_[0-9A-HJKMNP-TV-Z]{26}$"
          },
          "amount": {
            "type": "string",
            "minLength": 1,
            "maxLength": 32,
            "pattern": "^\\d+(\\.\\d+)?$"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "metadata": {
            "default": {},
            "type": "object",
            "propertyNames": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            }
          }
        },
        "required": [
          "description",
          "currency",
          "fromAccountId",
          "toAccountId",
          "amount"
        ]
      },
      "Pagination": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "limit": {
            "default": 25,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "cursor": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256
          },
          "direction": {
            "default": "forward",
            "type": "string",
            "enum": [
              "forward",
              "backward"
            ]
          },
          "format": {
            "default": "json",
            "type": "string",
            "enum": [
              "json",
              "csv"
            ]
          }
        }
      },
      "CreateEndpoint": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2048
          },
          "description": {
            "type": "string",
            "maxLength": 200
          },
          "eventTypes": {
            "default": [],
            "maxItems": 6,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "entry.posted",
                "entry.pending",
                "entry.settled",
                "entry.archived",
                "entry.reversed",
                "account.opened"
              ]
            }
          }
        },
        "required": [
          "url"
        ]
      },
      "UpdateEndpoint": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "enabled"
        ]
      },
      "CreateApiKey": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          }
        },
        "required": [
          "name"
        ]
      },
      "RecordRate": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "base": {
            "type": "string",
            "enum": [
              "USD",
              "EUR",
              "GBP",
              "AUD",
              "NZD",
              "SGD",
              "JPY",
              "VND",
              "KRW",
              "BHD",
              "KWD"
            ]
          },
          "quote": {
            "type": "string",
            "enum": [
              "USD",
              "EUR",
              "GBP",
              "AUD",
              "NZD",
              "SGD",
              "JPY",
              "VND",
              "KRW",
              "BHD",
              "KWD"
            ]
          },
          "rate": {
            "type": "string",
            "pattern": "^\\d+(\\.\\d{1,10})?$"
          },
          "asOf": {
            "type": "string",
            "format": "date",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
          },
          "source": {
            "default": "manual",
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          }
        },
        "required": [
          "base",
          "quote",
          "rate",
          "asOf"
        ]
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "Liveness and database readiness",
        "description": "Runs a trivial query against the database. A 503 body reports whether DATABASE_URL is configured and the driver error code, so the cause is visible without reading logs.",
        "responses": {
          "200": {
            "description": "Healthy"
          },
          "503": {
            "description": "The database is unreachable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "const": "degraded"
                    },
                    "database": {
                      "type": "object",
                      "properties": {
                        "configured": {
                          "type": "boolean"
                        },
                        "reachable": {
                          "const": false
                        },
                        "code": {
                          "type": "string",
                          "description": "Driver or Postgres error code, e.g. ENOTFOUND or 28P01."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/accounts": {
      "get": {
        "tags": [
          "Accounts"
        ],
        "summary": "List every account with its current balance",
        "responses": {
          "200": {
            "description": "Accounts, ordered by name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Account"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Accounts"
        ],
        "summary": "Open an account",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAccount"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Account"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/accounts/{accountId}": {
      "get": {
        "tags": [
          "Accounts"
        ],
        "summary": "Fetch one account",
        "parameters": [
          {
            "name": "accountId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The account",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Account"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/accounts/{accountId}/statement": {
      "get": {
        "tags": [
          "Accounts"
        ],
        "summary": "Paginated statement with a running balance",
        "parameters": [
          {
            "name": "accountId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque keyset cursor taken from a previous response’s meta.nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "csv streams a download instead of JSON — RFC 4180, CRLF, a UTF-8 BOM so Excel reads it correctly, and cells beginning = + - @ neutralised against spreadsheet formula injection. An export is the whole listing; limit and cursor apply to the JSON view only.",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ],
              "default": "json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Statement lines, newest first"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/entries": {
      "get": {
        "tags": [
          "Journal"
        ],
        "summary": "List journal entries, newest first",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque keyset cursor taken from a previous response’s meta.nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "csv streams a download instead of JSON — RFC 4180, CRLF, a UTF-8 BOM so Excel reads it correctly, and cells beginning = + - @ neutralised against spreadsheet formula injection. An export is the whole listing; limit and cursor apply to the JSON view only.",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ],
              "default": "json"
            }
          },
          {
            "name": "accountId",
            "in": "query",
            "required": false,
            "description": "Only entries with a posting touching this account.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring of the description.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Restrict to one lifecycle state.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "posted",
                "archived"
              ]
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "csv streams a download instead of JSON: one row per posting, RFC 4180, UTF-8 BOM, and cells beginning = + - @ neutralised against spreadsheet formula injection.",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ],
              "default": "json"
            }
          },
          {
            "name": "metadataKey",
            "in": "query",
            "required": false,
            "description": "With metadataValue, an exact match on one metadata pair — e.g. metadataKey=invoice&metadataValue=INV-42. Answered by a GIN index.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "metadataValue",
            "in": "query",
            "required": false,
            "description": "The value metadataKey must equal. Both are required together.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Transaction"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "nextCursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "previousCursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Journal"
        ],
        "summary": "Record a balanced journal entry",
        "description": "Postings must sum to zero. The entry is written in a single database transaction and a deferred constraint verifies the balance at COMMIT.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Replays a previous response instead of posting twice. Reusing a key with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateEntry"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idempotent replay of an earlier request"
          },
          "201": {
            "description": "Recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Transaction"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but cannot be applied to the ledger",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/entries/{entryId}": {
      "get": {
        "tags": [
          "Journal"
        ],
        "summary": "Fetch one entry with its postings",
        "parameters": [
          {
            "name": "entryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The entry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Transaction"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/transfers": {
      "post": {
        "tags": [
          "Journal"
        ],
        "summary": "Move money between two accounts",
        "description": "Sugar for a two-legged journal entry; identical guarantees.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Replays a previous response instead of posting twice. Reusing a key with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTransfer"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idempotent replay of an earlier request"
          },
          "201": {
            "description": "Recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Transaction"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but cannot be applied to the ledger",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/entries/{entryId}/post": {
      "post": {
        "tags": [
          "Journal"
        ],
        "summary": "Settle a pending entry",
        "description": "Moves the entry from pending to posted: its amounts stop being reserved and start counting toward the posted balance. The overdraft rule is re-checked, because funds available at authorisation may be gone by settlement.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "entryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The settled entry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Transaction"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but cannot be applied to the ledger",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/entries/{entryId}/archive": {
      "post": {
        "tags": [
          "Journal"
        ],
        "summary": "Cancel a pending entry before it settles",
        "description": "Releases the reservation; nothing moves. Distinct from a reversal, which cancels money that did move by posting an opposite entry — here there is nothing to mirror.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "entryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The archived entry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Transaction"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/entries/{entryId}/reverse": {
      "post": {
        "tags": [
          "Journal"
        ],
        "summary": "Undo an entry by posting its mirror image",
        "description": "A POST that creates a new entry rather than a DELETE that removes one, because that is what happens: the original stays on the record and a second entry cancels it. An entry can be reversed at most once.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "entryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Replays a previous response instead of posting twice. Reusing a key with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": "string",
                    "description": "Defaults to \"Reversal of <original>\"."
                  },
                  "occurredAt": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The reversing entry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Transaction"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but cannot be applied to the ledger",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/reports/balance-sheet": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Assets, liabilities and equity at a point in time",
        "description": "`balanced` is computed rather than assumed: a balance sheet that does not balance means the ledger is inconsistent.",
        "parameters": [
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "USD"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The balance sheet"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/reports/income-statement": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Revenue and expenses over a period",
        "description": "Revenue and expenses are flows, so a period is required. Defaults to the last 30 days rather than all time, because an income statement with no period attached is meaningless.",
        "parameters": [
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "USD"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The income statement"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/rates": {
      "get": {
        "tags": [
          "Periods"
        ],
        "summary": "Exchange rates on file",
        "description": "Point-in-time facts, never updated. A lookup asks for the most recent rate at or before a date, so re-running last quarter’s reports uses last quarter’s rates.",
        "responses": {
          "200": {
            "description": "Rates, newest first"
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Periods"
        ],
        "summary": "Record a rate",
        "description": "Re-recording the same pair, day and source is a correction rather than a second opinion. A rate is a decimal string with up to ten places — never a JSON number, which would already have lost precision.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordRate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Recorded"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but cannot be applied to the ledger",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/periods/{periodMonth}/revalue": {
      "post": {
        "tags": [
          "Periods"
        ],
        "summary": "Retranslate foreign monetary balances at the closing rate",
        "description": "IAS 21 remeasurement: cash, receivables and payables held in a foreign currency are restated at the month-end rate and the difference goes to profit or loss. Inventory and fixed assets are not — they stay at the rate they were bought at. Cumulative rather than reversing, so running it twice posts nothing the second time. `?preview=true` computes the adjustment without posting it.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "periodMonth",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-(0[1-9]|1[0-2])$"
            }
          },
          {
            "name": "preview",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The retranslation, posted or previewed"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but cannot be applied to the ledger",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/periods": {
      "get": {
        "tags": [
          "Periods"
        ],
        "summary": "Months, and whether they still accept entries",
        "description": "A month with entries but no period row is open; the row is created when it is closed.",
        "responses": {
          "200": {
            "description": "Months, newest first"
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/periods/{periodMonth}/close": {
      "post": {
        "tags": [
          "Periods"
        ],
        "summary": "Close a month",
        "description": "Posts a closing entry that zeroes revenue and expense into retained earnings, then locks the month against any entry dated inside it. The closing entry goes through the journal like any other, so it obeys the same balance rule. Periods close in order, and the current month cannot be closed.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "periodMonth",
            "in": "path",
            "required": true,
            "description": "The month, as YYYY-MM.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-(0[1-9]|1[0-2])$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The closed period"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but cannot be applied to the ledger",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/periods/{periodMonth}/reopen": {
      "post": {
        "tags": [
          "Periods"
        ],
        "summary": "Reopen a closed month",
        "description": "Reverses the closing entry, dated inside the month rather than today, and unlocks it. The original close stays on the record.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "periodMonth",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-(0[1-9]|1[0-2])$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reopened period"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys": {
      "get": {
        "tags": [
          "Credentials"
        ],
        "summary": "List this tenant’s keys",
        "description": "Digests are never returned. Each key carries an identifying prefix and the time it was last used, which is what makes revoking the right one possible.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Keys, newest first"
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Credentials"
        ],
        "summary": "Issue a key",
        "description": "Returns the token exactly once; only its SHA-256 digest is stored. Minting a key requires an existing key, so the first one comes from the seed rather than from an open endpoint.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKey"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Issued, with the token"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys/{keyId}": {
      "delete": {
        "tags": [
          "Credentials"
        ],
        "summary": "Revoke a key",
        "description": "The row is kept and `revoked_at` is set, because a deleted row answers \"who had access, and until when?\" with silence. Revoking twice is a 404, not a silent success.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "keyId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The revoked key"
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/webhook-endpoints": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List registered endpoints",
        "description": "Signing secrets are never included; they are returned once, at creation.",
        "responses": {
          "200": {
            "description": "Endpoints, newest first"
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Register an endpoint",
        "description": "Returns the signing secret exactly once. Store it: it cannot be read back, only rotated. An empty eventTypes subscribes to everything.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateEndpoint"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered, with the signing secret"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/webhook-endpoints/{endpointId}": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Fetch one endpoint",
        "parameters": [
          {
            "name": "endpointId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The endpoint"
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Enable or disable an endpoint",
        "description": "Re-enabling clears the consecutive-failure count, so the circuit breaker does not trip again on the next single failure.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "endpointId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateEndpoint"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated endpoint"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Remove an endpoint and its delivery history",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "endpointId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/webhook-deliveries": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "The delivery log",
        "description": "Every attempt, with its status code, response excerpt and next retry time — so a subscriber can diagnose its own failures without a support thread.",
        "parameters": [
          {
            "name": "endpointId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "delivering",
                "succeeded",
                "failed"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries, newest first"
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/webhook-deliveries/{deliveryId}/replay": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Queue the same event again",
        "description": "Creates a new delivery rather than resetting the old one, so the record of the original failure survives. The payload carries a replayOf link for deduplication.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "deliveryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued"
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/dispatch": {
      "post": {
        "tags": [
          "Operations"
        ],
        "summary": "Drain the delivery queue",
        "description": "Triggered by the scheduler, authenticated with CRON_SECRET. Documented because an operator needs to know it exists, not because clients should call it.",
        "responses": {
          "200": {
            "description": "What the run claimed, sent and abandoned"
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/metrics": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "Prometheus metrics",
        "description": "Text exposition format. obol_ledger_residual_minor is the one worth alerting on: it has exactly one correct value, zero, in every currency.",
        "responses": {
          "200": {
            "description": "Metrics in Prometheus text format"
          }
        }
      }
    },
    "/reports/trial-balance": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Debits, credits and residual per currency",
        "description": "A residual other than zero means the ledger is inconsistent.",
        "responses": {
          "200": {
            "description": "One row per currency"
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  }
}