{
  "openapi": "3.1.0",
  "info": {
    "title": "ProofLedger API",
    "version": "1.0.0",
    "summary": "Blockchain-anchored timestamp proofs for evidence workflows.",
    "description": "The ProofLedger API lets BUSINESS-tier customers submit SHA-256 hashes for dual-chain anchoring (Polygon + Bitcoin) and retrieve proof status programmatically. A public, no-auth verify endpoint is also available for any party — opposing counsel, auditors, courts — to independently confirm a proof.",
    "contact": {
      "name": "ProofLedger Support",
      "email": "support@proofledger.io",
      "url": "https://proofledger.io/support.html"
    },
    "termsOfService": "https://proofledger.io/terms.html",
    "license": {
      "name": "Proprietary",
      "url": "https://proofledger.io/terms.html"
    }
  },
  "servers": [
    {
      "url": "https://proofledger.io",
      "description": "Production"
    },
    {
      "url": "https://staging.proofledger.io",
      "description": "Staging"
    }
  ],
  "tags": [
    {
      "name": "Proofs",
      "description": "Submit and retrieve blockchain-anchored timestamp proofs. Requires a BUSINESS-tier API key."
    },
    {
      "name": "Verification",
      "description": "Public, no-auth verification of any proof by SHA-256 hash."
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/api/v1/proof": {
      "post": {
        "tags": ["Proofs"],
        "summary": "Submit a SHA-256 hash for anchoring",
        "description": "Submits a 64-character SHA-256 hex digest to be anchored on Polygon (instant) and optionally Bitcoin (daily batch). The file content itself never leaves the caller — only the hash is sent. Returns immediately with a proof ID; blockchain confirmation happens asynchronously.",
        "operationId": "submitProof",
        "security": [{ "ApiKeyAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SubmitProofRequest" },
              "examples": {
                "basic": {
                  "summary": "Polygon-only anchor",
                  "value": {
                    "sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
                    "filename": "site-inspection-2026-04-21.pdf"
                  }
                },
                "dualChain": {
                  "summary": "Polygon + Bitcoin anchor",
                  "value": {
                    "sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
                    "filename": "policy-holder-photos.zip",
                    "bitcoin_requested": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Proof accepted and queued for anchoring.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SubmittedProof" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": {
            "description": "Monthly proof limit reached (BUSINESS tier: 5000/month).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/proof/{id}": {
      "get": {
        "tags": ["Proofs"],
        "summary": "Retrieve a proof by ID",
        "description": "Returns full status of a proof, including chain transaction hash, anchoring status, and public explorer URLs. Callers can only retrieve proofs they own.",
        "operationId": "getProof",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "integer", "minimum": 1 },
            "description": "The proof ID returned by POST /api/v1/proof."
          }
        ],
        "responses": {
          "200": {
            "description": "Proof retrieved.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Proof" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/verify": {
      "get": {
        "tags": ["Verification"],
        "summary": "Verify a proof by SHA-256 hash (public)",
        "description": "Public, no-auth endpoint. Returns whether a proof exists for the given hash, along with blockchain transaction details and links to public explorers. Designed for use by opposing counsel, auditors, expert witnesses, and any third party that needs to independently confirm a ProofLedger proof. Rate-limited to 120 requests per hour per IP.",
        "operationId": "verifyProof",
        "security": [],
        "parameters": [
          {
            "name": "hash",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{64}$"
            },
            "description": "The 64-character SHA-256 hex digest of the file being verified. Lowercase."
          }
        ],
        "responses": {
          "200": {
            "description": "Verification result (whether or not a matching proof was found).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VerifyResult" },
                "examples": {
                  "found": {
                    "summary": "Proof exists",
                    "value": {
                      "found": true,
                      "proof": {
                        "id": 42,
                        "sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
                        "filename": "site-inspection.pdf",
                        "created_at": "2026-04-15T14:22:03.000Z",
                        "status": "approved",
                        "polygon_status": "anchored",
                        "bitcoin_status": "anchored",
                        "chain_tx": "0xabc123...",
                        "bitcoin_txid": "def456...",
                        "user_email": "a***@example.com"
                      },
                      "certificate_url": "/cert/42",
                      "verification_url": "https://proofledger.io/verify.html?hash=a1b2c3...",
                      "explorer_url": "https://polygonscan.com/tx/0xabc123...",
                      "bitcoin_explorer_url": "https://blockstream.info/tx/def456..."
                    }
                  },
                  "notFound": {
                    "summary": "No matching proof",
                    "value": {
                      "found": false,
                      "sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": {
            "description": "Too many verify requests from this IP. Try again later.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ProofLedger API key (sk_...)",
        "description": "Send your API key as a Bearer token. Keys are issued per-user from Account > API Keys on the BUSINESS plan. Format: `Authorization: Bearer sk_<8-hex>_<remainder>`. Keys are shown once at creation — store them securely."
      }
    },
    "schemas": {
      "SubmitProofRequest": {
        "type": "object",
        "required": ["sha256"],
        "properties": {
          "sha256": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{64}$",
            "description": "64-character SHA-256 hex digest of the file. Compute locally — file content is never sent."
          },
          "filename": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional label for the proof. Helps identify the proof in your dashboard. Defaults to 'api-submission'."
          },
          "bitcoin_requested": {
            "type": "boolean",
            "default": false,
            "description": "If true, queue the proof for Bitcoin anchoring (Merkle-batched daily). BUSINESS tier includes 100 Bitcoin anchors/month; beyond that the proof is marked REQUIRED and can be paid for individually."
          }
        }
      },
      "SubmittedProof": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "example": 42 },
          "sha256": { "type": "string", "example": "a1b2c3d4..." },
          "filename": { "type": "string", "example": "site-inspection.pdf" },
          "status": { "type": "string", "enum": ["approved", "pending", "pending_review", "denied", "contested"] },
          "created_at": { "type": "string", "format": "date-time" },
          "bitcoin_requested": { "type": "boolean" },
          "btc_payment_status": { "type": "string", "enum": ["NONE", "INCLUDED", "REQUIRED", "PAID"] },
          "duplicate_of": {
            "type": ["object", "null"],
            "description": "Present if a proof with this hash was previously submitted (by any user). ProofLedger still records your submission as a separate proof, but surfaces the earliest known record.",
            "properties": {
              "id": { "type": "integer" },
              "created_at": { "type": "string", "format": "date-time" }
            }
          },
          "certificate_url": { "type": "string", "example": "/cert/42" },
          "verification_url": { "type": "string", "example": "/verify.html?hash=a1b2c3..." }
        }
      },
      "Proof": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "sha256": { "type": "string" },
          "filename": { "type": "string" },
          "size": { "type": "integer" },
          "mime": { "type": "string" },
          "status": { "type": "string", "enum": ["approved", "pending", "pending_review", "denied", "contested"] },
          "review_status": { "type": "string" },
          "polygon_status": { "type": "string", "enum": ["anchored", "pending", "failed", "not_requested"] },
          "bitcoin_status": { "type": "string", "enum": ["anchored", "pending", "awaiting_payment", "failed", "not_requested"] },
          "evidence_readiness": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" },
          "chain_tx": { "type": ["string", "null"], "description": "Polygon transaction hash once anchored." },
          "chain_name": { "type": ["string", "null"], "enum": ["polygon-mainnet", "polygon-amoy", null] },
          "bitcoin_txid": { "type": ["string", "null"] },
          "bitcoin_anchor_status": { "type": ["string", "null"], "enum": ["NOT_REQUESTED", "AWAITING_PAYMENT", "PENDING", "ANCHORED", "FAILED", null] },
          "btc_merkle_root": { "type": ["string", "null"], "description": "Merkle root of the batch this proof was included in, once Bitcoin-anchored." },
          "btc_payment_status": { "type": "string", "enum": ["NONE", "INCLUDED", "REQUIRED", "PAID"] },
          "public_cert_id": { "type": ["string", "null"] },
          "certificate_url": { "type": "string" },
          "explorer_url": { "type": ["string", "null"], "description": "Polygonscan URL once anchored." },
          "bitcoin_explorer_url": { "type": ["string", "null"], "description": "Blockstream URL once Bitcoin-anchored." }
        }
      },
      "VerifyResult": {
        "type": "object",
        "required": ["found"],
        "properties": {
          "found": { "type": "boolean" },
          "sha256": {
            "type": "string",
            "description": "Echoed only when found = false."
          },
          "proof": {
            "type": "object",
            "description": "Present only when found = true.",
            "properties": {
              "id": { "type": "integer" },
              "sha256": { "type": "string" },
              "filename": { "type": "string" },
              "size": { "type": "integer" },
              "created_at": { "type": "string", "format": "date-time" },
              "status": { "type": "string" },
              "review_status": { "type": "string" },
              "polygon_status": { "type": "string" },
              "bitcoin_status": { "type": "string" },
              "evidence_readiness": { "type": "string" },
              "chain_tx": { "type": ["string", "null"] },
              "bitcoin_txid": { "type": ["string", "null"] },
              "bitcoin_anchor_status": { "type": ["string", "null"] },
              "public_cert_id": { "type": ["string", "null"] },
              "user_email": {
                "type": ["string", "null"],
                "description": "Owner's email, masked (e.g. 'a***@example.com'). Exposed so verifiers can corroborate ownership in an evidence context without leaking the full address."
              }
            }
          },
          "certificate_url": { "type": "string" },
          "verification_url": { "type": "string" },
          "explorer_url": { "type": ["string", "null"] },
          "bitcoin_explorer_url": { "type": ["string", "null"] }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message. Do not parse programmatically — use the HTTP status code."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body or parameters were invalid (e.g. hash is not 64-character hex).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Unauthorized": {
        "description": "API key was missing, malformed, or does not match a known key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Forbidden": {
        "description": "API key belongs to an account that is not on the BUSINESS plan, or the account is disabled.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "NotFound": {
        "description": "The requested resource was not found, or the authenticated account does not own it.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "ServerError": {
        "description": "Something went wrong on our side. Retry with exponential backoff.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      }
    }
  }
}
