TitlPdfProcessor.Models
2.10.0
dotnet add package TitlPdfProcessor.Models --version 2.10.0
NuGet\Install-Package TitlPdfProcessor.Models -Version 2.10.0
<PackageReference Include="TitlPdfProcessor.Models" Version="2.10.0" />
<PackageVersion Include="TitlPdfProcessor.Models" Version="2.10.0" />
<PackageReference Include="TitlPdfProcessor.Models" />
paket add TitlPdfProcessor.Models --version 2.10.0
#r "nuget: TitlPdfProcessor.Models, 2.10.0"
#:package TitlPdfProcessor.Models@2.10.0
#addin nuget:?package=TitlPdfProcessor.Models&version=2.10.0
#tool nuget:?package=TitlPdfProcessor.Models&version=2.10.0
TITL PDF Processor - .NET SDK Models
C# class definitions for deserializing TITL PDF Processor API responses.
Version: 2.9
Last Updated: April 8, 2026
Installation
Copy TitlPdfProcessor.Models.cs into your project, or reference it directly.
Contract Testing
This SDK includes automated contract tests to ensure it stays in sync with the API.
# Run contract tests (requires .NET 8 SDK)
cd tests
./run-tests.sh
# Or manually
dotnet test
When making API changes, see SYNC_CHECKLIST.md for the update process.
Dependencies:
- .NET 6.0+ (or .NET Standard 2.0+)
System.Text.Json(built-in) orNewtonsoft.Json
Quick Start
using System.Net.Http;
using System.Text.Json;
using TitlPdfProcessor.Models;
public class PdfProcessorClient
{
private readonly HttpClient _client;
private readonly JsonSerializerOptions _jsonOptions;
public PdfProcessorClient(string apiKey)
{
_client = new HttpClient();
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
_client.BaseAddress = new Uri("https://pdf-processor-azyjo4e2za-uc.a.run.app");
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
}
public async Task<JobResponse> GetJobAsync(string jobId)
{
var response = await _client.GetAsync($"/api/v1/jobs/{jobId}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<JobResponse>(json, _jsonOptions);
}
}
Key Type Guarantees
The API now guarantees consistent types for all fields:
| Field | Type | Notes |
|---|---|---|
PropertyIdentifiers.Address |
string |
Multiple addresses joined with \| |
PropertyIdentifiers.ParcelNumbers |
List<string> |
May be empty, never null |
NormalizedFields |
NormalizedFields? |
Standardized party/date/address fields (null for TSS) |
NormalizedFields.AddressComponents |
AddressComponents? |
Parsed US components when Address is set (TITL-779) |
StructuredFields[*] |
string |
Most values are strings or null |
StructuredFields["easements"] |
EasementItem[] |
Array of easements (EASEMENT docs only) |
StructuredFields["covenants"] |
CovenantItem[] |
Array of covenants (RESTRICTIVE_COVENANT docs only) |
StructuredFields["additional_parcels"] |
ParcelItem[] |
Array of parcels (DEED docs with multiple properties) |
StructuredFields["schedule_a_coverage"] |
ScheduleAItem[] |
Schedule A coverage items (COMMITMENT_FOR_POLICY only) |
StructuredFields["schedule_b_exceptions"] |
ScheduleBExceptionItem[] |
Schedule B exceptions (COMMITMENT_FOR_POLICY only) |
Page-Reference Linking (January 2026)
For TITLE_SEARCH_SUMMARY documents, each detected section now includes a SourcePage property linking the extracted data to its original location in the PDF:
// Access sections with their source page numbers
if (dataSet.DataType == "TITLE_SEARCH_SUMMARY" && dataSet.DetectedSections != null)
{
foreach (var section in dataSet.DetectedSections)
{
// section.SourcePage can be "1" (single page) or "2-3" (range)
Console.WriteLine($"[Page {section.SourcePage}] {section.SectionName}");
// section.Data contains the extracted values for this section
if (section.Data != null)
{
foreach (var kvp in section.Data)
{
Console.WriteLine($" {kvp.Key}: {kvp.Value}");
}
}
}
}
This enables:
- Linking extracted fields back to their source page in the PDF viewer
- Auditing which page each piece of data came from
- Better user experience when reviewing extractions
Supported Document Types
The API extracts structured data from 19+ document types:
Core Legal Documents:
DEED- Warranty Deed, Quit Claim Deed, Deed of Trust, Open End Deed of TrustMORTGAGE- Mortgages, Loan DocumentsLIEN- Tax Liens, Judgment Liens, Mechanic's LiensEASEMENT- Utility, Access, Drainage, Conservation EasementsRESTRICTIVE_COVENANT- CC&Rs, HOA Covenants
Supporting Documents:
CONSENT- Assent to Execution of Deeds, Waiver of Marital RightsAPPOINTMENT- Appointment of Substitute/Successor TrusteeASSIGNMENT- Assignment of Rights/InterestsAFFIDAVIT- Heirship, Identity, Name Change AffidavitsAGREEMENT- Maintenance, Road, Easement Agreements
Property Documents:
PLAT- Plat Maps, Subdivision PlansSURVEY- Property SurveysTAX_STATEMENT- Tax Bills, Tax Receipts
Title & Policy Documents:
TITLE_SEARCH_SUMMARY- Title Search ReportsCOMMITMENT_FOR_POLICY- Title Commitments, ALTA Policies
Release & Satisfaction Documents:
RELEASE- Satisfaction of Mortgage, Full Reconveyance, Release of LienPARTIAL_RELEASE- Partial Release, Partial Reconveyance, Partial Satisfaction
Court & Estate Documents:
JUDGMENT- Court Judgments, Final JudgmentsLAST_WILL_AND_TESTAMENT- Wills, Testaments
Other:
CONTRACT- Real Estate ContractsORDINANCE- Municipal OrdinancesBILL_OF_SALE- Personal Property TransfersUCC- UCC Filings
Working with Property Identifiers
// Access is always safe - no need to check for arrays
var job = await client.GetJobAsync(jobId);
foreach (var dataSet in job.Results.StructuredDataSets)
{
if (dataSet.PropertyIdentifiers != null)
{
// Address is GUARANTEED to be a string
string address = dataSet.PropertyIdentifiers.Address;
// If you need individual addresses (when multiple exist)
string[] addresses = dataSet.PropertyIdentifiers.GetAddressArray();
// ParcelNumbers is GUARANTEED to be an array
foreach (var parcel in dataSet.PropertyIdentifiers.ParcelNumbers)
{
Console.WriteLine($"Parcel: {parcel}");
}
}
}
Working with Structured Fields
foreach (var dataSet in job.Results.StructuredDataSets)
{
switch (dataSet.DataType)
{
case "DEED":
// Use type-safe field name constants
var grantor = dataSet.GetField(StructuredFieldNames.GrantorName);
var grantee = dataSet.GetField(StructuredFieldNames.GranteeName);
var amount = dataSet.GetField(StructuredFieldNames.ConsiderationAmount);
Console.WriteLine($"Deed: {grantor} -> {grantee} for {amount}");
break;
case "MORTGAGE":
var borrower = dataSet.GetField(StructuredFieldNames.BorrowerName);
var lender = dataSet.GetField(StructuredFieldNames.LenderName);
var principal = dataSet.GetField(StructuredFieldNames.PrincipalAmount);
Console.WriteLine($"Mortgage: {borrower} from {lender} for {principal}");
break;
case "TITLE_SEARCH_SUMMARY":
// Title search summaries have different structure
foreach (var section in dataSet.DetectedSections ?? new())
{
Console.WriteLine($"Section: {section.SectionName}");
}
break;
}
}
Working with Easements and Covenants
Easement and Restrictive Covenant documents can contain multiple items. Use the helper extension methods to extract them:
foreach (var dataSet in job.Results.StructuredDataSets)
{
// Handle EASEMENT documents (may contain multiple easements)
if (dataSet.IsEasement())
{
var propertyOwner = dataSet.GetField(StructuredFieldNames.PropertyOwner);
Console.WriteLine($"Easement from: {propertyOwner}");
// Get all easements in this document
foreach (var easement in dataSet.GetEasements())
{
Console.WriteLine($" Type: {easement.EasementType}");
Console.WriteLine($" Holder: {easement.EasementHolder}");
Console.WriteLine($" Purpose: {easement.EasementPurpose}");
Console.WriteLine($" Location: {easement.EasementLocation}");
Console.WriteLine($" Width: {easement.EasementWidth}");
}
}
// Handle RESTRICTIVE_COVENANT documents (typically 10-50+ restrictions)
if (dataSet.IsRestrictiveCovenant())
{
var subdivision = dataSet.GetField(StructuredFieldNames.SubdivisionName);
Console.WriteLine($"Covenants for: {subdivision}");
// Get all covenants/restrictions in this document
foreach (var covenant in dataSet.GetCovenants())
{
Console.WriteLine($" [{covenant.CovenantType}] {covenant.Description}");
Console.WriteLine($" Applies to: {covenant.AppliesTo}");
Console.WriteLine($" Enforcement: {covenant.Enforcement}");
}
}
// Handle DEED documents (may have multiple parcels)
if (dataSet.IsDeed())
{
var grantor = dataSet.GetField(StructuredFieldNames.GrantorName);
var grantee = dataSet.GetField(StructuredFieldNames.GranteeName);
var deedType = dataSet.GetField(StructuredFieldNames.DeedType);
// For Deed of Trust, also extract trustee and lender
if (deedType?.Contains("trust", StringComparison.OrdinalIgnoreCase) == true)
{
var trustee = dataSet.GetField(StructuredFieldNames.TrusteeName);
var lender = dataSet.GetField(StructuredFieldNames.LenderName);
Console.WriteLine($"Deed of Trust: Borrower={grantor}, Trustee={trustee}, Lender={lender}");
}
// Get all parcels if deed covers multiple properties
foreach (var parcel in dataSet.GetParcels())
{
Console.WriteLine($" Parcel: {parcel.ParcelId}");
Console.WriteLine($" Address: {parcel.PropertyAddress}");
Console.WriteLine($" Legal: {parcel.LegalDescription}");
}
}
}
Working with Title Commitments (Schedule A/B)
Title commitment documents (COMMITMENT_FOR_POLICY) now return Schedule A and Schedule B as structured arrays instead of free-text blobs (TITL-212, February 2026).
foreach (var dataSet in job.Results.StructuredDataSets)
{
if (dataSet.IsCommitmentForPolicy())
{
var policyNumber = dataSet.GetField(StructuredFieldNames.PolicyNumber);
var insured = dataSet.GetField(StructuredFieldNames.InsuredName);
Console.WriteLine($"Commitment: {policyNumber} for {insured}");
// Schedule A - coverage terms (effective date, amount, insured, etc.)
foreach (var item in dataSet.GetScheduleACoverage())
{
Console.WriteLine($" Schedule A: {item.Label} = {item.Value}");
}
// Schedule B - exceptions (easements, liens, restrictions, etc.)
foreach (var exception in dataSet.GetScheduleBExceptions())
{
Console.WriteLine($" Schedule B [{exception.Type}]: {exception.Description}");
}
}
}
Document Types Quick Reference
| Type | Description | Array Fields |
|---|---|---|
DEED |
Warranty deed, quit claim, deed of trust, etc. | additional_parcels[] |
MORTGAGE |
Mortgages and loan documents | - |
LIEN |
Tax liens, judgment liens, mechanic's liens | - |
EASEMENT |
Utility, access, drainage easements | easements[] |
RESTRICTIVE_COVENANT |
HOA declarations, CC&Rs | covenants[] |
CONSENT |
Assent to deed, waiver of marital rights | - |
APPOINTMENT |
Appointment of substitute/successor trustee | - |
TITLE_SEARCH_SUMMARY |
Title search reports | - |
TAX_STATEMENT |
Property tax statements, receipts | - |
PLAT |
Recorded plat maps | - |
SURVEY |
Survey documents | - |
CONTRACT |
Real estate contracts | - |
AGREEMENT |
Maintenance, road agreements | - |
ASSIGNMENT |
Assignment of rights/interests | - |
AFFIDAVIT |
Heirship, identity affidavits | - |
UCC |
UCC financing statements | - |
BILL_OF_SALE |
Personal property transfers | - |
ORDINANCE |
Municipal ordinances | - |
COMMITMENT_FOR_POLICY |
Title commitments, ALTA policies | schedule_a_coverage[], schedule_b_exceptions[] |
JUDGMENT |
Court judgments, final judgments | - |
LAST_WILL_AND_TESTAMENT |
Wills, testaments | - |
RELEASE |
Satisfaction of mortgage, reconveyance | - |
PARTIAL_RELEASE |
Partial release of mortgage/lien | - |
UNKNOWN |
Unclassified documents | - |
Working with Address Analysis (v2.8+)
When a search package contains multiple documents, the API collects all property street addresses, filters out legal descriptions (lot/plat/subdivision references), and runs AI-powered comparison to determine if multiple distinct properties are present.
var job = await client.GetJobAsync(jobId);
if (job.Results?.AddressAnalysis != null)
{
var analysis = job.Results.AddressAnalysis;
// Quick check: does this package reference multiple properties?
if (analysis.MultipleAddressesDetected)
{
Console.WriteLine("⚠️ Multiple properties detected!");
}
// List all unique street addresses
foreach (var addr in analysis.UniqueAddresses)
{
Console.WriteLine($"Address: {addr.Address}");
foreach (var src in addr.Sources)
Console.WriteLine($" Found in: {src.DocumentType} (pages {src.SourcePages})");
}
// Pairwise comparison details (when multiple addresses exist)
if (analysis.Comparisons != null)
{
foreach (var cmp in analysis.Comparisons)
{
var status = cmp.IsSameProperty ? "same" : "DIFFERENT";
Console.WriteLine($" {cmp.Address1} vs {cmp.Address2} → {status}");
}
}
}
// Or use the convenience helpers:
if (job.Results.HasMultipleProperties())
{
var addresses = job.Results.GetUniqueAddresses();
Console.WriteLine($"Found {addresses.Count} distinct addresses");
}
Key behaviors:
- Legal descriptions (lot references, plat books, subdivision text) are excluded from analysis
MultipleAddressesDetectedis based on semantic comparison, not string equality- Different formatting of the same address (e.g., "26 Steeplechase Dr" vs "26 STEEPLECHASE DRIVE") resolves to
isSameProperty: true - The field is absent when no street addresses are extracted
Working with Normalized Fields (v2.7+)
Every structured extraction (except TITLE_SEARCH_SUMMARY) now includes a NormalizedFields object that provides standardized access to common fields regardless of document type:
foreach (var dataSet in job.Results.StructuredDataSets)
{
if (dataSet.NormalizedFields != null)
{
var nf = dataSet.NormalizedFields;
Console.WriteLine($"{nf.ContentType}: {nf.FirstParty} → {nf.SecondParty}");
Console.WriteLine($" Date: {nf.DocumentDate}, Recorded: {nf.RecordedDate}");
Console.WriteLine($" Address: {nf.Address}");
Console.WriteLine($" Amount: {nf.ConsiderationAmount}");
if (nf.TaxIds?.Count > 0)
Console.WriteLine($" Tax IDs: {string.Join(", ", nf.TaxIds)}");
if (nf.ReferencedInstruments?.Count > 0)
Console.WriteLine($" References: {nf.ReferencedInstruments.Count} instruments");
}
}
| Normalized Field | DEED | MORTGAGE | LIEN | TAX_STATEMENT | RELEASE |
|---|---|---|---|---|---|
FirstParty |
Grantor | Borrower | Property Owner | Property Owner | Releasing Party |
SecondParty |
Grantee | Lender | Lien Holder | - | Borrower |
ThirdParty |
Trustee | - | - | - | - |
DocumentDate |
Deed Date | Mortgage Date | Lien Date | Tax Year | Release Date |
ConsiderationAmount |
Amount | Principal | Lien Amount | - | - |
Polling for Completion
public async Task<JobResponse> WaitForCompletionAsync(string jobId, int maxWaitSeconds = 300)
{
var startTime = DateTime.UtcNow;
var delay = TimeSpan.FromSeconds(2);
while ((DateTime.UtcNow - startTime).TotalSeconds < maxWaitSeconds)
{
var job = await GetJobAsync(jobId);
if (job.Status == "COMPLETED" || job.Status == "FAILED")
{
return job;
}
await Task.Delay(delay);
// Exponential backoff up to 10 seconds
if (delay < TimeSpan.FromSeconds(10))
delay = TimeSpan.FromSeconds(delay.TotalSeconds * 1.5);
}
throw new TimeoutException($"Job {jobId} did not complete within {maxWaitSeconds} seconds");
}
Using with Newtonsoft.Json
If you prefer Newtonsoft.Json:
using Newtonsoft.Json;
var json = await response.Content.ReadAsStringAsync();
var job = JsonConvert.DeserializeObject<JobResponse>(json);
Complete Example
using System;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using TitlPdfProcessor.Models;
class Program
{
static async Task Main(string[] args)
{
var apiKey = "sk-your-api-key-here";
var client = new PdfProcessorClient(apiKey);
// Upload a PDF
var jobId = await client.UploadPdfAsync("document.pdf");
Console.WriteLine($"Job created: {jobId}");
// Wait for completion
var job = await client.WaitForCompletionAsync(jobId);
if (job.Status == "COMPLETED")
{
Console.WriteLine($"Found {job.Results.StructuredDataSets.Count} documents");
foreach (var dataSet in job.Results.StructuredDataSets)
{
Console.WriteLine($"\n--- {dataSet.DataType}: {dataSet.Title} ---");
Console.WriteLine($"Pages: {dataSet.SourcePages}");
Console.WriteLine($"Confidence: {dataSet.Confidence:P0}");
// Safe access to property identifiers
if (dataSet.PropertyIdentifiers != null)
{
Console.WriteLine($"Address: {dataSet.PropertyIdentifiers.Address}");
Console.WriteLine($"Parcels: {string.Join(", ", dataSet.PropertyIdentifiers.ParcelNumbers)}");
}
// Safe access to structured fields
if (dataSet.StructuredFields != null)
{
foreach (var (field, value) in dataSet.StructuredFields)
{
if (!string.IsNullOrEmpty(value))
Console.WriteLine($" {field}: {value}");
}
}
// For TITLE_SEARCH_SUMMARY: Access detected sections with page references
if (dataSet.DataType == "TITLE_SEARCH_SUMMARY" && dataSet.DetectedSections != null)
{
Console.WriteLine("\n Detected Sections:");
foreach (var section in dataSet.DetectedSections)
{
Console.WriteLine($" [{section.SourcePage}] {section.SectionName} ({section.SectionType})");
// section.Data contains the extracted key-value pairs for this section
}
}
}
}
else
{
Console.WriteLine($"Job failed: {job.Error}");
}
}
}
public class PdfProcessorClient
{
private readonly HttpClient _client;
private readonly JsonSerializerOptions _jsonOptions;
private const string BaseUrl = "https://pdf-processor-azyjo4e2za-uc.a.run.app";
public PdfProcessorClient(string apiKey)
{
_client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
}
public async Task<string> UploadPdfAsync(string filePath)
{
using var content = new MultipartFormDataContent();
using var fileStream = File.OpenRead(filePath);
using var fileContent = new StreamContent(fileStream);
content.Add(fileContent, "file", Path.GetFileName(filePath));
content.Add(new StringContent("true"), "enableDataExtraction");
content.Add(new StringContent("true"), "enableClassification");
var response = await _client.PostAsync("/api/v1/upload", content);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
return doc.RootElement.GetProperty("jobId").GetString();
}
public async Task<JobResponse> GetJobAsync(string jobId)
{
var response = await _client.GetAsync($"/api/v1/jobs/{jobId}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<JobResponse>(json, _jsonOptions);
}
public async Task<JobResponse> WaitForCompletionAsync(string jobId, int maxWaitSeconds = 300)
{
var startTime = DateTime.UtcNow;
var delay = TimeSpan.FromSeconds(2);
while ((DateTime.UtcNow - startTime).TotalSeconds < maxWaitSeconds)
{
var job = await GetJobAsync(jobId);
Console.WriteLine($"Status: {job.Status} ({job.Progress}%)");
if (job.Status == "COMPLETED" || job.Status == "FAILED")
return job;
await Task.Delay(delay);
if (delay < TimeSpan.FromSeconds(10))
delay = TimeSpan.FromSeconds(delay.TotalSeconds * 1.5);
}
throw new TimeoutException($"Job did not complete within {maxWaitSeconds} seconds");
}
}
Support
For questions or issues:
- Email: hello@titl.com
- API Documentation: https://pdf-processor-azyjo4e2za-uc.a.run.app/api
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Text.Json (>= 8.0.5)
-
net6.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v2.10.0 (April 2026)
- Added normalizedFields.address_components (AddressComponents) — structured US address fields derived server-side from address (TITL-779)
v2.9.0 (April 2026)
- Normalized JUDGEMENT → JUDGMENT as the canonical document type (TITL-606)
- Added IsJudgment() helper method (checks DataType == "JUDGMENT")
- Deprecated IsJudgement() with [Obsolete] attribute — still works for both spellings
- Updated sample responses and contract tests for JUDGMENT
v2.8.0 (April 2026)
- Added AddressAnalysis to JobResults for cross-document address comparison (TITL-598)
- AddressAnalysis includes: UniqueAddresses, MultipleAddressesDetected, Comparisons
- Legal descriptions (lot/plat/subdivision references) are now filtered from address analysis
- Added AddressSource, AddressDocumentSource, AddressComparison classes
- Added HasMultipleProperties() and GetUniqueAddresses() helper methods on JobResults
- Non-breaking: existing v2.7.0 consumers will ignore the new optional field
v2.7.0 (March 2026)
- Added NormalizedFields to all structured extractions (first_party, second_party, document_date, etc.)
- Added RELEASE document type (satisfaction of mortgage, full reconveyance, release of lien)
- Added IsRelease() helper method
- Added ReleaseExtraction interface with fields matching release.md prompt
- Aligned extraction interfaces with AI prompts:
- MORTGAGE: loan_amount → principal_amount
- LIEN: debtor_name → property_owner
- TAX_STATEMENT: fully restructured (owner_name → property_owner, parcel_number → parcel_id, etc.)
- Deprecated old field constants with [Obsolete] attributes (DebtorName, ParcelNumber, etc.)
- Added new StructuredFieldNames: PrincipalAmount, ParcelId, TotalAssessedValue, TotalTaxDue, etc.
v2.6.0 (February 2026)
- BREAKING: schedule_a_coverage and schedule_b_exceptions are now structured arrays (TITL-212)
- Added ScheduleAItem class (label, value) for Schedule A coverage terms
- Added ScheduleBExceptionItem class (description, type) for Schedule B exceptions
- Added GetScheduleACoverage() and GetScheduleBExceptions() helper methods
- Updated sample responses and contract tests
v2.5.0 (January 2026)
- Added PARTIAL_RELEASE, SECURITY_DEED, DEED_OF_TRUST, NOTICE_OF_COMPLETION document types
- Added ParcelItem class and GetParcels() helper for multi-parcel deeds
- Added JurisdictionInfo for state/county-aware processing
v2.3.0 (January 2026)
- Added COMMITMENT_FOR_POLICY document type (title commitments, ALTA policies)
- Added JUDGEMENT document type (court judgments)
- Added LAST_WILL_AND_TESTAMENT document type
- Fixed nullable TotalPages in PageNumberingContext
- Added contract tests for SDK validation
v2.2.0 (January 2026)
- Added CONSENT and APPOINTMENT document types
- Added sourcePage support for TITLE_SEARCH_SUMMARY sections
- Improved table extraction for large documents