Create the integration
A firm admin creates an API integration in Firm -> Apps & API access and chooses the least scopes required.
Developer docs
Use firm-owned API integrations to create cases, add parties, and send onboarding invites from another system with auditable, least-privilege access.
Reference for coding tools
Stable URLs, schemas, examples, and guardrails.
Quickstart
The integration flow is intentionally small: create credentials, request a scoped token, then call only the endpoints enabled by the firm.
A firm admin creates an API integration in Firm -> Apps & API access and chooses the least scopes required.
Use the returned appId as clientId and store the one-time clientSecret in a backend secret manager.
Exchange client credentials for a short-lived MicroApp system token.
Create cases, add parties, generate the Client Care Letter when configured, and then send invites with Authorization: Bearer <accessToken>.
Browser proxy
/api/v1
Local API
http://localhost:8081/v1
Production
Use relative /api/v1 from browser contexts
MCP
Use /api/mcp from connector clients
Authentication
Secrets never belong in browser code. This is a POST-only server-side call: exchange credentials from your backend for a MicroApp system token, then send the bearer token with each scoped API call.
curl -X POST "$VERIFY_CLIENT_BASE_URL/api/v1/microapps/system-tokens" \
-H "Content-Type: application/json" \
-d '{
"clientId": "00000000-0000-0000-0000-000000000000",
"clientSecret": "$VERIFY_CLIENT_CLIENT_SECRET",
"scopes": ["cases.create", "parties.manage", "client-care-letters.generate", "invites.send"],
"expiresInSeconds": 3600
}'curl -X POST "$VERIFY_CLIENT_BASE_URL/api/v1/cases" \
-H "Authorization: Bearer $VERIFY_CLIENT_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"matterRef": "MAT-2026-001",
"matterType": "Purchase",
"parties": [
{
"name": "Alex Client",
"email": "alex@example.com",
"role": "Buyer"
}
]
}'The clientSecret is shown only when created or rotated.
Requested scopes are intersected with firm-enabled scopes.
allowedIpRanges are enforced at token issuance and API use.
MCP
Use the Streamable HTTP MCP endpoint when ChatGPT, Codex, Claude, or another MCP client should act as the signed-in user rather than as a headless API integration.
codex mcp add verify-client --url "$VERIFY_CLIENT_BASE_URL/api/mcp"
codex mcp login verify-clientclaude mcp add --transport http verify-client "$VERIFY_CLIENT_BASE_URL/api/mcp"
claude mcp login verify-clientRegister the MCP URL without a static Authorization header when you want the client to open the OAuth sign-in flow. The server publishes protected resource metadata, sends a WWW-Authenticate challenge on unauthenticated MCP requests, and advertises OAuth security schemes on protected tools.
GET /.well-known/oauth-protected-resource/mcp
GET /api/mcp
WWW-Authenticate: Bearer resource_metadata="https://verify.example/.well-known/oauth-protected-resource/mcp", scope="vc.mcp.read"Endpoint reference
These are the endpoints currently documented for server-to-server onboarding. Do not invent endpoints; inspect implementation before using anything outside this list.
Exchanges an API integration clientId and clientSecret for a scoped JWT.
{
"clientId": "00000000-0000-0000-0000-000000000000",
"clientSecret": "stored-secret",
"scopes": ["cases.create", "parties.manage", "client-care-letters.generate", "invites.send"],
"expiresInSeconds": 3600
}{
"ok": true,
"accessToken": "<jwt>",
"principal": {
"principalType": "microApp",
"tokenKind": "system",
"appId": "00000000-0000-0000-0000-000000000000",
"orgId": "org_123",
"identifier": "case-management-system",
"scopes": ["cases.create", "parties.manage", "client-care-letters.generate", "invites.send"]
},
"expiresAt": 1770000000
}Creates an onboarding matter and initial parties. If no primary client is supplied, the first party becomes the primary client automatically; payloads may still mark exactly one party with isPrimaryContact to override that default. Use tenure plus sale/purchase addresses where needed so the right pack can be selected. Optional externalQuote links must be HTTPS, money values are integer minor units, and supplied net/VAT/gross totals must be consistent. Source of funds is only applied where the matter type supports it, so Sale matters omit it even if requireSof is true.
{
"matterRef": "MAT-2026-001",
"matterType": "Sale & Purchase",
"tenure": "leasehold",
"propertyAddress": {
"line1": "1 Purchase Street",
"city": "London",
"postcode": "SW1A 1AA"
},
"purchasePropertyAddress": {
"line1": "1 Purchase Street",
"city": "London",
"postcode": "SW1A 1AA"
},
"salePropertyAddress": {
"line1": "9 Sale Road",
"city": "London",
"postcode": "SW1A 2AA"
},
"externalQuote": {
"provider": "gc-crm",
"quoteReference": "Q-123",
"quoteUrl": "https://crm.example/quotes/Q-123",
"currency": "GBP",
"totalNetMinor": 100000,
"totalVatMinor": 20000,
"totalAmountMinor": 120000,
"lines": [
{
"group": "legal_fee",
"label": "Legal fee",
"netAmountMinor": 95000,
"vatAmountMinor": 19000,
"vatable": true,
"sort": 1
},
{
"group": "disbursement",
"label": "HM Land Registry fee",
"description": "Estimated registration fee",
"netAmountMinor": 5000,
"vatAmountMinor": 0,
"vatable": false,
"sort": 2
}
],
"depositAmountMinor": 25000,
"depositPaid": true
},
"requireSof": true,
"parties": [
{
"name": "Alex Client",
"email": "alex@example.com",
"mobile": "+447700900123",
"role": "Buyer",
"partyType": "individual",
"isPrimaryContact": true
}
]
}{
"caseId": "case_opaque_id",
"status": "NotStarted",
"parties": [
{
"partyId": "party_opaque_id",
"name": "Alex Client",
"clientAccessStatus": "not_sent"
}
]
}Issues a presigned upload URL for a third-party quote or Terms of Business PDF. Upload the PDF, then save the returned key on the case externalQuote.
{
"filename": "Q-123.pdf",
"contentType": "application/pdf",
"kind": "quote"
}{
"uploadUrl": "https://...",
"key": "org/org_123/case/case_123/quotes/quote/Q-123.pdf",
"expiresIn": 900
}Saves hosted quote links, uploaded quote/Terms PDF keys, quote line breakdowns, total quote amount, and deposit paid values without granting broad case update access. Hosted links must be HTTPS and PDF keys must be returned by the presign endpoint.
{
"externalQuote": {
"provider": "gc-crm",
"quoteReference": "Q-123",
"quotePdfKey": "org/org_123/case/case_123/quotes/quote/Q-123.pdf",
"termsPdfKey": "org/org_123/case/case_123/quotes/terms/terms.pdf",
"totalNetMinor": 100000,
"totalVatMinor": 20000,
"totalAmountMinor": 120000,
"lines": [
{
"group": "legal_fee",
"label": "Legal fee",
"netAmountMinor": 95000,
"vatAmountMinor": 19000,
"vatable": true,
"sort": 1
}
],
"depositAmountMinor": 25000,
"depositPaid": true
}
}{
"case": {
"caseId": "case_opaque_id",
"matterRef": "MAT-2026-001"
}
}Returns paginated opaque case IDs for remote ledger sync and duplicate-check connectivity.
GET /api/v1/microapps/api/cases/ids?limit=200&cursor=<nextCursor>
Authorization: Bearer <system-token>{
"ok": true,
"items": ["case_opaque_id"],
"nextCursor": null
}Adds an individual or company party to an existing case. Mobile is optional, but any supplied phone number must be valid and can use UK or international format.
{
"name": "Jordan Client",
"email": "jordan@example.com",
"mobile": "+447700900456",
"role": "Seller",
"partyType": "individual"
}{
"partyId": "party_opaque_id",
"name": "Jordan Client",
"email": "jordan@example.com",
"role": "Seller",
"clientAccessStatus": "not_sent"
}Updates contact, role, or company details for an existing party. Email must remain unique within the case; supplied mobile numbers are validated and normalized.
{
"name": "Jordan Client",
"email": "jordan.updated@example.com",
"mobile": "+447700900456",
"role": "Seller"
}{
"partyId": "party_opaque_id",
"name": "Jordan Client",
"email": "jordan.updated@example.com",
"mobile": "+447700900456",
"role": "Seller",
"clientAccessStatus": "not_sent"
}Marks exactly one party as the matter's primary client. Use this when the first-party default is not the right addressee for Client Care Letters or invite readiness.
{
"partyId": "party_opaque_id"
}{
"ok": true,
"party": {
"partyId": "party_opaque_id",
"name": "Alex Client",
"isPrimaryContact": true
}
}Returns whether a Client Care Letter is required, whether a generated template is configured, and what matter data is missing before generation.
GET /api/v1/cases/{caseId}/client-care-letter/status
Authorization: Bearer <system-token>{
"required": true,
"mode": "generated",
"readinessStatus": "ready_to_generate",
"templateConfigured": true,
"templateName": "Purchase Client Care Letter",
"document": null,
"missingFields": []
}Renders the configured matter-type or firm default template from stored matter data and firm defaults, then saves an invite-ready HTML document.
{}{
"ok": true,
"document": {
"documentId": "document_opaque_id",
"type": "care_letter",
"title": "Purchase Client Care Letter",
"source": { "kind": "template" }
},
"status": {
"readinessStatus": "ready",
"document": {
"documentId": "document_opaque_id",
"sourceKind": "template"
}
}
}Returns a short-lived download URL for an immutable ZIP snapshot. Every entry is at the ZIP root, with category and party context kept in readable filenames.
GET /api/v1/cases/{caseId}/evidence-packs/{packId}/zip
Authorization: Bearer <token>{
"packId": "pack_opaque_id",
"downloadUrl": "https://...",
"expiresIn": 900,
"filename": "ABC123 - evidence pack.zip",
"fileCount": 8
}Sends a client onboarding invite by email, SMS, or WhatsApp according to party details. If a generated Client Care Letter is required, generate it first or include autoGenerateClientCareLetter with the CCL scope so the API can complete the workflow.
{
"partyId": "party_opaque_id",
"autoGenerateClientCareLetter": true,
"sendSms": false
}{
"ok": true,
"inviteId": "invite_opaque_id",
"channel": "email"
}Duplicate safeguards
Use a stable source-system matterRef for every case. Verify Client rejects duplicate matter references within the firm, and integrations should keep a local ledger plus a remote sync check where required.
Scopes and schemas
Use the smallest scope set possible. Treat all identifiers as opaque strings and persist only the IDs needed to continue a workflow.
| Scope | Purpose | Endpoint |
|---|---|---|
| cases.create | Create onboarding cases | POST /api/v1/cases |
| parties.manage | Add/edit parties and choose the primary client | POST/PATCH /api/v1/cases/{caseId}/parties, PUT /api/v1/cases/{caseId}/primary-client |
| client-care-letters.generate | Generate configured Client Care Letters | POST /api/v1/cases/{caseId}/client-care-letter/generate |
| invites.send | Send onboarding invites | POST /api/v1/cases/{caseId}/invites |
| cases.ids.list | List case IDs | GET /api/v1/microapps/api/cases/ids |
| users.list | List firm users | GET /api/v1/microapps/api/users |
Errors and audit
Integrations should branch on HTTP status first. Error body fields are useful but should be treated as optional.
Case, party, and invite actions performed by system tokens are written as MicroApp audit actors with the integration appId and identifier.
{
"actorType": "microApp",
"actorId": "<appId>",
"meta": {
"actorTokenKind": "system",
"actorIdentifier": "<identifier>"
}
}AI files
AI coding tools should start with llms.txt, then use the OpenAPI slice for schemas. Humans can use the same files to verify what automation is supported.
Short AI entrypoint with source files, scopes, URLs, and guardrails.
Full workflow guide for tokens, case creation, generated CCLs, and invites.
Machine-readable endpoint, request, and response schemas.
Firm admins manage integrations from Apps & API access.