browse_deal_vaults
ChatGPTOpen a deal's document workspace — an inline browser of its vaults with the files and memos inside. Use for any broad "show me / what are the documents (files, attachments, paperwork) on this deal", "what's in the dataroom", or "browse / explore / look through the documents" ask — the user wants to see or work through a deal's documents, not find one named file. On MCP Apps hosts (Claude Desktop) this opens an inline browser of the deal's vaults, each expandable into the documents and memos it holds, with any PDF or image viewable inline. Other hosts get the deal's files (from Resources, its complete set) plus its memos as a list. Tool-use examples: show me the documents on deal 174124 -> this tool; what's in the dataroom for deal 174124 -> this tool. Use list_deal_documents instead to find one specific file or filter (a rent roll, an extension, one folder); list_deal_vaults for just the list of vaults; get_deal_document to open or download one specific file; and search_deal_index for what facts came from the documents.
create_company
ChatGPTCreate a new private company. company_type: "lender" or "sponsor" address: optional street address (free-form single line) city: optional city name state: optional US state as a 2-letter code (e.g. NY, CA) zip: optional postal code as a string (preserves leading zeros) linkedin_url: optional LinkedIn profile URL for the company org_id: optional ID of a platform-wide Organization record to link this company to. Omit unless you already know the exact ID — the MCP does not expose a tool to list or search organizations, so a guessed or hallucinated value will silently bind the company to the wrong real-world entity. When omitted, the backend finds or creates the Organization automatically from name/website.
create_contact
ChatGPTCreate a new contact linked to a company. contact_type: "lender_contact" or "sponsor" (the backend's CreateContactRequest schema is strictly limited to these two values — see lev-backend/app/api_schemas/request/external_v2_contacts.py) company_id: the ID of the company this contact belongs to (use list_companies to find it) department: optional department or team name (e.g. Originations, Credit) address: optional street address (free-form single line) city: optional city name state: optional US state as a 2-letter code (e.g. NY, CA) zip: optional postal code as a string (preserves leading zeros) linkedin_url: optional LinkedIn profile URL for the contact
create_deal
ChatGPTCreate a new CRE financing deal. Requires at minimum a title. Enum values (must match exactly — see lev-backend/app/models/enums.py): loan_type: "construction", "heavy_bridge", "light_bridge", "permanent", "land", "predevelopment", "tbd" transaction_type: "acquisition", "refinance", "new_construction", "tbd" business_plan: "stabilized", "value_add", "construction", "land" estimated_close_date: ISO 8601 date string (e.g. 2026-06-01) sponsor_private_company_id: optional ID of an existing sponsor company to link to the deal. Find it with list_companies (filter by company_type= sponsor) or create one with create_company. The sponsor's users are automatically added to the deal team when the deal is brokered. Use this in the onboarding flow: create_company → create_contact → create_deal(..., sponsor_private_company_id=<id>) so the sponsor is linked from the start rather than needing a follow-up update_deal call. pipeline_ids: optional list of pipeline IDs to associate with the deal at creation time (use list_pipelines to discover available IDs). To change pipeline/status after creation, use move_deal_to_pipeline_status — update_deal does not accept this field.
create_note
ChatGPTLog a free-text note on a contact, deal, placement, or company — or post a comment on a checklist task. Use for human-written commentary. To record a structured deal fact, use record_deal_facts instead — notes are not indexed facts. parent_type: "contact", "deal", "placement", "company", or "checklist_task" — which kind of record to note. Use "placement" for notes on one lender's outreach within a deal; "deal" for deal-wide notes; "company" for a lender or sponsor firm; "contact" for a specific person; "checklist_task" to comment on a checklist item (e.g. a document request) — these are the comments borrowers and your team see in the portal. parent_id: numeric ID of the contact, deal, placement, company, or checklist task text: the note content (required, non-empty). Simple HTML formatting is preserved; scripts and other unsafe markup are stripped. The note is attributed to you and visible in the Lev web app.
create_placement
ChatGPTCreate a new placement (record of lender outreach) on a deal. Use after the user picks a lender to reach out to: pair the deal with a lender company (and optionally a specific lender contact at that company). Subsequent term sheets are attached to this placement via create_term_sheet. Required: deal_id: numeric ID of the deal (use list_deals or get_deal to find it). private_company_id: numeric ID of the lender company (use list_companies filtered by lender company_type to find it). Optional: contact_id: numeric ID of the lender contact at that company. Must belong to the same lender — otherwise the request is rejected. When omitted, the placement is created without a tied contact; no heuristic matching is performed. status: placement status. Defaults to "new" if omitted. visibility: who can see this placement. The default depends on the deal-owner account's configuration (typically "hidden" for direct outreach, "shared" for customer-owned deals). description: free-text note, e.g. 08/11 soft quote received. score: manual relevance score (0-100). outreach_date: ISO 8601 timestamp when the lender was first contacted (e.g. 2026-05-01T00:00:00Z). lender_status: lender-side status flag. Enum values (must match exactly — see lev-backend/app/models/enums.py): status: "new", "sent", "lender_reviewing", "terms_received", "term_sheet_received", "executed_ts_in_closing", "closed", "unresponsive", "willing_to_negotiate", "lender_passed", "carve_out", "carve_out_closed" (archived is intentionally NOT a valid create status — it is the removal sentinel and only applies via update_placement on an existing row.) visibility: "hidden", "masked", "shared" lender_status: "origination", "new", "lead_qualification", "quotation", "negotiation", "offer", "term_sheet", "good_faith_deposit", "diligence", "in_closing", "closed", "archived" Duplicate guard: applies only when contact_id is provided. If a placement already exists for that deal + contact combination (on either the same lender or a different one), the request is rejected — use update_placement to edit the existing one, or PATCH status="archived" first if you want to recreate with a different contact. Without contact_id, the backend does not preflight duplicates, so multiple placements on the same deal + lender can coexist. There is no DELETE — to remove a placement, call update_placement with status="archived".
create_term_sheet
ChatGPTCreate a new term sheet (lender loan quote) on a placement. Use after a lender returns a quote on an existing placement: record the headline economics (rate, loan amount, term, recourse, etc.) so the deal team can compare quotes across lenders. The placement must already exist on this deal — see create_placement. Required: deal_id: numeric ID of the deal. placement_id: numeric ID of the placement this quote belongs to. Must be a placement on deal_id; cross-deal IDs return 404. total_rate: total interest rate as a percent (e.g. 7.53 for 7.53%). initial_funding: provided loan amount (whole dollars). quote_type: type of quote being recorded. rate_type: fixed or floating. Enum values (must match exactly — see lev-backend/app/models/enums.py): quote_type: "guidance", "indication", "soft_quote", "hard_quote", "term_sheet" rate_type: "fixed", "floating" recourse: "personal_recourse", "fund_corporate_recourse", "non_recourse" recourse_type: "full", "partial", "burn_off" floor_type: "base_rate", "total_rate" prepayment_penalty: "step_down", "defeasance", "minimum_interest", "yield_maintenance", "minimum_multiple", "swap_breakage", "no_prepayment_penalty", "no_ability_to_prepay", "flat_fee", "other" payment_method: "accrued", "partial", "current_pay" capital_source_type: "balance_sheet", "agency", "cmbs_clo", "warehouse", "sba" base_rate: BaseRateNames member name (e.g. "treasury_y5", "sofr_m1", "prime_rate") OR the matching display value (e.g. "Treasury 5-Yr"). Use the literal string "none-fixed" for fixed- rate quotes that have no underlying base rate. Unknown names return 422 with the full valid list. Defaults applied by the backend when omitted: winning → false (only one term sheet per deal should be winning; this is not enforced server-side). is_visible_to_borrower → true. Visibility caveat: the read endpoint hides term sheets whose quote_type is not in the default-visible set or whose is_visible_to_borrower is false (visibility policy applies to non-principal viewers). A successful create followed by a get returning 404 means the new row exists but is hidden by policy — re-check the quote_type / is_visible_to_borrower values you sent.
delete_deal
ChatGPTDelete a deal. This is a soft delete and can be reversed by Lev support.
delete_note
ChatGPTPermanently delete a free-text note from a contact, deal, placement, or company — or a comment from a checklist task. Use only when the user asks to remove a human-written note. To remove another resource, use that resource's delete/archive tool instead — this only deletes notes. parent_type: "contact", "deal", "placement", "company", or "checklist_task" — which kind of record owns the note parent_id: numeric ID of the contact, deal, placement, company, or checklist task note_id: numeric ID of the note to delete This is a permanent hard delete with no undo. You can only delete a note you created and still have parent write access to; another user's note or a note on another parent returns not found.
delete_term_sheet
ChatGPTDelete a term sheet. Soft delete — reversible by Lev support.
get_asset_types
ChatGPTGet the list of property asset type classifications used in Lev. Returns types like Office, Apartment/Multifamily, Industrial, Retail, Hotel, Self-Storage, etc. Each type has an ID used when creating deals and filtering properties.
get_base_rates
ChatGPTGet current market base rates (SOFR, Prime, Treasury, etc.). Returns the latest reference rates used in CRE lending. Useful for understanding current market conditions and evaluating term sheet rates. Data is cached and updates approximately every 30 minutes.
get_company
ChatGPTGet full details for a specific company including name, type, address, and website.
get_contact
ChatGPTGet full details for a specific contact including name, email, title, company, and address.
get_deal
ChatGPTGet full details for a specific deal including financials, properties, team, and pipelines. Use after list_deals to drill into a specific deal. The include parameter controls which sub-resources are embedded: financials — NOI, LTV, costs properties — addresses, asset types team — assigned users and roles pipelines — the deal's current pipeline rows, each with pipeline_id, pipeline_name, pipeline_type, pipeline_status_id, status, status_name, updated_at. Capped at 10 rows per deal. Empty array when the deal is not on any pipeline. To narrow the response, pass a comma-separated subset (e.g. include="financials,pipelines").
get_deal_checklist
ChatGPTRead a deal's checklist — what still needs to be collected, reviewed, or completed, and what's done. Use when the user asks what's outstanding, missing, still needed, requested, or finished on a deal: open checklist items, document requests, diligence or closing checklist progress. Each of the deal's Deal Rooms (shared datarooms) can carry its own checklist; this returns one checklist per Deal Room — the deal's primary Deal Room first — each with its sections and the tasks inside, in display order. Tasks carry two completion signals: is_completed is the done flag, and status is the review state — a task can be approved but not yet marked complete. Tasks can also include document_types (the expected document categories for that request) and files (attachments already linked to that task, with document_id, vault_resource_id, name, and origin). The checklist is still the deal's to-do / request list, not the full file browser: use list_deal_documents or browse_deal_vaults for every file uploaded to a vault, and search_deal_index for facts extracted from them. Each task can include assignee (the individual person) and assigned_team (the account, lender, or borrower team responsible for the task). Treat assigned_team.name as nullable: stale or out-of-scope team references keep id/type but return name as null. status: each task's review state — "to_do", "requested", "reviewing", "updates_needed", "approved", or "cancelled". vault_id: optional Deal Room id from list_deal_vaults to read just that room's checklist. Omit to get every Deal Room's checklist. A deal's private Resources area has no checklist (returns an empty list).
get_deal_document
ChatGPTGet one uploaded deal document, including a clickable download link. Use after list_deal_documents when the user wants to inspect, open, or download a specific uploaded file. Returns the document metadata plus download_link, a Markdown link labeled with the file name and backed by Lev's short-lived signed download URL. Present download_link as the clickable link it already is; never reconstruct or display the raw underlying URL. Use get_deal_memo instead for generated memo / deal book PDFs.
get_deal_memo
ChatGPTGet one memo for a deal, including a downloadable PDF preview link. Use after list_deal_memos when the user wants to preview, open, or download a specific memo's PDF. memo_id is the memo's uuid from list_deal_memos. Returns the memo's metadata — including, when the workspace has them, who created it, when it was last modified, and which Deal Room(s) or Resources area it lives in — plus a pdf_link (a ready-to-click Markdown link to the rendered PDF) and a pdf_versions array with one link per rendered quality. On MCP Apps hosts (Claude Desktop) the memo renders inline as a scrollable PDF viewer (the `ui://memo-view` widget); other hosts see the markdown pdf_link. Present pdf_link to the user as the clickable link it already is; never reconstruct or display the raw underlying URL — it is a short-lived signed link. When the memo is an unpublished draft that has not been rendered yet, there is no PDF: pdf_link is absent and pdf_versions is empty (pdf_ready will be false). Say the PDF isn't available yet rather than implying one exists. quality: which rendered variant the top-level pdf_link points at. One of "original" (default, full fidelity), "high", "medium", "low" (progressively more compressed / smaller files). pdf_versions always lists every rendered variant regardless of this choice.
get_lender
ChatGPTGet lender details including their lending programs. Programs describe specific loan products the lender offers: loan type, amount range, LTV/LTC limits, DSCR requirements, term length, and supported asset types.
get_my_billing_summary
ChatGPTGet your Lev account's current subscription and credit balance. Returns two blocks: - subscription: the active plan's type, status, billing cycle, and end date - credits: whether the account is provisioned for credit-based billing, the current balance, and the unit (always "credits") Use this before suggesting work that consumes credits (e.g. AI actions, some placement flows) to confirm the user has headroom. An enabled value of false means the account is not provisioned for credits — balance will be null rather than 0 so you can distinguish "not provisioned for credits" from "provisioned with a zero balance". Cost-per-credit and transaction history are not currently returned — use the web app for those. Call get_my_credit_balance instead when only the balance block is needed.
get_my_credit_balance
ChatGPTGet your Lev account's current credit balance (singleton snapshot). Returns { enabled, balance, unit }. Prefer get_my_billing_summary when you also want subscription context — this tool is for cases where only the balance is needed. enabled=false / balance=null means the account is not provisioned for credit-based billing. enabled=true / balance=0 means it is provisioned but has zero credits available. Do not conflate the two when relaying to the user.
get_my_profile
ChatGPTGet your Lev profile, currently-active account, subscription tier, and API capabilities. Returns your user details, active account, subscription status, granted scopes, rate limits, and API key quota. Call this first to confirm your connection and see what you have access to. To see all accounts you can switch into, use list_available_accounts. To switch, use switch_account. The granted_scopes field is internal context for your own reasoning — use it to decide which tools a workflow is allowed to call. Do not echo the raw scope list back to the user; summarize capabilities in plain language if they ask what access they have.
get_pipeline
ChatGPTGet details for a specific pipeline including all its statuses with IDs and ordering. Each status carries both a raw status (wire form, e.g. under_review) and a custom_status_name (display form, e.g. "Under Review"). When speaking to the user about a status, use custom_status_name; the raw status is for filtering and for the move_deal_to_pipeline_status call.
get_placement
ChatGPTGet details for a specific placement including status, match score, and lender info.
get_term_sheet
ChatGPTGet full term sheet details including rate structure, fees, recourse terms, and extensions. Contains all financial details of a lender's loan quote: rate type (fixed/floating), base rate, spread, origination fee, prepayment penalty, amortization, IO period, extension options, recourse terms, DSCR/debt yield requirements, and more.
insert_deal_index_facts
ChatGPTRecord one or more user-supplied metric values on a deal. Use this to record a measured or extracted underwriting fact — NOI, occupancy, expenses, valuation, purchase price — that the deal does not have yet, or to log an additional sourced observation for one it does. Discover the metric and its id with list_deal_index_metric_definitions. To change or correct a fact the deal already shows (e.g. 'fix the NOI to $1.25M'), don't use this tool — use search_deal_index to find it, then update_deal_index_fact, which sets the canonical value directly. Reach for this tool to add a value, not to edit one in place. Route by the catalog's suggested_write_via before recording here: - index — record it with this tool. - deal_record — do NOT use this tool; write it on the deal's canonical record with the named tool (update_deal / update_company / update_contact). - clarify — loan amount is the case: ask the user whether they're setting the deal's official value (use the named CRM tool) or logging what a source reported (record it here). Don't guess. How recording behaves: this logs the value as an observation; it does not erase prior values, and the value shown on the deal follows Lev's promotion logic, so it may not change immediately even when recording succeeds. Recording the same metric again adds another observation rather than editing one in place. Always confirm the concrete change with the user in plain English first. This does not create entities. To record a value for a property the deal doesn't have yet, add the property via update_deal (add_properties) first, then record against its entity_ref. values: a list of values to record, each an object with: metric_id: numeric id from list_deal_index_metric_definitions. Internal — never surface it to the user. value: the value to record, as text (e.g. 1250000, or 92%). entity_ref: optional. Target an existing entity, such as property:702 or sponsor_contact:55, taken from the catalog or a search_deal_index result. Omit for deal-level facts; on a single-property deal Lev can infer it. Internal — never surface it. group_ref: optional. Use only to group several values for the same entity when no entity_ref is available yet. Internal — never surface it. Each value is recorded independently: the response reports how many were recorded and surfaces any the backend rejected, so report the true outcome — never claim a value was saved if it appears under failed.
list_available_accounts
ChatGPTList the Lev accounts you can switch into. Returns each account's name, slug, and type. Use this before switch_account when the user asks to switch accounts but you don't know the exact target — show the returned list and let them pick by name. If you need to know which account is currently active, call `get_my_profile` — the backend tracks the active account server-side.
list_companies
ChatGPTSearch private companies (lenders and sponsors) in your account by name, state, or city. Returns company summaries — use get_company for full details. sort: pass a bare key for ascending; prefix with a minus sign for descending (e.g. -name). Defaults to descending created_at. Valid keys (must match exactly — see lev-backend COMPANY_SORTABLE_FIELDS): "name", "city", "state", "created_at", "updated_at" Pagination: - Default (sort set): offset mode. Walk pages with offset=N using the value surfaced in the previous response's hint. - Stable paging for bulk sync: pass sort=None; walk pages with cursor=<value> from the hint. cursor and sort cannot be combined — passing both returns a 400.
list_contacts
ChatGPTSearch contacts (lender contacts and sponsor contacts) by name, email, or type. contact_type: one of "lender_contact", "sponsor", "legal" (the filter column accepts any value from the backend ContactType enum; these are the externally useful ones — see lev-backend/app/models/enums.py for the full set) Returns contact summaries — use get_contact for full details. sort: pass a bare key for ascending; prefix with a minus sign for descending (e.g. -last_name). Defaults to descending created_at. Valid keys (must match exactly — see lev-backend CONTACT_SORTABLE_FIELDS): "first_name", "last_name", "email", "created_at", "updated_at" Pagination: - Default (sort set): offset mode. Walk pages with offset=N using the value surfaced in the previous response's hint. - Stable paging for bulk sync: pass sort=None; walk pages with cursor=<value> from the hint. cursor and sort cannot be combined — passing both returns a 400.
list_deal_documents
ChatGPTFind specific uploaded files on a deal — by name, type, folder, or vault. Use when the user wants particular uploaded documents: a named file, a rent roll, an appraisal, a budget, closing docs, files of one extension, or the contents of one folder or one vault. Returns file metadata only; no signed download links. Each file is tagged with the container it lives in (Resources, or a Deal Room / shared dataroom). When naming a container to the user, use its title, never the raw type value. For a broad "what documents are on this deal / what's in the dataroom" ask — where the user wants to see or browse everything rather than find one file — use browse_deal_vaults instead: it opens the deal's document workspace and shows uploaded files and generated deal books together (users don't distinguish Lev's uploaded-documents-vs-memos split). Stay on this tool when the user names a specific file or filter (a rent roll, an extension, one folder, "how many"). Example: find the rent roll PDF on deal 174124 -> this tool; show me the documents on deal 174124 -> browse_deal_vaults. Use list_deal_vaults to see the deal's vaults themselves, get_deal_document when the user wants to open or download a specific file, and search_deal_index when the user asks what facts came from the documents. search: optional file-name search text. extension: optional file extension filter, with or without the leading dot; case is normalized before calling Lev. folder_id: optional folder id to list only files in one vault folder. vault_id: optional vault id to list only the files in one of the deal's containers (a Deal Room or Resources); ids come from list_deal_vaults. Omit to list across every container. limit: maximum documents to return. Defaults to 20, clamped by Lev to 1-200. cursor: pagination cursor from the previous response hint.
list_deal_index_metric_definitions
ChatGPTList the metrics that can be recorded on a deal, with a routing hint. Use this before recording a brand-new fact the deal doesn't have yet — it tells you which metrics exist (e.g. NOI, occupancy, purchase price) and, for each, the recommended way to write it. To change a fact the deal already has, prefer search_deal_index (it returns the fact's identifier directly); reach for this catalog when you need to discover what's recordable. Each metric carries a suggested_write_via routing hint: - index — record it with insert_deal_index_facts (a measured / extracted underwriting fact: NOI, occupancy, expenses, valuation, purchase price). Pass this metric's id to that tool. - deal_record — write it on the deal's canonical record instead; the metric also carries write_via_tool naming which tool to use (update_deal, update_company, or update_contact). - clarify — the metric can live in BOTH places and the right one depends on what the user means. Ask them: are they setting the deal's official value (→ the named CRM tool), or logging what a source reported (→ insert_deal_index_facts)? Loan amount is the common case. Do not pick a door for these without asking. suggested_write_via is a best-effort hint, not a guarantee — confirm the intended change with the user in plain English before any write, and never surface the hint, the metric id, or write_via_tool to them. metric_label: optional case-insensitive search term to narrow the catalog to matching metrics (e.g. occupancy, loan). category_id: optional numeric category filter. limit: maximum metrics to return per page. Defaults to 100, clamped by Lev to 1-200. The catalog is usually small — narrowing with metric_label or category_id is often easier than paging. offset: 0-based index of the first metric to return. Defaults to 0. When the response has has_more=true, walk to the next page by passing the offset emitted in the response hint; keep limit constant across pages.
list_deal_index_observations
ChatGPTShow the per-document values our extractors pulled for a deal fact. Use when the user wants to know where a fact came from, see every value we extracted for it across documents (e.g. 'what NOIs do we have on file?', 'where did this loan amount come from?'), or pick among multiple extracted values during an edit. The user will describe the fact in plain English — they will not know its internal identifiers, and you should never ask for one. Resolve the right identifier from a prior search_deal_index result yourself. Prefer promoting an extracted value over a free-text override when the user is choosing among candidates — it preserves the source document, page range, and confidence score. Translate everything back to plain English when speaking to the user: never surface the word 'observation' or echo any internal identifier. Frame it as 'extracted values' or 'what we found in the documents'. sot_id: identifier carried by a prior search_deal_index result (e.g. d:5362, a:123:property:690). Preferred filter — returns only the extracted values tied to that specific fact. Internal — never surface to the user. metric_id: numeric metric definition id. Use to fetch every extracted value for a metric across entities (broader than what the current canonical value picked). Combine with entity_ref to scope to a single property or group. Internal — never surface to the user. entity_ref: optional entity scoping (e.g. property:690). Used alongside metric_id; ignored without one. Internal — never surface to the user. limit: maximum results to return. Defaults to 20 and is clamped by Lev to 1-200. Most facts have only a handful of extracted values, so the default is usually plenty. offset: 0-based index of the first result to return. Defaults to 0 (first page). When the response has has_more=true, walk to the next page by passing the offset emitted in the response hint. Keep limit constant across pages; mixing limits mid-walk can skip or duplicate items. include_signed_urls: default false. When true, document-backed source records may include short-lived signed attachment URLs for inspection or download. The response converts returned signed URLs into source.file_link Markdown hyperlinks and omits raw source.signed_url. Keep false when the user asks only what the source is; set true only when the user asks to open or inspect the source document. Never show a bare signed URL in the final user answer; present only the file name as a hyperlink. At least one of sot_id or metric_id must be provided. Each result carries the extracted value, metric metadata, context and summary text, confidence, weight, and the same source metadata shape as search_deal_index. When the user is choosing among candidates to update a fact, pass the chosen result's `observation_id back to update_deal_index_fact — silently, behind a plain-English confirmation. The observation_id` field is internal; never surface it to the user.
list_deal_memos
ChatGPTList the memos (generated CRE deal books) for a deal. Use when the user asks what memos or deal books exist for a deal, or as the first step before fetching one to preview. Returns both published memos and unpublished drafts so a user can find work in progress, most recently active first. Narrow the result server-side with the filters below — published memos first, then drafts — rather than pulling every memo and filtering client-side. On MCP Apps hosts (Claude Desktop) the list renders inline as a memo browser (the `ui://memo-view` widget) where each memo expands into a PDF preview; other hosts see this JSON list. Each row carries the memo's publish status (draft or published), when it was published (null for drafts), its type, and a pdf_ready flag telling you whether a downloadable PDF has been rendered yet; when the workspace has them, a row also carries when the memo was last modified, who created it, and which Deal Room or Resources area it lives in. The list does not include the PDF link itself — call get_deal_memo for the downloadable preview link. A row also carries a memo_url deep link into the Lev web app; surface it only when the user explicitly asks to open the memo in Lev, and never fabricate one — use only the value present in the response. Filters below are optional and combine; pagination reflects the narrowed set. There is no sort control — ordering is fixed (published first). status: return only memos in one publish state. Values (exact): "draft", "published". Omit to return both. search: case-insensitive substring match on the memo title. Pass a distinctive fragment of the title, not the user's whole sentence. Omit to match every title. memo_type: return only memos of one kind. Values (exact): "debt_financing_om", "credit_memo", "investment_sales", "debt_brokerage", "equity_raise", "broker_opinion_of_value", "investment_committee", "invoice", "other". Omit to return all types. pdf_ready: pass true for only memos whose downloadable PDF has finished rendering, false for only those without a rendered PDF yet. Omit to return both. vault_id: optional vault id to list only the memos in one of the deal's containers (a Deal Room or Resources); ids come from list_deal_vaults. Omit to list memos across every container. limit: maximum memos to return. Defaults to 20, clamped by Lev to 1-200. offset: 0-based index of the first memo to return. Defaults to 0. When the response has has_more=true, walk to the next page by passing the offset emitted in the response hint; keep limit constant across pages.
list_deal_vaults
ChatGPTSee a deal's vaults — its Deal Rooms (shared datarooms) and its private Resources area. Use when the user asks what vaults, datarooms, or Deal Rooms a deal has, how its documents are organized into rooms, or wants to pick one to look in. To customers "vaults" usually means the shared Deal Rooms; a deal also has one private Resources area for its own working files. Returns each one with its title, whether it is the deal's primary Deal Room, and how many documents it holds. When naming one to the user, use its title (the Resources area shows as Deal Resources; Deal Rooms carry names like Deal Room, Closing, or a per-lender room) — never the raw type value. This returns the rooms (the containers), not the files inside them — use list_deal_documents to browse the files, optionally passing a vault_id from here to list just one room's files.
list_deals
ChatGPTSearch and list CRE financing deals in your Lev account. Filter by loan type, transaction type, business plan, loan amount range, or current pipeline / pipeline status. Returns deal summaries — use get_deal for full details including financials, properties, team, and pipeline rows. Enum values (must match exactly — see lev-backend/app/models/enums.py): loan_type: "construction", "heavy_bridge", "light_bridge", "permanent", "land", "predevelopment", "tbd" transaction_type: "acquisition", "refinance", "new_construction", "tbd" business_plan: "stabilized", "value_add", "construction", "land" Caveat on transaction_type: deals commonly have a null transaction_type — it is not required at creation and is often filled in later. Filtering by transaction_type silently excludes every deal that hasn't been categorized yet, so prefer leaving it unset unless you specifically want the categorized subset. Pipeline filters (exact match only — one value each, no range or "in" support): pipeline_id: numeric ID of a pipeline (from list_pipelines). Returns only deals currently on that pipeline. pipeline_status_id: numeric ID of a pipeline status (from list_pipelines, which embeds statuses with IDs). Returns only deals currently in that specific status. Use this for stage-specific filtering and stuck-deal audits. sort: pass a bare key for ascending; prefix with a minus sign for descending (e.g. -loan_amount). Defaults to descending created_at. Valid keys (must match exactly — see lev-backend DEAL_SORTABLE_FIELDS): "title", "loan_amount", "created_at", "updated_at" Pagination: - Default (sort set): offset mode. Walk pages with offset=N using the value surfaced in the previous response's hint. - Stable paging for bulk sync: pass sort=None; walk pages with cursor=<value> from the hint. cursor and sort cannot be combined — passing both returns a 400.
list_notes
ChatGPTRead the notes logged on a contact, deal, placement, or company — or the comment thread on a checklist task. Use for human-written notes like call logs or relationship context. For structured underwriting facts (NOI, loan amount, where a number came from) use search_deal_index instead — that is not what notes are. parent_type: "contact", "deal", "placement", "company", or "checklist_task" — which kind of record to read from. "checklist_task" returns the comment thread shown on that checklist item in the borrower portal. parent_id: numeric ID of the contact, deal, placement, company, or checklist task Returns each note's text, author, and timestamps in chronological order (oldest first). Internal or private notes are not returned. Pagination: uses cursor pagination. To walk beyond the first page, pass the cursor value surfaced in the previous response's hint; the most recent notes are on the last page.
list_pipelines
ChatGPTList all pipelines and their statuses in your Lev account. Each pipeline has ordered statuses that deals can be moved through (e.g., "New Lead" → "Under Review" → "In Closing" → "Closed"). Use this to see available pipelines and status IDs before calling move_deal_to_pipeline_status. Each status carries both a raw status (wire form, e.g. under_review) and a custom_status_name (display form, e.g. "Under Review"). When speaking to the user about a status, use custom_status_name; the raw status is for filtering and for the move_deal_to_pipeline_status call.
list_placements
ChatGPTList lender placements (outreach) across your deals. Shows which lenders have been contacted for each deal and their response status. Filter by deal_id to see all placements for one deal, or by status to find placements in a specific stage. Filter by contact_id or private_company_id to see every outreach aimed at a specific lender contact or lender company. Enum values (must match exactly — see lev-backend/app/models/enums.py): status: "new", "sent", "lender_reviewing", "terms_received", "term_sheet_received", "executed_ts_in_closing", "closed", "unresponsive", "willing_to_negotiate", "lender_passed", "archived", "carve_out", "carve_out_closed" lender_status: "origination", "new", "lead_qualification", "quotation", "negotiation", "offer", "term_sheet", "good_faith_deposit", "diligence", "in_closing", "closed", "archived" visibility: "hidden", "masked", "shared" Identity filters (exact match, no range): contact_id: numeric ID of a lender contact (use list_contacts or get_contact to find it). private_company_id: numeric ID of a lender company (use list_companies to find it). Timestamp range filters (ISO 8601 datetime with explicit timezone — trailing Z or ±HH:MM — e.g. 2026-04-01T00:00:00Z; date-only and naive local forms are not guaranteed to parse): min_created_at / max_created_at: inclusive bounds on created_at (gte / lte). min_updated_at / max_updated_at: inclusive bounds on updated_at (gte / lte). sort: pass a bare key for ascending; prefix with a minus sign for descending (e.g. -score). Defaults to descending created_at. Valid keys (must match exactly — see lev-backend PLACEMENT_SORTABLE_FIELDS): "status", "score", "created_at", "updated_at" Pagination: - Default (sort set): offset mode. Walk pages with offset=N using the value surfaced in the previous response's hint. - Stable paging for bulk sync: pass sort=None; walk pages with cursor=<value> from the hint. cursor and sort cannot be combined — passing both returns a 400.
list_team_members
ChatGPTList active team members in your Lev account. Returns each member's name, email, role, and title. /account/team uses offset pagination — walk subsequent pages with the offset value from the returned hint.
list_term_sheets
ChatGPTList term sheets (loan quotes from lenders) for a specific deal. Shows each lender's quote summary: lender name, rate, loan amount, and status. Use get_term_sheet for the full breakdown including fees, recourse, extensions, and prepayment terms. Pagination: uses cursor pagination. To walk beyond the first page, pass the cursor value surfaced in the previous response's hint.
move_deal_to_pipeline_status
ChatGPTMove a deal to a specific status in a pipeline. Use list_pipelines first to see available pipelines and their statuses (with IDs). Example: move a deal to "Under Review" in the Financing pipeline.
read_deal_document_bytes
ChatGPTStream an uploaded document's bytes in chunks for the inline viewer widget. Internal plumbing for the vault-browser widget — the model should not call this directly. Given a document's identifiers, the server re-resolves its short-lived signed download link (re-applying the caller's authorization on every call) and returns the requested byte range. The widget loops this, advancing offset until has_more is false, then renders or downloads the assembled bytes. Identifiers (not a URL) are passed in so no signed link is ever exposed to the client and the fetch target can't be tampered with; streaming through the server keeps the browser off S3 (no CORS) and bounds each response. deal_id, document_id: identify the document (same as get_deal_document). offset: 0-based byte offset to start at. byte_count: bytes to read this call (clamped to 512KB). The response carries total_bytes and has_more so the widget knows when to stop.
read_memo_pdf_bytes
ChatGPTStream a memo PDF's bytes in chunks for the inline viewer widget. Internal plumbing for the memo-view widget — the model should not call this directly. Given the same identifiers as get_deal_memo, the server re-resolves the memo's signed PDF link (re-applying the caller's authorization on every call) and returns the requested byte range. The widget loops this, advancing offset until has_more is false, then renders the assembled bytes with PDF.js. Identifiers (not a URL) are passed in so no signed link is ever exposed to the client and the fetch target can't be tampered with. Streaming through the server keeps the browser off S3 (no CORS) and bounds each response. deal_id, memo_id, quality: identify the memo (same as get_deal_memo). offset: 0-based byte offset to start at. byte_count: bytes to read this call (clamped to 512KB). The response carries total_bytes and has_more so the widget knows when to stop.
record_deal_facts
ChatGPTAdd a metric value the deal doesn't have yet — record one or more user-supplied or sourced values. Use this to record a measured or extracted underwriting fact — NOI, occupancy, expenses, or source-reported valuation/purchase price — that the deal does not have yet, or to log an additional sourced observation for one it does. Discover the metric and its id with list_deal_index_metric_definitions. To change or correct a fact the deal already shows (e.g. 'fix the NOI to $1.25M'), don't use this tool — use search_deal_index to find it, then update_deal_index_fact, which sets the canonical value directly. Reach for this tool to add a value, not to edit one in place. Route by the catalog's suggested_write_via before recording here: index → this tool; deal_record → the named CRM tool (update_deal / update_company / update_contact), NOT this one; clarify (loan amount and deal financials are common cases) → ask whether the user is setting the deal's official value (CRM tool) or logging what a source reported (here). Don't guess. How recording behaves: think of the SOT as a fact slot keyed by metric_id + entity_ref. This logs a new observation into that slot; it does not erase prior observations or create a separate fact slot for the same metric/entity pair. Repeating the same metric/entity keeps the same sot_id and creates a new observation_id. The displayed value follows Lev's promotion logic, so it may not change after recording succeeds. Always confirm the concrete change with the user in plain English first. This does not create entities. To record a value for a property the deal doesn't have yet, add the property via update_deal (add_properties) first, then record against its entity_ref. values: a list of values to record, each an object with: metric_id: numeric id from list_deal_index_metric_definitions. value: the value to record, as text (e.g. 1250000, or 92%). entity_ref: optional. Target an existing entity, such as property:702 or sponsor_contact:55, taken from the catalog or a search_deal_index result. Omit for deal-level facts; on a single-property deal Lev can infer it. group_ref: optional. Use only to group several values for the same entity when no entity_ref is available yet. Each value is recorded independently: the response reports how many were recorded and surfaces any the backend rejected, so report the true outcome — never claim a value was saved if it appears under failed.
search_deal_index
ChatGPTSearch indexed canonical deal facts, metrics, and source provenance. Prefer this after resolving a deal when the user asks what Lev knows about any specific deal fact, underwriting topic, metric, missing field, or source document. This is broader than in-place NOI: use it for loan terms, proceeds, LTV/LTC, DSCR, debt yield, occupancy, rent roll, revenue/expenses, valuation, capex, property facts, sponsor or borrower background, dates, and other indexed deal context. This is semantic retrieval over Metric SOT / Index data. It is not an exhaustive full Index export and it is not a replacement for get_deal. Use get_deal for structured fields and broad deal detail; use this tool to enrich an answer to a specific question with canonical indexed facts and source provenance. Do not conclude that source documents are unavailable just because get_deal does not list files — inspect this tool's source metadata first. context: required natural-language search phrase. Be specific about the metric or topic you want. For broad discovery, use a context like: key underwriting facts, loan terms, property facts, financial metrics, sponsor, valuation, rent roll, occupancy, capex, and source documents. min_score: similarity threshold from 0 to 1. Defaults to 0.3; raise it for narrower matches, lower it only when recall matters more than precision. limit: maximum ranked facts to return. Defaults to 20 and is clamped by Lev to 1-200. For broad fact-discovery workflows (e.g., to give the user a more complete editable picture), set min_score=0 and limit=200. This is high-recall, not exhaustive: Lev clamps to 200 results per call, so for deals within that cap the result set is deterministic across differently-phrased contexts (with min_score=0, context becomes ranking-only); for deals over the cap, context-driven ranking selects which 200 you see. If `total or has_more in the response indicates more facts exist, treat the result as the top page, not the full inventory. Beware: high-recall responses can be large (tens of KB on deals with rich indexed data). For deeper inspection of any one fact, follow up with list_deal_index_observations on its sot_id. include_signed_urls: default false. When true, document-backed source records may include short-lived signed attachment URLs for inspection or download. The MCP response converts returned signed URLs into source.file_link Markdown hyperlinks and omits raw source.signed_url. Keep false when the user asks only what the source is; summarize file_name, pages, source.type, and observation_count instead. Set true only when the user asks for links/downloads or wants to open or inspect the source document. Never show a bare signed URL in the final user answer; present only the file name as a hyperlink. If signing is requested but cannot produce a link, source.file_link is absent and source.signed_url_error explains the typed reason. Results include ranked metric/value fields, context and summary text, score, and source metadata such as document id/type, file name, page range, and observation count. source.type=user_input means the indexed fact came from user-entered Lev data rather than an attachment; say no document-backed indexed facts were found when every relevant result is user_input. Treat ids as reasoning context unless the user asks for them, and use source.file_link only as a presentation-ready file-name hyperlink when the user asks for document access. Each result also carries an editable boolean (true / false / null) as a hint for whether the fact can be changed via update_deal_index_fact. It is a hint only; the backend re-checks at update time and rejects non-editable facts with a clear message. Use it to decide whether to offer an update_deal_index_fact option, never as a security boundary or as a verdict on other CRM update paths. The sot_id` field on each result is the fact identifier you pass back to list_deal_index_observations and update_deal_index_fact — internal, never surface it to the user. Critical scope clarification on editable=false: this flag covers ONLY update_deal_index_fact. Many facts with editable=false remain updatable via the standard CRM tools — do NOT tell the user a fact is read-only based on editable=false alone. When the user asks what they can change about a deal, consider these alternative update paths before concluding anything is unsupported: - Deal-level fields (loan_amount, business_plan, transaction_type, loan_type, title, description, estimated_close_date, sponsor_private_company_id) → update_deal. - Sponsor profile facts (Sponsor Name, Sponsor State, Website) → update_company on the linked company_id, resolved from deal.sponsor_private_company_id (get_deal returns it). - Broker profile facts (broker company name, state, website) → update_company on the broker's company. Resolve the company_id by looking up the broker contact via list_contacts (filter by email, or first_name/last_name from the deal-principal row in get_deal's team array — list_contacts does not accept a generic "name" arg). If the list_contacts summary does not carry the company_id, call get_contact on the matched contact_id and read the company_id off the full record. - Broker team / contact facts (broker team email, contact name, title, address) → update_contact. Resolve the contact_id via list_contacts (filter by email, or first_name/last_name from the get_deal team row — list_contacts does not accept a generic "name" arg); do not pass the team row's id or user_id directly to update_contact — those are different identifiers. A small set of editable=false facts has no alternate standard CRM write path today: property address and other property-canonical fields (no update_property tool — when editable=true, update_deal_index_fact can still override them as user_input observations), sponsor financials (Net Worth, Liquidity, Portfolio Value, Units Owned), contact phone, and broker_profile Additional States. Treat "no standard CRM path" as a conclusion you reach AFTER considering the alternatives, not a default reading of editable=false. Before calling any update tool, confirm the concrete change with the user — write-safety guidance on the individual update tools still applies.
show_deal
ChatGPTShow one deal visually — an interactive overview card of its headline terms, property, status, and key facts. Use when the user wants to see a deal: "show me the deal", "pull up deal X", "open the deal card / overview / dashboard". On MCP Apps hosts (Claude Desktop) this renders an inline card with the deal's headline (amount, loan type, transaction), property, pipeline stage, key financials, deal team, and its indexed facts grouped by category with source-document tags. Other hosts get a compact text summary. Use this ONLY for a visual look at one deal. For a written summary or narrative overview, use summarize_deal. For raw CRM fields, use get_deal. For one specific metric or where a number came from, use search_deal_index. Resolve a deal name to its deal_id with list_deals first.
summarize_deal
ChatGPTNarrative deal snapshot at three detail levels. Consolidates get_deal + search_deal_index + list_term_sheets + list_placements into one call, producing prose suited for chat surfaces (Claude Desktop, ChatGPT, Cursor) where the user wants a coherent answer to "tell me about deal X" without seeing raw structured shapes. detail: one of "brief" — 1-paragraph headline (~80 words) "standard" — ~200 words: title, property, financials, loan request, activity, open items (default) "memo" — ~600 words, lender-shareable 1-pager structure Every fact-derived quantitative claim cites its source via search_deal_index's source.file_name. Facts without a document-backed observation render as "(user input)". Use get_deal for raw CRM detail, search_deal_index for single-metric lookups, list_term_sheets / list_placements for full lists — this tool's structuredContent intentionally omits those lists so calls stay cheap. Resilient to partial backend failures: if term-sheet or placement data is unavailable, the summary still renders from deal + facts with a graceful degradation note. If the deal endpoint itself fails, returns a friendly error. Never fabricate URLs — only emit links present in structuredContent.urls.
summarize_pipeline
ChatGPTNarrative pipeline snapshot at three detail levels. Aggregates pipeline stages + per-stage deal counts + total loan amounts + highlighted deals (stuck longest, closing soon) into one call, producing prose suited for chat surfaces where the user wants a book-level read without paging through list_deals per stage. pipeline_id: optional. When omitted, the tool picks the lowest-order pipeline (matches the Lev UI ordering — for most brokers that's the Financing pipeline). If the account has multiple pipelines, the chosen one is announced in the output so the user can rerun with an explicit id to target a different pipeline. detail: one of "brief" — 1-paragraph headline (~80 words) "standard" — ~200 words: totals, stage distribution, stuck longest, closing soon, open items (default) "memo" — ~600 words, full per-stage breakdown with top 5 deals per stage "Stuck longest" highlights use deal.updated_at as a proxy for in-stage time. Caveat: deal.updated_at advances on any field change, not just stage transitions, so a deal that's been in a stage for months but had its title edited yesterday reads as fresh. The accurate signal (pipeline-row updated_at) requires per-deal fetches we skip in v1. "Closing soon" highlights are drawn from the same per-stage top-page sample used for stuck-longest. On stages with more than 50 deals, a deal with an imminent close date but a fresher updated_at can fall outside the sample — the standard view flags this in Open items. Use get_pipeline for raw status definitions, list_deals for filterable per-stage queries, this tool for narrative synthesis.
summarize_term_sheets
ChatGPTNarrative comparison of all term sheets on a deal at three detail levels. Aggregates list_term_sheets + get_deal (for context) + best-effort placement → company lookups (for lender labels) into one call, producing prose suited for chat surfaces where the broker is deciding which quote to take. Computes two "leaders" — lowest total_rate, and lowest all-in cost (rate + origination_fee / term_years) — and surfaces tradeoffs across rate, fees, and term. deal_id: required. The deal whose term sheets to compare. Use list_deals or get_deal to discover the id; use list_term_sheets to confirm a deal has at least one quote. detail: one of "brief" — 1-paragraph headline (~80 words): N quotes, leaders, key tradeoff "standard" — ~250 words: summary, leaders, 5-column comparison table, tradeoffs, open items (default) "memo" — ~600 words, full per-quote detail blocks with every surfaced field Quote labels resolve via the placement → private_company → name chain when available; fall back to a cleaned ts.title (brokers hand-type the lender into the title) when the chain fails. Edge cases: - No term sheets on the deal → friendly error suggesting create_term_sheet / list_placements - Single term sheet → mini-summary instead of a 1-row comparison ("Only one quote on file — consider sourcing more") - Mezz / second-lien quotes → flagged in Open items; v1 treats every quote as a standalone senior in the comparison Resilient to partial backend failures: degrades lender resolution per TS when placements or companies fail; surfaces a degraded flag in structuredContent. Re-raises only when list_term_sheets itself fails. On busy deals the backend term-sheet endpoint paginates; this tool walks pages up to a safety cap and exposes `total_on_deal plus a partial flag in structuredContent. The renderer marks leaders as computed over the fetched subset when partial=true` so the agent doesn't claim a global winner from a prefix.
switch_account
ChatGPTSwitch to a different Lev account for subsequent operations. account: the target account's name or slug. Matching priority: exact slug, exact name (case-insensitive), then case-insensitive name substring. If the user says switch to Acme, pass Acme — you do not need to know the slug. Call list_available_accounts first if unsure what's available. Persistence: the chosen account is stored server-side on the user's preferences and picked up automatically on every subsequent tool call, across workers and across sessions — no need to re-issue `switch_account each time you reconnect. Concurrent sessions: the active account is stored once per user, not per session. If the same user is using MCP from multiple clients at the same time (e.g. Claude Desktop and a CLI agent), a switch_account` call in one affects the others — their subsequent tool calls will silently follow. Single-session is the common case; flag to the user if they seem to expect per-session isolation. "No match" semantics: this tool only sees the accounts currently listed in the caller's available-accounts response. A "no match" message can mean either a typo or that the user is no longer a member of the named account (access revoked, account deleted, etc.). When you surface the message to the user, frame it as "I couldn't find that account in your available list" rather than "that account doesn't exist" — the two are indistinguishable from here.
update_company
ChatGPTUpdate fields on an existing company. Only send fields you want to change. Note: org_id is not updatable via this endpoint — the parent-organization link is set at creation time only.
update_contact
ChatGPTUpdate fields on an existing contact. Only send fields you want to change. Note: is_primary, contact_type, and company_id are intentionally not updatable here — toggling is_primary requires backend business logic (unset the previous primary) and the other two reshape the contact's identity. These remain tracked as F-016 for a future release.
update_deal
ChatGPTUpdate fields on an existing deal. Only send fields you want to change. Enum values (must match exactly — see lev-backend/app/models/enums.py): loan_type: "construction", "heavy_bridge", "light_bridge", "permanent", "land", "predevelopment", "tbd" transaction_type: "acquisition", "refinance", "new_construction", "tbd" business_plan: "stabilized", "value_add", "construction", "land" estimated_close_date: ISO 8601 date string (e.g. 2026-06-01) sponsor_private_company_id: link (or re-link) the deal to a sponsor company. Must be an existing PrivateCompany in the caller's account — cross-account IDs are rejected. Pass an int to link; omit to leave the current linkage unchanged.
update_deal_index_fact
ChatGPTChange a deal fact to a new value. Use when the user asks to change, correct, or override a deal fact. They will say things like 'update NOI to $1.25M', 'fix the loan amount', 'this address is wrong' — describing the fact in plain English. They will not know the fact's internal identifier and you should never ask for one. Resolve it yourself from a prior search_deal_index result. Always confirm the proposed change with the user in plain English before calling this tool. There is no version history and the last write wins, so a wrong update can't be undone in v1. Provide exactly one of value or observation_id: value: a free-text replacement, e.g. 1250000. Use when the user dictates a number that isn't already extracted from a document on the deal. Be careful — this strips provenance, so the fact will no longer be tied to a source document. observation_id: numeric id of an extracted value returned by list_deal_index_observations. Use when the user is choosing among values we already pulled from documents. Preferred over free-text value because it preserves the source document, page range, and confidence score. Internal — never surface to the user. sot_id: the fact identifier carried by a prior search_deal_index result (e.g. d:5362, a:123:property:690). Identifies which fact to update. Internal — never surface to the user. Editability is enforced server-side; some facts aren't user- editable and the request will be rejected with a clear message even if a prior search_deal_index result hinted editable=true. When that happens, tell the user plainly that the fact can't be edited via this surface — never expose the underlying check or say 'MetricDefinition'. On success, report the new value in plain English and name the source document (if any), not its id.
update_note
ChatGPTEdit the text of a free-text note on a contact, deal, placement, or company — or a comment on a checklist task. Use for human-written commentary like call logs or relationship context. To change a structured deal fact (NOI, loan amount, where a number came from), use update_deal_index_fact instead — notes are not indexed facts. parent_type: "contact", "deal", "placement", "company", or "checklist_task" — which kind of record owns the note parent_id: numeric ID of the contact, deal, placement, company, or checklist task note_id: numeric ID of the note to edit text: replacement note content (required, non-empty). Simple HTML formatting is preserved; scripts and other unsafe markup are stripped. You can only edit a note you created and still have parent write access to; another user's note or a note on another parent returns not found.
update_placement
ChatGPTUpdate an existing placement. Only send fields you want to change. Use to advance a placement through its lifecycle (set status as the lender responds), adjust visibility, or record a per-lender note. Enum values (must match exactly — see lev-backend/app/models/enums.py): status: "new", "sent", "lender_reviewing", "terms_received", "term_sheet_received", "executed_ts_in_closing", "closed", "unresponsive", "willing_to_negotiate", "lender_passed", "archived", "carve_out", "carve_out_closed" visibility: "hidden", "masked", "shared" lender_status: "origination", "new", "lead_qualification", "quotation", "negotiation", "offer", "term_sheet", "good_faith_deposit", "diligence", "in_closing", "closed", "archived" Archiving (removing) a placement: set status="archived". This is the only way to remove a placement — there is no DELETE verb. Archiving fully removes the row from active views (it becomes invisible to GET / list, the linked contact is cleared, and document access is revoked). Other PATCH fields sent alongside (e.g. a final description) still apply. To "move" a placement to a different lender or contact, archive it and create a new one. Not updatable here: deal_id, private_company_id, contact_id are set at create-time and cannot be reassigned. Archive the placement and recreate it with the desired linkage instead.
update_term_sheet
ChatGPTUpdate a term sheet. Only send fields you want to change. Use when a lender revises a quote (rate moved, term changed, recourse renegotiated) or to mark the winning quote on a deal. Enum values (must match exactly — see lev-backend/app/models/enums.py): quote_type: "guidance", "indication", "soft_quote", "hard_quote", "term_sheet" rate_type: "fixed", "floating" recourse: "personal_recourse", "fund_corporate_recourse", "non_recourse" recourse_type: "full", "partial", "burn_off" floor_type: "base_rate", "total_rate" prepayment_penalty: "step_down", "defeasance", "minimum_interest", "yield_maintenance", "minimum_multiple", "swap_breakage", "no_prepayment_penalty", "no_ability_to_prepay", "flat_fee", "other" payment_method: "accrued", "partial", "current_pay" capital_source_type: "balance_sheet", "agency", "cmbs_clo", "warehouse", "sba" base_rate: BaseRateNames member name (e.g. "treasury_y5") OR the matching display value (e.g. "Treasury 5-Yr"). Use "none-fixed" for fixed-rate quotes that have no underlying base rate. Not updatable here: placement_id and deal_id are set at create-time and cannot be reassigned. To move a term sheet to a different placement, delete it (delete_term_sheet) and create a new one. Visibility caveat: setting quote_type or is_visible_to_borrower to a value the read endpoint filters out will make the term sheet disappear from subsequent get_term_sheet / list_term_sheets results. The update succeeded — re-check the quote_type / is_visible_to_borrower values if the row vanishes.