{
  "openapi": "3.1.0",
  "info": {
    "title": "BookFold",
    "version": "0.1.0",
    "description": "Upload a PDF or EPUB and get a summary. Pay with MPP.",
    "guidance": "Use POST /v1/uploads to create a private upload target for one PDF or EPUB. Upload the file with the returned client token. Use POST /v1/quotes to get the exact quote for the uploaded book. Use POST /v1/jobs with the quoteId to start the paid job. If POST /v1/jobs returns 402, pay the MPP challenge and retry the same request body. Poll GET /v1/jobs/{jobId} until the status is succeeded or failed. Books must be PDF or EPUB, uploads are capped at 50 MB, and quotes last about 15 minutes. Scanned PDFs are not supported and DRM-protected EPUBs are rejected. The live 402 challenge is the final payment source of truth.",
    "x-guidance": "Use POST /v1/uploads to create a private upload target for one PDF or EPUB. Upload the file with the returned client token. Use POST /v1/quotes to get the exact quote for the uploaded book. Use POST /v1/jobs with the quoteId to start the paid job. If POST /v1/jobs returns 402, pay the MPP challenge and retry the same request body. Poll GET /v1/jobs/{jobId} until the status is succeeded or failed. Books must be PDF or EPUB, uploads are capped at 50 MB, and quotes last about 15 minutes. Scanned PDFs are not supported and DRM-protected EPUBs are rejected. The live 402 challenge is the final payment source of truth."
  },
  "x-service-info": {
    "categories": [
      "ai",
      "media"
    ],
    "docs": {
      "homepage": "https://bookfold.vercel.app/",
      "apiReference": "https://bookfold.vercel.app/openapi.json",
      "llms": "https://bookfold.vercel.app/llms.txt"
    }
  },
  "x-discovery": {
    "ownershipProofs": [
      "mpp-verify=bookfold.vercel.app"
    ]
  },
  "servers": [
    {
      "url": "https://bookfold.vercel.app"
    }
  ],
  "tags": [
    {
      "name": "Uploads"
    },
    {
      "name": "Quotes"
    },
    {
      "name": "Jobs"
    }
  ],
  "paths": {
    "/v1/uploads": {
      "post": {
        "operationId": "createUpload",
        "tags": [
          "Uploads"
        ],
        "summary": "Create a private direct upload target",
        "description": "Creates a short-lived private upload target for one PDF or EPUB before quote creation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload target created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid upload request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Upload rate limit exceeded.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "string"
                },
                "description": "Current limit for the tightest active bucket."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "string"
                },
                "description": "Remaining allowance for the tightest active bucket."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds until the tightest active bucket resets."
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string"
                },
                "description": "Applied rate-limit windows for this route."
              },
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds to wait before retrying."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/quotes": {
      "post": {
        "operationId": "createQuote",
        "tags": [
          "Quotes"
        ],
        "summary": "Create a deterministic quote from an uploaded book",
        "description": "Parses the uploaded book, freezes the summary plan, and returns the exact quote used by the paid job request.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Quote created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid quote request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Upload not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Blob not uploaded yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Quote rate limit exceeded.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "string"
                },
                "description": "Current limit for the tightest active bucket."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "string"
                },
                "description": "Remaining allowance for the tightest active bucket."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds until the tightest active bucket resets."
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string"
                },
                "description": "Applied rate-limit windows for this route."
              },
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds to wait before retrying."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/jobs": {
      "post": {
        "operationId": "createJob",
        "tags": [
          "Jobs"
        ],
        "summary": "Create or resume a paid summary job",
        "description": "Creates the paid summary job from a quote. Call once to receive the 402 challenge, then retry the same request after paying.",
        "x-payment-info": {
          "price": {
            "mode": "dynamic",
            "min": "0.05",
            "max": "5.00",
            "currency": "USD"
          },
          "protocols": [
            {
              "mpp": {
                "method": "tempo",
                "intent": "charge",
                "currency": "0x20C000000000000000000000b9537d11c60E8b50"
              }
            }
          ]
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobCreateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job created or reused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobCreateResponse"
                }
              }
            }
          },
          "202": {
            "description": "Payment succeeded but queue start should be retried.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobCreateResponse"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string"
                },
                "description": "MPP challenge for the paid retry."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Quote not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "Quote expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Job create rate limit exceeded.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "string"
                },
                "description": "Current limit for the tightest active bucket."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "string"
                },
                "description": "Remaining allowance for the tightest active bucket."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds until the tightest active bucket resets."
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string"
                },
                "description": "Applied rate-limit windows for this route."
              },
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds to wait before retrying."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/jobs/{jobId}": {
      "get": {
        "operationId": "getJob",
        "tags": [
          "Jobs"
        ],
        "summary": "Read job status and result payload",
        "description": "Reads the async job state. Keep polling until the status is succeeded or failed.",
        "parameters": [
          {
            "in": "path",
            "name": "jobId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatusResponse"
                }
              }
            }
          },
          "404": {
            "description": "Job not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Job poll rate limit exceeded.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "string"
                },
                "description": "Current limit for the tightest active bucket."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "string"
                },
                "description": "Remaining allowance for the tightest active bucket."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds until the tightest active bucket resets."
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string"
                },
                "description": "Applied rate-limit windows for this route."
              },
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds to wait before retrying."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "ok",
          "service",
          "priceSheetVersion"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "service": {
            "type": "string",
            "example": "bookfold-mpp-server"
          },
          "environment": {
            "type": "string"
          },
          "priceSheetVersion": {
            "type": "string",
            "example": "bookfold-price-v1"
          }
        }
      },
      "UploadRequest": {
        "type": "object",
        "required": [
          "fileName",
          "sizeBytes"
        ],
        "properties": {
          "fileName": {
            "type": "string",
            "example": "book.pdf"
          },
          "contentType": {
            "type": "string",
            "example": "application/pdf"
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": 1,
            "example": 524288
          }
        }
      },
      "UploadResponse": {
        "type": "object",
        "required": [
          "fileId",
          "blobPath",
          "contentType",
          "sizeBytes",
          "upload"
        ],
        "properties": {
          "fileId": {
            "type": "string"
          },
          "blobPath": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer"
          },
          "upload": {
            "type": "object",
            "required": [
              "method",
              "access",
              "clientToken",
              "validUntil"
            ],
            "properties": {
              "method": {
                "type": "string",
                "example": "PUT"
              },
              "access": {
                "type": "string",
                "example": "private"
              },
              "clientToken": {
                "type": "string"
              },
              "validUntil": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "detail"
        ],
        "properties": {
          "uploadId": {
            "type": "string"
          },
          "blobPath": {
            "type": "string"
          },
          "detail": {
            "type": "string",
            "enum": [
              "short",
              "medium",
              "long"
            ]
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "required": [
          "quoteId",
          "uploadId",
          "blobPath",
          "detail",
          "amount",
          "currency",
          "currencyDecimals",
          "expiresAt",
          "fileDigestSha256",
          "plan",
          "versions",
          "price"
        ],
        "properties": {
          "quoteId": {
            "type": "string"
          },
          "uploadId": {
            "type": "string"
          },
          "blobPath": {
            "type": "string"
          },
          "detail": {
            "type": "string",
            "enum": [
              "short",
              "medium",
              "long"
            ]
          },
          "amount": {
            "type": "string",
            "example": "250000"
          },
          "currency": {
            "type": "string",
            "example": "0x20C000000000000000000000b9537d11c60E8b50"
          },
          "currencyDecimals": {
            "type": "integer",
            "example": 6
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "fileDigestSha256": {
            "type": "string"
          },
          "plan": {
            "type": "object",
            "required": [
              "hash",
              "version",
              "strategy",
              "sectionCount",
              "totals",
              "modelIds"
            ],
            "properties": {
              "hash": {
                "type": "string"
              },
              "version": {
                "type": "string"
              },
              "strategy": {
                "type": "string"
              },
              "sectionCount": {
                "type": "integer"
              },
              "totals": {
                "type": "object"
              },
              "modelIds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "versions": {
            "type": "object",
            "required": [
              "parser",
              "tokenizer",
              "prompt",
              "priceSheet"
            ],
            "properties": {
              "parser": {
                "type": "string"
              },
              "tokenizer": {
                "type": "string"
              },
              "prompt": {
                "type": "string"
              },
              "priceSheet": {
                "type": "string"
              }
            }
          },
          "price": {
            "type": "object"
          }
        }
      },
      "JobCreateRequest": {
        "type": "object",
        "required": [
          "quoteId"
        ],
        "properties": {
          "quoteId": {
            "type": "string"
          }
        }
      },
      "JobCreateResponse": {
        "type": "object",
        "required": [
          "jobId",
          "quoteId",
          "uploadId",
          "status",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "quoteId": {
            "type": "string"
          },
          "uploadId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "paid",
              "queued",
              "running",
              "succeeded",
              "failed",
              "refund_review"
            ]
          },
          "workflowRunId": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InboundPayment": {
        "type": "object",
        "required": [
          "id",
          "method",
          "amount",
          "currency",
          "status",
          "receiptReference",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "method": {
            "type": "string",
            "example": "tempo"
          },
          "amount": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "failed"
            ]
          },
          "challengeId": {
            "type": "string"
          },
          "receiptReference": {
            "type": "string"
          },
          "receipt": {
            "type": "object"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OutboundPayment": {
        "type": "object",
        "required": [
          "id",
          "provider",
          "kind",
          "status",
          "spent",
          "cumulative",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "provider": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "failed"
            ]
          },
          "spent": {
            "type": "string"
          },
          "cumulative": {
            "type": "string"
          },
          "channelId": {
            "type": "string"
          },
          "requestCount": {
            "type": "integer"
          },
          "receipt": {
            "type": "object"
          },
          "closeError": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "JobResult": {
        "type": "object",
        "required": [
          "summary",
          "detail",
          "metadata",
          "debug"
        ],
        "properties": {
          "summary": {
            "type": "string"
          },
          "detail": {
            "type": "string",
            "enum": [
              "short",
              "medium",
              "long"
            ]
          },
          "metadata": {
            "type": "object"
          },
          "debug": {
            "type": "object"
          }
        }
      },
      "JobStatusResponse": {
        "type": "object",
        "required": [
          "jobId",
          "quoteId",
          "uploadId",
          "status",
          "createdAt",
          "updatedAt",
          "payment"
        ],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "quoteId": {
            "type": "string"
          },
          "uploadId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "paid",
              "queued",
              "running",
              "succeeded",
              "failed",
              "refund_review"
            ]
          },
          "workflowRunId": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": "string",
            "format": "date-time"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "error": {
            "type": "object",
            "properties": {
              "message": {
                "type": "string"
              }
            }
          },
          "payment": {
            "type": "object",
            "required": [
              "outbound"
            ],
            "properties": {
              "inbound": {
                "$ref": "#/components/schemas/InboundPayment"
              },
              "outbound": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/OutboundPayment"
                }
              }
            }
          },
          "result": {
            "$ref": "#/components/schemas/JobResult"
          }
        }
      }
    }
  }
}