{
 "paths": {
  "/documents": {
   "post": {
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "A unique key to ensure the request is processed at most once. Must be unique per request \u2014 use a UUID. Keys expire after 24 hours. If a duplicate key is detected, the original response is replayed with `X-Idempotency-Replayed: true`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "X-Filename",
      "in": "header",
      "required": false,
      "description": "Original filename for the uploaded PDF. Used for display purposes only. Defaults to 'document.pdf' if not provided.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "X-Webhook-URL",
      "in": "header",
      "required": false,
      "description": "Webhook URL for processing completion notifications. Overrides the default webhook URL configured on the API key.",
      "schema": {
       "type": "string",
       "format": "uri"
      }
     },
     {
      "name": "X-Password",
      "in": "header",
      "required": false,
      "description": "Decryption password for encrypted PDFs. When provided, the PDF is decrypted and processed in a single request \u2014 no need for the two-step unlock flow. The password is used in memory only and never stored or logged.",
      "schema": {
       "type": "string",
       "maxLength": 1000
      }
     },
     {
      "name": "retry",
      "in": "query",
      "required": false,
      "description": "Set to 'true' to reprocess a previously failed upload of the same file. Without this, re-uploading a failed file returns an already_processing error.",
      "schema": {
       "type": "string",
       "enum": [
        "true"
       ]
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "upload_id": {
          "type": "string",
          "description": "An upload_id from POST /v1/upload-tokens, after PUTting the file to its upload_url. Converts the previously-uploaded file by reference instead of sending PDF bytes in this request."
         },
         "webhook_url": {
          "type": "string",
          "format": "uri",
          "description": "Webhook URL for processing completion notifications. Overrides the default webhook URL configured on the API key."
         }
        },
        "required": [
         "upload_id"
        ]
       }
      }
     },
     "application/pdf": {
      "schema": {
       "type": "string",
       "format": "binary",
       "description": "The raw PDF bank statement bytes. Set Content-Type to application/pdf."
      }
     }
    }
   },
   "operationId": "uploadDocument",
   "summary": "Upload a bank statement PDF",
   "description": "Upload a PDF bank statement for conversion. The file is processed asynchronously. Poll the returned `id` via GET /v1/documents/{id} to check progress. Rate limit: 10 requests per minute. Alternatively, POST a JSON body `{ \"upload_id\": \"...\" }` referencing a token minted via POST /v1/upload-tokens, to convert a previously-PUT file by reference.",
   "tags": [
    "Documents"
   ],
   "responses": {
    "202": {
     "description": "Upload accepted. Processing has started, or the PDF requires a password.",
     "headers": {
      "X-Request-Id": {
       "$ref": "#/components/headers/X-Request-Id"
      },
      "X-API-Version": {
       "$ref": "#/components/headers/X-API-Version"
      },
      "X-Idempotency-Replayed": {
       "$ref": "#/components/headers/X-Idempotency-Replayed"
      },
      "RateLimit-Limit": {
       "$ref": "#/components/headers/RateLimit-Limit"
      },
      "RateLimit-Remaining": {
       "$ref": "#/components/headers/RateLimit-Remaining"
      },
      "RateLimit-Reset": {
       "$ref": "#/components/headers/RateLimit-Reset"
      }
     },
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/StatementUploadResponse"
       },
       "examples": {
        "processing": {
         "summary": "Upload accepted \u2014 processing started",
         "value": {
          "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
          "status": "processing",
          "contentHash": "a1b2c3d4e5f6",
          "createdAt": "2026-02-27T10:30:00.000Z",
          "statements": [],
          "links": {
           "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851"
          }
         }
        },
        "encrypted_pdf": {
         "summary": "Password-protected PDF \u2014 requires unlock",
         "value": {
          "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
          "status": "password_required",
          "contentHash": "a1b2c3d4e5f6",
          "createdAt": "2026-02-27T10:30:00.000Z",
          "statements": [],
          "links": {
           "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851",
           "unlock": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851/unlock"
          }
         }
        }
       }
      }
     }
    },
    "400": {
     "description": "Invalid request (e.g., missing file, wrong content type, invalid file).",
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ErrorResponse"
       },
       "examples": {
        "missing_file": {
         "summary": "No file uploaded",
         "value": {
          "error": {
           "type": "invalid_request_error",
           "code": "missing_file",
           "message": "No file provided. Send a PDF as multipart/form-data with field name \"file\".",
           "request_id": "req_7yNm3kPqW2a4bZ1c"
          }
         }
        },
        "invalid_file_type": {
         "summary": "Unsupported file type",
         "value": {
          "error": {
           "type": "invalid_request_error",
           "code": "invalid_file_type",
           "message": "Invalid file type. Only PDF files are supported.",
           "request_id": "req_2bFh6jLnR0c4dE8g"
          }
         }
        },
        "invalid_pdf": {
         "summary": "Corrupted or unreadable PDF",
         "value": {
          "error": {
           "type": "invalid_request_error",
           "code": "invalid_pdf",
           "message": "This file could not be read as a valid PDF. It may be corrupted.",
           "request_id": "req_6fJl0nRtV4g8hI2k"
          }
         }
        },
        "invalid_parameters": {
         "summary": "Invalid webhook_url or other parameters",
         "value": {
          "error": {
           "type": "invalid_request_error",
           "code": "invalid_parameters",
           "message": "Invalid webhook_url: Webhook URL must use HTTPS.",
           "meta": {
            "fields": [
             {
              "field": "webhook_url",
              "message": "Webhook URL must use HTTPS."
             }
            ]
           },
           "request_id": "req_8hLn2pTvX6i0jK4m"
          }
         }
        }
       }
      },
      "401": {
       "$ref": "#/components/responses/Unauthorized"
      },
      "403": {
       "description": "Quota exceeded (insufficient credits, page limit, or file size).",
       "content": {
        "application/json": {
         "schema": {
          "$ref": "#/components/schemas/ErrorResponse"
         },
         "examples": {
          "insufficient_credits": {
           "summary": "Not enough credits to process file",
           "value": {
            "error": {
             "type": "quota_exceeded",
             "code": "insufficient_credits",
             "message": "Insufficient credits. This file requires 3 credit(s) but you have 1 remaining. Purchase more at https://bankstatemently.com/pricing.",
             "meta": {
              "credits_required": 3,
              "credits_remaining": 1
             },
             "request_id": "req_8zAb2cDe4fGh6iJk"
            }
           }
          },
          "page_limit_exceeded": {
           "summary": "PDF exceeds plan's page limit",
           "value": {
            "error": {
             "type": "quota_exceeded",
             "code": "page_limit_exceeded",
             "message": "Page limit exceeded. This file has 25 pages but your plan allows 10. Upgrade at https://bankstatemently.com/pricing.",
             "meta": {
              "page_count": 25,
              "page_limit": 10
             },
             "request_id": "req_0bDf4hJlN8c2eG6i"
            }
           }
          },
          "file_too_large": {
           "summary": "File exceeds plan's size limit",
           "value": {
            "error": {
             "type": "quota_exceeded",
             "code": "file_too_large",
             "message": "File too large. Your plan allows files up to 10 MB. Upgrade at https://bankstatemently.com/pricing.",
             "meta": {
              "max_size_mb": 10
             },
             "request_id": "req_2dFh6jLnR0e4gI8k"
            }
           }
          }
         }
        }
       }
      },
      "409": {
       "description": "Document already exists or is being processed.",
       "content": {
        "application/json": {
         "schema": {
          "$ref": "#/components/schemas/ErrorResponse"
         },
         "example": {
          "error": {
           "type": "invalid_request_error",
           "code": "already_processing",
           "message": "This document is already being processed. Poll GET /v1/documents/{id} for status updates.",
           "request_id": "req_4fHj8lNpT2g6iK0m"
          }
         }
        }
       }
      },
      "422": {
       "description": "Wrong password. Only returned when X-Password header is provided for an encrypted PDF.",
       "content": {
        "application/json": {
         "schema": {
          "$ref": "#/components/schemas/ErrorResponse"
         },
         "example": {
          "error": {
           "type": "invalid_request_error",
           "code": "wrong_password",
           "message": "The password provided is incorrect. Please try again.",
           "request_id": "req_3eGi7kMoS1f5hJ9l"
          }
         }
        }
       }
      },
      "429": {
       "$ref": "#/components/responses/RateLimited"
      },
      "500": {
       "$ref": "#/components/responses/InternalError"
      },
      "503": {
       "$ref": "#/components/responses/ServiceUnavailable"
      }
     }
    },
    "get": {
     "parameters": [
      {
       "name": "limit",
       "in": "query",
       "required": false,
       "description": "Maximum number of documents to return.",
       "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 100,
        "default": 20
       }
      },
      {
       "name": "offset",
       "in": "query",
       "required": false,
       "description": "Number of documents to skip for pagination.",
       "schema": {
        "type": "integer",
        "minimum": 0,
        "default": 0
       }
      },
      {
       "name": "status",
       "in": "query",
       "required": false,
       "description": "Filter documents by processing status (e.g., processing, completed, error).",
       "schema": {
        "type": "string",
        "enum": [
         "processing",
         "completed",
         "error"
        ]
       }
      }
     ],
     "operationId": "listDocuments",
     "summary": "List processed documents",
     "description": "Retrieve a paginated list of processed documents with optional status filtering.",
     "tags": [
      "Documents"
     ],
     "responses": {
      "200": {
       "description": "A list of documents.",
       "content": {
        "application/json": {
         "schema": {
          "type": "object",
          "properties": {
           "documents": {
            "type": "array",
            "items": {
             "$ref": "#/components/schemas/Document"
            }
           }
          },
          "example": {
           "documents": [
            {
             "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
             "status": "completed",
             "contentHash": "a1b2c3d4e5f6",
             "createdAt": "2026-02-27T10:30:00.000Z",
             "statements": [
              {
               "type": "transaction",
               "date": "2023-01-01",
               "description": "Coffee Shop",
               "amount": -3.5
              }
             ],
             "links": {
              "self": "https://api.bankstatemently.com/v1/documents/d290f1ee-6c54-4b01-90e6-d701748f0851"
             }
            }
           ]
          }
         }
        }
       }
      }
     }
    }
   }
  }
 },
 "components": {
  "headers": {
   "X-Request-Id": {
    "description": "Unique identifier for this request, useful for debugging and support.",
    "schema": {
     "type": "string",
     "pattern": "^req_[a-f0-9]{16}$"
    },
    "example": "req_7yNm3kPqW2a4bZ1c"
   },
   "X-API-Version": {
    "description": "The API version used to process this request. Always `1` for v1 endpoints.",
    "schema": {
     "type": "string",
     "enum": [
      "1"
     ]
    },
    "example": "1"
   },
   "X-Idempotency-Replayed": {
    "description": "Present and set to `true` when the response was served from the idempotency cache rather than reprocessed.",
    "schema": {
     "type": "string",
     "enum": [
      "true"
     ]
    }
   },
   "RateLimit-Limit": {
    "description": "Maximum number of requests allowed in the current rate limit window.",
    "schema": {
     "type": "integer"
    },
    "example": 120
   },
   "RateLimit-Remaining": {
    "description": "Number of requests remaining in the current rate limit window.",
    "schema": {
     "type": "integer"
    },
    "example": 117
   },
   "RateLimit-Reset": {
    "description": "Unix timestamp (seconds) when the current rate limit window resets.",
    "schema": {
     "type": "integer"
    },
    "example": 1740653520
   },
   "Retry-After": {
    "description": "Number of seconds to wait before retrying a rate-limited request.",
    "schema": {
     "type": "integer"
    },
    "example": 12
   }
  },
  "schemas": {
   "StatementUploadResponse": {
    "type": "object",
    "description": "Response returned when a PDF is successfully accepted for processing.",
    "required": [
     "id",
     "status",
     "contentHash",
     "createdAt",
     "statements",
     "links"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "Unique document identifier. Use this to poll status and request exports."
     },
     "status": {
      "$ref": "#/components/schemas/DocumentStatus"
     },
     "contentHash": {
      "type": "string",
      "description": "Content-addressable hash of the uploaded file."
     },
     "createdAt": {
      "type": "string",
      "format": "date-time",
      "description": "Timestamp when the document was created."
     },
     "statements": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/StatementSummary"
      },
      "description": "Always an empty array on upload; populated once processing completes."
     },
     "links": {
      "type": "object",
      "required": [
       "self"
      ],
      "properties": {
       "self": {
        "type": "string",
        "format": "uri",
        "description": "URL to poll for processing status."
       },
       "unlock": {
        "type": "string",
        "format": "uri",
        "description": "URL to submit a password for unlocking. Only present when status is `password_required`."
       }
      }
     }
    }
   },
   "ErrorResponse": {
    "type": "object",
    "required": [
     "error"
    ],
    "properties": {
     "error": {
      "type": "object",
      "required": [
       "type",
       "code",
       "message",
       "request_id"
      ],
      "properties": {
       "type": {
        "$ref": "#/components/schemas/ApiErrorType"
       },
       "code": {
        "$ref": "#/components/schemas/ApiErrorCode"
       },
       "message": {
        "type": "string",
        "description": "A human-readable description of the error with actionable context. Do not parse this \u2014 use `code` for programmatic handling."
       },
       "meta": {
        "type": "object",
        "description": "Optional structured data for programmatic use. Shape varies by `code`. Only present when actionable data exists.",
        "additionalProperties": true
       },
       "request_id": {
        "type": "string",
        "description": "The unique request identifier. Include this when contacting support.",
        "pattern": "^req_[a-f0-9]{16}$"
       }
      }
     }
    }
   },
   "Document": {
    "type": "object",
    "description": "Referenced by this document but never defined in it. API Evangelist added this empty placeholder so the document resolves; the shape is unknown and is NOT a claim about the API.",
    "x-ae-placeholder": true
   },
   "DocumentStatus": {
    "type": "string",
    "enum": [
     "processing",
     "completed",
     "failed",
     "password_required"
    ],
    "description": "The processing status of a document."
   },
   "StatementSummary": {
    "type": "object",
    "description": "Summary of a single bank statement found within the uploaded document.",
    "required": [
     "index"
    ],
    "properties": {
     "index": {
      "type": "integer",
      "minimum": 0,
      "description": "Zero-based index of this statement within the document."
     },
     "statementPeriod": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/StatementPeriod"
       },
       {
        "type": "null"
       }
      ],
      "description": "The date range of the statement, or null if not detected."
     },
     "transactionCount": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 0,
      "description": "Number of transactions in this statement, or null if not yet counted."
     }
    }
   },
   "ApiErrorType": {
    "type": "string",
    "enum": [
     "authentication_error",
     "invalid_request_error",
     "quota_exceeded",
     "rate_limit_error",
     "api_error"
    ],
    "description": "Broad error category (5 values, frozen). Determines the developer's retry/handling strategy."
   },
   "ApiErrorCode": {
    "type": "string",
    "enum": [
     "invalid_api_key",
     "key_revoked",
     "key_expired",
     "invalid_export_token",
     "missing_file",
     "invalid_file_type",
     "encrypted_pdf",
     "invalid_pdf",
     "invalid_parameters",
     "invalid_export_data_mode",
     "endpoint_not_found",
     "document_not_found",
     "document_not_ready",
     "already_processing",
     "export_failed",
     "insufficient_credits",
     "page_limit_exceeded",
     "file_too_large",
     "rate_limit_exceeded",
     "internal_error",
     "service_unavailable"
    ],
    "description": "Specific error code (append-only, never rename or repurpose). Use this for granular programmatic handling."
   },
   "StatementPeriod": {
    "type": "object",
    "description": "The date range covered by a single statement within the document.",
    "required": [
     "from",
     "to"
    ],
    "properties": {
     "from": {
      "type": "string",
      "format": "date",
      "description": "Start date of the statement period (ISO 8601)."
     },
     "to": {
      "type": "string",
      "format": "date",
      "description": "End date of the statement period (ISO 8601)."
     }
    }
   }
  },
  "responses": {
   "Unauthorized": {
    "description": "Authentication failed, API key revoked, or API key expired.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/ErrorResponse"
      },
      "examples": {
       "missing_key": {
        "summary": "No API key provided",
        "value": {
         "error": {
          "type": "authentication_error",
          "code": "invalid_api_key",
          "message": "Invalid API key provided.",
          "request_id": "req_3cGi7jKlN1b5eF2g"
         }
        }
       },
       "revoked_key": {
        "summary": "API key has been revoked",
        "value": {
         "error": {
          "type": "authentication_error",
          "code": "key_revoked",
          "message": "This API key has been revoked. Generate a new key at the Developer Portal.",
          "request_id": "req_8wTs2uXvA4f9hJ6k"
         }
        }
       },
       "expired_key": {
        "summary": "API key has expired",
        "value": {
         "error": {
          "type": "authentication_error",
          "code": "key_expired",
          "message": "This API key has expired. Generate a new key at the Developer Portal.",
          "request_id": "req_1aEh5iLmP0c4dC8f"
         }
        }
       },
       "invalid_export_token": {
        "summary": "Export download token is invalid, expired, or doesn't match this document (export endpoint only)",
        "value": {
         "error": {
          "type": "authentication_error",
          "code": "invalid_export_token",
          "message": "This download link has expired or is invalid. Ask your AI assistant for a fresh link, or sign in with an API key.",
          "request_id": "req_6nQr0sUwB3e8iK5j"
         }
        }
       }
      }
     }
    }
   },
   "RateLimited": {
    "description": "Too many requests. Wait the number of seconds indicated by the `Retry-After` header.",
    "headers": {
     "Retry-After": {
      "$ref": "#/components/headers/Retry-After"
     },
     "X-Request-Id": {
      "$ref": "#/components/headers/X-Request-Id"
     },
     "X-API-Version": {
      "$ref": "#/components/headers/X-API-Version"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/ErrorResponse"
      },
      "example": {
       "error": {
        "type": "rate_limit_error",
        "code": "rate_limit_exceeded",
        "message": "Rate limit exceeded. Retry after 12 seconds.",
        "meta": {
         "retry_after": 12
        },
        "request_id": "req_6dFj8kMnR2a3bE5g"
       }
      }
     }
    }
   },
   "InternalError": {
    "description": "An unexpected server error occurred.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/ErrorResponse"
      },
      "example": {
       "error": {
        "type": "api_error",
        "code": "internal_error",
        "message": "An unexpected error occurred. If this persists, contact support with the request_id.",
        "request_id": "req_0zAg4hIlO9b2cD7e"
       }
      }
     }
    }
   },
   "ServiceUnavailable": {
    "description": "The service is temporarily unavailable.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/ErrorResponse"
      },
      "example": {
       "error": {
        "type": "api_error",
        "code": "service_unavailable",
        "message": "Document processing is temporarily unavailable. Please try again later.",
        "request_id": "req_1aEh5iLmP0c4dC8f"
       }
      }
     }
    }
   }
  }
 }
}