{
  "openapi": "3.1.1",
  "info": {
    "title": "Prepared API",
    "description": "The Prepared API: send us a property contract and choose a product; we tell you as each part of it happens.\n\nSign in at https://api.prepared.com.au with GitHub to create an organisation and an integration. A new\nintegration starts in the sandbox (`https://api-sandbox.prepared.com.au`).\n[Getting started](https://api.prepared.com.au/getting-started) walks you from a key to your first published\ncontract, with runnable code. An AI coding agent should read https://api.prepared.com.au/llms.txt first.\n\n## The whole flow\n\n1. `POST /v1/token` with `Authorization: Bearer <API key>` answers a token that lasts 15 minutes.\n2. `POST /v1/contract-reviews` with that token, as `multipart/form-data`: a `file` part (the PDF) and a\n   `request` part (JSON). The `Idempotency-Key` and `Content-Digest` headers are both required.\n3. `GET /v1/contract-reviews/{id}` every 30 seconds, or wait for the webhook. When a publication's `state` is\n   `published`, its `url` is the link: usually within 20 minutes. That is longer than a token lasts, so\n   exchange the key again when a call answers 401.\n\n## Products\n\n| Product | What you get | Status |\n|---|---|---|\n| `contract_viewer` | The contract on a page anyone with the link can open. No review. | Available |\n| `legal_information` | A review anyone with the link can read, beside the contract. Send `\"review\": \"lawyer\"`: a lawyer checks the review and publishes it. | Available |\n| `legal_advice` | A lawyer's review written for one person, published as a snapshot. Send `recipient` (`first_name`, `last_name`, `email`), or name them first in `clients`. | Available |\n\n## Client onboarding\n\nYour customers can become Prepared clients on your own site, before any contract exists.\n\n1. Your server calls `POST /v1/clients/onboardings/sessions` with your own `reference` for the sign-up. The\n   answer carries a `session_token`.\n2. Your page loads `/onboarding.js` from the same base URL and calls\n   `Prepared.onboarding.mount({ container, sessionToken, onSubmitted })`. Prepared's sign-up form appears in\n   `container`. Your page cannot read what the customer types.\n3. When the form is submitted we send your webhook `clients.submitted`, once for each buyer on it, with their\n   `client_id`. `clients.accepted` follows for the same `client_id` once Prepared has accepted the sign-up.\n4. Send those ids as `clients` in the `request` part of `POST /v1/contract-reviews` to say who a contract is\n   for.\n\n[Client onboarding](https://api.prepared.com.au/getting-started/client-onboarding) has a complete page and\nserver to copy.\n\n## Authentication\n\nCreate an API key on your integration's API keys page in the portal (`prep_test_…` for the sandbox,\n`prep_live_…` for production), or have a coding agent create it through the MCP server (below). Exchange it at\n`POST /v1/token` on that environment's base URL for a token that lasts 15 minutes. Send the token as\n`Authorization: Bearer <token>`.\n\n## The MCP server\n\n`https://api.prepared.com.au/mcp` is an MCP server (streamable HTTP) for coding agents. You add it to your\nagent and sign in with GitHub in the browser; nothing is pasted. For Claude Code:\n`claude mcp add --transport http prepared https://api.prepared.com.au/mcp`. Connected, the agent does what you\ncan do in the portal, in both environments: create an integration, create and revoke API keys, register a\nwebhook endpoint and send it a test event, read the webhook deliveries and the request history, and redeliver.\nIt does not send contracts: that is this API, called by your code with an API key.\n\n## Errors\n\nEvery error is `{ \"error\": { \"code\": \"…\", \"message\": \"…\" } }`: branch on the code, show the message. The\nmessage says what to send instead.\n\n## Limits\n\nA PDF up to 100 MB. A list returns 20 contract reviews or clients unless `limit` says otherwise, 100 at most.\nA contract names up to 10 clients.\n\nThe sandbox has a budget: it processes a set number of PDFs for each organisation, in all. A new organisation\nstarts with 3, and every contract and every new version counts, across all its integrations. After that an\nupload answers 403 `budget_used`; email integrations@prepared.com.au to have it raised. The\norganisation's overview in the portal shows how much is used. Production has no budget. A person can create\none organisation.\n\n## Help\n\nStuck, or need more than the sandbox gives you? Email integrations@prepared.com.au, with the\n`Prepared-Request-Id` of the call you are asking about.\n\n## Request IDs\n\nEvery response carries a `Prepared-Request-Id` header (`rqs_…`). Log it. Your integration's Request history in\nthe portal lists each call by that ID for 30 days: when, the method and path, the status and how long it took.\nA call made with a key or token we didn't accept isn't listed.\n\n## Webhooks\n\nWe tell your endpoint when something changes. Register an endpoint (a public https address, and the events it\nis sent) on your integration's Webhooks page in the portal, or through the MCP server. Every message is a JSON\n`POST` with the same envelope — `type`, `timestamp`, `test`, `data` — signed the\n[Standard Webhooks](https://www.standardwebhooks.com) way:\n\n| Header | Value |\n|---|---|\n| `webhook-id` | The message ID. The same on every retry of one message: drop a repeat you already handled. |\n| `webhook-timestamp` | Seconds since the Unix epoch, when we signed it. Reject messages more than 5 minutes old. |\n| `webhook-signature` | `v1,` then the base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}`. |\n\nTo verify: take your endpoint's signing secret (`whsec_…`, shown once when you registered the endpoint),\nbase64-decode the part after `whsec_` to get the key, compute HMAC-SHA256 over the exact bytes of\n`{webhook-id}.{webhook-timestamp}.{raw request body}`, base64-encode it, and compare it to the value after\n`v1,` using a constant-time comparison. Any Standard Webhooks library does all of this for you.\n\nAnswer with any 2xx within 10 seconds. Anything else — or no answer — is retried after 1 minute, 5 minutes,\n30 minutes, 2, 6, 12 and 24 hours (8 attempts), then given up; you can redeliver from the portal or through\nthe MCP server.\n\n**Send test event** in the portal, or the MCP server's `prepared_send_test_event`, sends a `test` message:\nthe example `contract-reviews.publication.published` data below, with `\"test\": true`. It refers to no real\ncontract.",
    "version": "v1"
  },
  "servers": [
    {
      "url": "https://api.prepared.com.au",
      "description": "Production"
    },
    {
      "url": "https://api-sandbox.prepared.com.au",
      "description": "Sandbox"
    }
  ],
  "paths": {
    "/v1/clients/onboardings/sessions": {
      "post": {
        "tags": [
          "Clients"
        ],
        "summary": "Open a sign-up and get the token your page shows its form with.",
        "description": "Call this from your server, then give `session_token` to the page your customer is on. The page loads\n`/onboarding.js` from the same base URL and calls\n`Prepared.onboarding.mount({ container, sessionToken, onSubmitted })`, which puts Prepared's sign-up form in\n`container`. Your page cannot read what is typed; `onSubmitted` is called with nothing when the form is sent.\n\nWhen the form is submitted we send your webhook one `clients.submitted` event for each buyer on it, carrying\ntheir `client_id`. You can receive it more than once for the same `client_id`: treat each one as that\nclient's latest details. `clients.accepted` follows for the same `client_id` once Prepared has accepted the\nsign-up, which can be minutes or days later.\n\n`reference` is your own ID for the sign-up. Sending one you have used before answers the same onboarding with\nanother token for it, so this call is safe to repeat and needs no `Idempotency-Key`. The token does not\nexpire, and the form may be submitted more than once: a buyer with the same email keeps their `client_id`.\n\n400 `reference_required` or `reference_invalid`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "reference"
                ],
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string",
                    "description": "Your own ID for this sign-up, up to 200 printable characters. Unique within the integration and environment.\nSending one you have used before returns that onboarding again."
                  }
                },
                "description": "The JSON body of `POST /v1/clients/onboardings/sessions`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingSessionResource"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [ ]
          }
        ]
      }
    },
    "/v1/clients/onboardings/{id}": {
      "get": {
        "tags": [
          "Clients"
        ],
        "summary": "Read an onboarding.",
        "description": "Its state, and the clients its form has named so far, the primary one first.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingResource"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [ ]
          }
        ]
      }
    },
    "/v1/clients/{id}": {
      "get": {
        "tags": [
          "Clients"
        ],
        "summary": "Read a client.",
        "description": "Who a `client_id` is: their name, email and mobile, and the onboarding they signed up through.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientResource"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [ ]
          }
        ]
      }
    },
    "/v1/clients": {
      "get": {
        "tags": [
          "Clients"
        ],
        "summary": "List clients.",
        "description": "Newest first, `limit` at a time (default 20, up to 100). For the next page pass `starting_after` with the last\nID. `onboarding_id` or `reference` (your own ID for the onboarding) finds the clients of one sign-up.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "onboarding_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reference",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientList"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [ ]
          }
        ]
      }
    },
    "/v1/token": {
      "post": {
        "tags": [
          "Token"
        ],
        "summary": "Exchange an API key for a token.",
        "description": "Send the key as `Authorization: Bearer prep_test_…` (sandbox) or `prep_live_…` (production), to that\nenvironment's base URL. The token lasts 15 minutes; exchange the key again for a new one. Keep the key on your\nserver.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResource"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "api_key": [ ]
          }
        ]
      }
    },
    "/v1/contract-reviews": {
      "post": {
        "tags": [
          "ContractReviews"
        ],
        "summary": "Send a contract.",
        "description": "`multipart/form-data` with two parts: `file`, the contract PDF (up to 100 MB), and `request`, JSON:\n`{ \"external_reference\": \"MC-TEST-001\", \"product\": \"contract_viewer\" }`. Both the `Idempotency-Key` header (a\nUUID) and the `Content-Digest` header (the SHA-256 of the whole body, RFC 9530) are required.\n\nFor a review beside the contract, send `\"product\": \"legal_information\", \"review\": \"lawyer\"`: a lawyer\nchecks the review and publishes it, which takes hours or days. `analysis` and `lawyer_review` on the\ncontract review say how far it has got, and `publications` has a second entry for the review.\n\nFor a lawyer's review written for one person, send `\"product\": \"legal_advice\"` and say who it is for:\n`\"recipient\": { \"first_name\": \"…\", \"last_name\": \"…\", \"email\": \"…\" }`, or leave `recipient` out and the first\nclient in `clients` is the recipient. It is published as a snapshot, the `review_snapshot` publication.\n\nTo say who the contract is for, add `\"clients\": [\"cli_…\", \"cli_…\"]` to `request`: up to 10 client IDs from\nclient onboarding. A client can be named as soon as it is submitted.\n\nAnswers 202 at once; the contract is read in the background, usually within 20 minutes. Hear when it is\nlive from your webhook (`contract-reviews.publication.published`), or poll `GET /v1/contract-reviews/{id}`\nevery 30 seconds until a publication's `state` is `published`: its `url` is the link. A token lasts 15\nminutes: exchange the key again when a call answers 401.\n\n400 `product_required` or `product_invalid`: `product` was left out, or isn't one of the products. 409\n`external_reference_exists`: that reference is taken; send a changed contract as a new version. 400\n`recipient_required`: `legal_advice` needs `recipient` or a client. 400 `recipient_invalid`: `recipient`\nlacks a name or an email address. 422 `recipient_client_unusable`: the first client's details can't be\nused; send `recipient`. 400 `recipient_not_applicable`: `recipient` was sent with another\nproduct. 400 `review_required`: `legal_information` needs `review`. 422 `review_not_available`: only `lawyer` is available. 400 `review_not_applicable`: `review` was\nsent with another product. 400 `review_invalid`: `review` isn't one of its values. 422 `file_rejected`: the\nfile isn't a readable PDF.\n422 `idempotency_key_reused`: the key was used for a different request. 400 `request_invalid`: the `request`\npart isn't JSON this call reads. 400 `clients_invalid`: `clients` isn't up to 10 `cli_…` IDs, each named\nonce. 422 `client_unknown`: one of them isn't a client of this environment. 403 `budget_used`: your\norganisation has had as many PDFs processed in the sandbox as its budget allows; the message says who to\nemail to have it raised.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "A value you make up for this request, such as a UUID. Sending the same request again with the same key answers the original response and does nothing twice; the same key with a different request is refused.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Content-Digest",
            "in": "header",
            "description": "The SHA-256 of the whole request body, exactly as sent (RFC 9530): `sha-256=:<base64>:`. Build the multipart body first, hash those bytes, then send them.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Exactly two parts, in either order: `file` and `request`.",
          "content": {
            "multipart/form-data": {
              "schema": {
                "required": [
                  "file",
                  "request"
                ],
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "description": "The contract PDF, up to 100 MB.",
                    "format": "binary"
                  },
                  "request": {
                    "required": [
                      "external_reference",
                      "product"
                    ],
                    "type": "object",
                    "properties": {
                      "external_reference": {
                        "type": "string",
                        "description": "Your own ID for the contract, up to 200 printable characters. Unique within the integration and environment,\nkept for the contract's life, and returned on every read and webhook."
                      },
                      "product": {
                        "enum": [
                          "legal_information",
                          "legal_advice",
                          "contract_viewer"
                        ]
                      },
                      "review": {
                        "enum": [
                          "on_request",
                          "lawyer",
                          "ai",
                          null
                        ]
                      },
                      "clients": {
                        "type": [
                          "null",
                          "array"
                        ],
                        "items": {
                          "type": "string"
                        },
                        "description": "The clients this contract is for: up to 10 client IDs (`cli_…`) from this environment, each named once.\nOptional."
                      },
                      "recipient": {
                        "required": [
                          "first_name",
                          "last_name",
                          "email"
                        ],
                        "type": "object",
                        "properties": {
                          "first_name": {
                            "type": "string"
                          },
                          "last_name": {
                            "type": "string"
                          },
                          "email": {
                            "type": "string"
                          }
                        },
                        "description": "The one person a `legal_advice` review is written for."
                      }
                    },
                    "description": "The JSON part of `POST /v1/contract-reviews`."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf"
                },
                "request": {
                  "contentType": "application/json"
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContractReviewResource"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "413": {
            "description": "Payload Too Large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [ ]
          }
        ]
      },
      "get": {
        "tags": [
          "ContractReviews"
        ],
        "summary": "List contract reviews.",
        "description": "Newest first, `limit` at a time (default 20, up to 100). For the next page pass `starting_after` with the last\nID. `external_reference` finds one by your own ID.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "external_reference",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContractReviewList"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [ ]
          }
        ]
      }
    },
    "/v1/contract-reviews/{id}/versions": {
      "post": {
        "tags": [
          "ContractReviews"
        ],
        "summary": "Send a new version of a contract.",
        "description": "A changed contract is a new version of the same contract review, at the same link. Same as sending a\ncontract, with `request` JSON `{ \"based_on_version\": 1 }`: the latest version you know of.\n\n202: a new version was made and is being read; until it is live its publication is `pending` with no `url`.\n200: the file is byte-identical to the latest version, so nothing was made and the latest is returned. 409\n`stale_version`: a newer version exists; the message names it. Sending `clients` replaces the contract's\nlist, and is refused with 400 `clients_invalid` or 422 `client_unknown` as on a first send. A new version\ncounts against the sandbox budget as a first send does: 403 `budget_used`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "A value you make up for this request, such as a UUID. Sending the same request again with the same key answers the original response and does nothing twice; the same key with a different request is refused.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Content-Digest",
            "in": "header",
            "description": "The SHA-256 of the whole request body, exactly as sent (RFC 9530): `sha-256=:<base64>:`. Build the multipart body first, hash those bytes, then send them.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Exactly two parts, in either order: `file` and `request`.",
          "content": {
            "multipart/form-data": {
              "schema": {
                "required": [
                  "file",
                  "request"
                ],
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "description": "The contract PDF, up to 100 MB.",
                    "format": "binary"
                  },
                  "request": {
                    "required": [
                      "based_on_version"
                    ],
                    "type": "object",
                    "properties": {
                      "based_on_version": {
                        "type": "integer",
                        "description": "The version this one replaces: the latest you know of.",
                        "format": "int32"
                      },
                      "clients": {
                        "type": [
                          "null",
                          "array"
                        ],
                        "items": {
                          "type": "string"
                        },
                        "description": "The clients the contract is for, replacing the list it had. Leave it out to keep the list as it is; send an\nempty list to clear it."
                      }
                    },
                    "description": "The JSON part of `POST /v1/contract-reviews/{id}/versions`."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf"
                },
                "request": {
                  "contentType": "application/json"
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContractReviewResource"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContractReviewResource"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "413": {
            "description": "Payload Too Large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [ ]
          }
        ]
      }
    },
    "/v1/contract-reviews/{id}": {
      "get": {
        "tags": [
          "ContractReviews"
        ],
        "summary": "Read a contract review.",
        "description": "The latest version's parts, each with its own state, and where it is published.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContractReviewResource"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResource"
                }
              }
            }
          }
        },
        "security": [
          {
            "token": [ ]
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "ApiErrorBody": {
        "required": [
          "code",
          "message"
        ],
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "A stable code to branch on, e.g. `review_required`."
          },
          "message": {
            "type": "string",
            "description": "What went wrong and what to do, for a person."
          }
        }
      },
      "ApiErrorResource": {
        "required": [
          "error"
        ],
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ApiErrorBody"
          }
        },
        "description": "Every /v1 error: `{ \"error\": { \"code\", \"message\" } }`."
      },
      "ClientList": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientResource"
            }
          },
          "has_more": {
            "type": "boolean"
          }
        },
        "description": "A page of clients, newest first. Ask for the next page with `starting_after` set to the last ID."
      },
      "ClientResource": {
        "required": [
          "id",
          "onboarding_id",
          "reference",
          "primary",
          "first_name",
          "last_name",
          "email",
          "mobile",
          "created_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Our ID for the client, e.g. `cli_…`. The same in every webhook about them."
          },
          "onboarding_id": {
            "type": "string",
            "description": "The onboarding they signed up through, e.g. `onb_…`."
          },
          "reference": {
            "type": "string",
            "description": "Your own ID for that onboarding, as you sent it."
          },
          "primary": {
            "type": "boolean",
            "description": "True for the first buyer named on the onboarding."
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "mobile": {
            "type": [
              "null",
              "string"
            ],
            "description": "As the buyer typed it; null when the form didn't ask."
          },
          "created_at": {
            "type": "string",
            "description": "When they were first submitted (UTC).",
            "format": "date-time"
          }
        },
        "description": "A client: one buyer who signed up through the onboarding form. Send its `id` with a contract to name them on\nit. Prepared keeps the rest of what the form asked; it is not part of the API."
      },
      "ClientWebhookMessage": {
        "required": [
          "type",
          "timestamp",
          "test",
          "data"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "The event's wire name, e.g. `contract-reviews.publication.published`, or `test`."
          },
          "timestamp": {
            "type": "string",
            "description": "When the event happened (UTC, ISO 8601).",
            "format": "date-time"
          },
          "test": {
            "type": "boolean",
            "description": "True for a test event sent from the portal. A test never refers to a real contract."
          },
          "data": {
            "required": [
              "client_id",
              "onboarding_id",
              "reference",
              "primary",
              "first_name",
              "last_name",
              "email"
            ],
            "type": "object",
            "properties": {
              "client_id": {
                "type": "string"
              },
              "onboarding_id": {
                "type": "string"
              },
              "reference": {
                "type": "string",
                "description": "Your own ID for the onboarding."
              },
              "primary": {
                "type": "boolean",
                "description": "True for the first buyer named on the onboarding."
              },
              "first_name": {
                "type": "string"
              },
              "last_name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              }
            },
            "description": "The data of every `clients.*` event: who the client is, and nothing else. Read anything more with\n`GET /v1/clients/{id}`."
          }
        },
        "description": "Every webhook body: what happened, when, whether it's a test, and the data. The same shape for every event, so a\nreceiver can route on `type` before reading `data`."
      },
      "ContractReviewLawyerReview": {
        "required": [
          "state"
        ],
        "type": "object",
        "properties": {
          "state": {
            "$ref": "#/components/schemas/EnvoyLawyerReviewState"
          },
          "requested_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "UTC; when the review became due.",
            "format": "date-time"
          },
          "completed_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "UTC; when the lawyer published.",
            "format": "date-time"
          }
        },
        "description": "A lawyer's review of a contract review version."
      },
      "ContractReviewList": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ContractReviewResource"
            }
          },
          "has_more": {
            "type": "boolean"
          }
        },
        "description": "A page of contract reviews, newest first. Ask for the next page with `starting_after` set to the last ID."
      },
      "ContractReviewPart": {
        "required": [
          "state"
        ],
        "type": "object",
        "properties": {
          "state": {
            "$ref": "#/components/schemas/EnvoyPartState"
          },
          "completed_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "UTC; set once the part has completed.",
            "format": "date-time"
          },
          "failure_reason": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "description": "Why the part failed; set while `state` is `failed`.",
                "$ref": "#/components/schemas/EnvoyProcessingFailureReason"
              }
            ]
          },
          "failed_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "UTC; when the part failed. Set while `state` is `failed`.",
            "format": "date-time"
          }
        },
        "description": "One part of a contract review version."
      },
      "ContractReviewPublication": {
        "required": [
          "channel",
          "state"
        ],
        "type": "object",
        "properties": {
          "channel": {
            "$ref": "#/components/schemas/EnvoyPublicationChannel"
          },
          "state": {
            "$ref": "#/components/schemas/EnvoyPublicationState"
          },
          "url": {
            "type": [
              "null",
              "string"
            ],
            "description": "Where it lives, once published."
          },
          "published_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "UTC; the latest publish.",
            "format": "date-time"
          }
        },
        "description": "One channel a contract review version is published to."
      },
      "ContractReviewRecipientDetails": {
        "required": [
          "first_name",
          "last_name",
          "email"
        ],
        "type": "object",
        "properties": {
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          }
        },
        "description": "The one person a `legal_advice` review is written for."
      },
      "ContractReviewResource": {
        "required": [
          "id",
          "external_reference",
          "product",
          "version",
          "processing",
          "publications",
          "clients",
          "created_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Our ID for the contract review, e.g. `crv_…`."
          },
          "external_reference": {
            "type": "string",
            "description": "Your own ID for the contract, as you sent it."
          },
          "product": {
            "$ref": "#/components/schemas/EnvoyProduct"
          },
          "version": {
            "type": "integer",
            "description": "The latest version: 1 for the first PDF, then one more for each new version you send.",
            "format": "int32"
          },
          "review": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "description": "How the review is published; null unless the product is `legal_information`.",
                "$ref": "#/components/schemas/EnvoyReviewMode"
              }
            ]
          },
          "processing": {
            "description": "Reading the PDF's pages and making their images.",
            "$ref": "#/components/schemas/ContractReviewPart"
          },
          "analysis": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "description": "Analysing the contract to write its review. Null for a product with no analysis.",
                "$ref": "#/components/schemas/ContractReviewPart"
              }
            ]
          },
          "lawyer_review": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "description": "A lawyer's review. Null for a contract no lawyer reviews.",
                "$ref": "#/components/schemas/ContractReviewLawyerReview"
              }
            ]
          },
          "publications": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ContractReviewPublication"
            },
            "description": "Where this version is published, one entry per channel the product publishes to."
          },
          "clients": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The clients you named on the contract (`cli_…`), in the order you sent them. Empty when you named none."
          },
          "recipient": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "description": "Who the review is for; null unless the product is `legal_advice`. Set when you first sent the\ncontract, from the `recipient` you sent or from the first client you named, and kept for its life.",
                "$ref": "#/components/schemas/ContractReviewRecipientDetails"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "description": "When you first sent the contract (UTC).",
            "format": "date-time"
          }
        },
        "description": "A contract review: the latest version's parts, each with its own state, and one entry per channel the product\npublishes to. There is no single status, because the parts happen in no fixed order."
      },
      "EnvoyEnvironment": {
        "enum": [
          "sandbox",
          "production"
        ],
        "description": "The environment an integration has reached. A key and a webhook endpoint each belong to exactly one."
      },
      "EnvoyLawyerReviewState": {
        "enum": [
          "not_requested",
          "requested",
          "completed"
        ],
        "description": "Where a lawyer's review of a contract review version stands."
      },
      "EnvoyOnboardingState": {
        "enum": [
          "open",
          "submitted",
          "accepted"
        ],
        "description": "Where a client onboarding stands: `open` until its form is first submitted, then `submitted`, then\n`accepted` once Prepared has accepted the sign-up. Submitting the form again makes it `submitted` again."
      },
      "EnvoyPartState": {
        "enum": [
          "pending",
          "running",
          "completed",
          "failed"
        ],
        "description": "How far one part of a contract review version has got."
      },
      "EnvoyProcessingFailureReason": {
        "enum": [
          "timed_out",
          "processing_error",
          null
        ]
      },
      "EnvoyProduct": {
        "enum": [
          "legal_information",
          "legal_advice",
          "contract_viewer"
        ],
        "description": "What a contract review asks for. It belongs to the contract and never changes; a different product for the same\ndocument is a different contract."
      },
      "EnvoyPublicationChannel": {
        "enum": [
          "contract_viewer",
          "review",
          "review_snapshot"
        ],
        "description": "Where a contract review is published. The product decides which channels a version has."
      },
      "EnvoyPublicationState": {
        "enum": [
          "not_requested",
          "pending",
          "published",
          "withdrawn",
          "failed"
        ],
        "description": "Where one publication channel of a contract review version stands."
      },
      "EnvoyReviewMode": {
        "enum": [
          "on_request",
          "lawyer",
          "ai",
          null
        ]
      },
      "OnboardingResource": {
        "required": [
          "id",
          "reference",
          "state",
          "clients",
          "created_at",
          "submitted_at",
          "accepted_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Our ID for the onboarding, e.g. `onb_…`."
          },
          "reference": {
            "type": "string",
            "description": "Your own ID for the sign-up, as you sent it."
          },
          "state": {
            "$ref": "#/components/schemas/EnvoyOnboardingState"
          },
          "clients": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientResource"
            },
            "description": "The buyers named so far, the primary one first. Empty until the form is submitted."
          },
          "created_at": {
            "type": "string",
            "description": "When you opened it (UTC).",
            "format": "date-time"
          },
          "submitted_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "The latest submission (UTC); null until the form is submitted.",
            "format": "date-time"
          },
          "accepted_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "When Prepared last accepted a submission (UTC); null until one is accepted.",
            "format": "date-time"
          }
        },
        "description": "One sign-up through the onboarding form, and the clients it has named so far."
      },
      "OnboardingSessionResource": {
        "required": [
          "id",
          "reference",
          "state",
          "session_token",
          "created_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Our ID for the onboarding, e.g. `onb_…`."
          },
          "reference": {
            "type": "string",
            "description": "Your own ID for the sign-up, as you sent it."
          },
          "state": {
            "$ref": "#/components/schemas/EnvoyOnboardingState"
          },
          "session_token": {
            "type": "string",
            "description": "Give this to your page and pass it to `Prepared.onboarding.mount`. It can show and submit this one\nonboarding's form and read nothing, so it is safe in a browser. It does not expire."
          },
          "created_at": {
            "type": "string",
            "description": "When you opened it (UTC).",
            "format": "date-time"
          }
        },
        "description": "What `POST /v1/clients/onboardings/sessions` returns: the onboarding, and the token your page shows its form with."
      },
      "ProcessingWebhookMessage": {
        "required": [
          "type",
          "timestamp",
          "test",
          "data"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "The event's wire name, e.g. `contract-reviews.publication.published`, or `test`."
          },
          "timestamp": {
            "type": "string",
            "description": "When the event happened (UTC, ISO 8601).",
            "format": "date-time"
          },
          "test": {
            "type": "boolean",
            "description": "True for a test event sent from the portal. A test never refers to a real contract."
          },
          "data": {
            "required": [
              "contract_review"
            ],
            "type": "object",
            "properties": {
              "contract_review": {
                "$ref": "#/components/schemas/ContractReviewResource"
              }
            },
            "description": "The data of the events about a part of a contract review (`processing.failed`, `analysis.completed`,\n`lawyer_review.requested`, `lawyer_review.completed`): the contract review as\n`GET /v1/contract-reviews/{id}` shows it, with the part's own state saying what happened."
          }
        },
        "description": "Every webhook body: what happened, when, whether it's a test, and the data. The same shape for every event, so a\nreceiver can route on `type` before reading `data`."
      },
      "TokenResource": {
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "environment"
        ],
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string"
          },
          "token_type": {
            "type": "string"
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds until it expires. Exchange your key again for a new one.",
            "format": "int32"
          },
          "environment": {
            "$ref": "#/components/schemas/EnvoyEnvironment"
          }
        },
        "description": "What `POST /v1/token` returns."
      },
      "WebhookMessage": {
        "required": [
          "type",
          "timestamp",
          "test",
          "data"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "The event's wire name, e.g. `contract-reviews.publication.published`, or `test`."
          },
          "timestamp": {
            "type": "string",
            "description": "When the event happened (UTC, ISO 8601).",
            "format": "date-time"
          },
          "test": {
            "type": "boolean",
            "description": "True for a test event sent from the portal. A test never refers to a real contract."
          },
          "data": {
            "required": [
              "channel",
              "url",
              "contract_review"
            ],
            "type": "object",
            "properties": {
              "channel": {
                "enum": [
                  "contract_viewer",
                  "review",
                  "review_snapshot"
                ],
                "description": "Where a contract review is published. The product decides which channels a version has."
              },
              "url": {
                "type": "string",
                "description": "Where it lives now."
              },
              "contract_review": {
                "$ref": "#/components/schemas/ContractReviewResource"
              }
            },
            "description": "The data of every `contract-reviews.publication.*` event: which channel, its URL, and the contract\nreview as `GET /v1/contract-reviews/{id}` shows it, so a receiver can refresh its copy from the event alone."
          }
        },
        "description": "Every webhook body: what happened, when, whether it's a test, and the data. The same shape for every event, so a\nreceiver can route on `type` before reading `data`."
      }
    },
    "securitySchemes": {
      "api_key": {
        "type": "http",
        "description": "Your API key (`prep_test_…` or `prep_live_…`). Only `POST /v1/token` takes it.",
        "scheme": "bearer"
      },
      "token": {
        "type": "http",
        "description": "The token `POST /v1/token` answers. It lasts 15 minutes.",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "tags": [
    {
      "name": "Clients"
    },
    {
      "name": "Token"
    },
    {
      "name": "ContractReviews"
    }
  ],
  "webhooks": {
    "contract-reviews.processing.failed": {
      "post": {
        "summary": "We couldn't process the PDF. `data.contract_review` is the contract review as `GET /v1/contract-reviews/{id}` shows it: `processing.state` is `failed` and `processing.failure_reason` says why (`timed_out` or `processing_error`). A failure isn't final: we may retry the run, and `publication.published` can follow.",
        "operationId": "webhook_contract-reviews_processing_failed",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProcessingWebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "contract-reviews.analysis.completed": {
      "post": {
        "summary": "A contract version has been analysed. `data.contract_review` is the contract review as `GET /v1/contract-reviews/{id}` shows it.",
        "operationId": "webhook_contract-reviews_analysis_completed",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProcessingWebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "contract-reviews.analysis.failed": {
      "post": {
        "summary": "A contract version couldn't be analysed. Coming with the review products.",
        "operationId": "webhook_contract-reviews_analysis_failed",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "contract-reviews.lawyer_review.requested": {
      "post": {
        "summary": "A lawyer's review of a contract version is due: its analysis is done. `data.contract_review` is the contract review.",
        "operationId": "webhook_contract-reviews_lawyer_review_requested",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProcessingWebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "contract-reviews.lawyer_review.completed": {
      "post": {
        "summary": "A lawyer reviewed a contract version and published it. `data.contract_review` is the contract review; a `publication.published` is sent for each channel.",
        "operationId": "webhook_contract-reviews_lawyer_review_completed",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProcessingWebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "contract-reviews.publication.published": {
      "post": {
        "summary": "Something went live, or was republished. `data.channel` says where, `data.url` is the link, and `data.contract_review` is the contract review as `GET /v1/contract-reviews/{id}` shows it.",
        "operationId": "webhook_contract-reviews_publication_published",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "contract-reviews.publication.withdrawn": {
      "post": {
        "summary": "Something was taken down. Coming with the review products.",
        "operationId": "webhook_contract-reviews_publication_withdrawn",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "clients.submitted": {
      "post": {
        "summary": "A buyer was named on a submitted sign-up form: one message per buyer, every time the form is submitted. `data.client_id` is the client; a buyer submitted again keeps the same one. You can receive it more than once for the same `client_id`: treat each one as that client's latest details.",
        "operationId": "webhook_clients_submitted",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientWebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "clients.accepted": {
      "post": {
        "summary": "Prepared accepted a sign-up: one message per buyer, with the same `data.client_id` as `clients.submitted`. It can follow `clients.submitted` at once, or by minutes or days.",
        "operationId": "webhook_clients_accepted",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientWebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    },
    "test": {
      "post": {
        "summary": "A test message sent from the portal or the MCP server. `test` is true; the data is an example.",
        "operationId": "webhook_test",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookMessage"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is retried."
          }
        }
      }
    }
  }
}