Background API integration
Your backend creates or updates onboarding cases without a user launching an app.
Client credentials → system token → scoped API calls
Follow the API quickstartDeveloper docs
Use firm-owned API integrations to create cases, add parties, and send onboarding invites from another system with auditable, least-privilege access.
Production endpoints
Server-to-server API · JSON · Scoped access
MicroApp concept
A MicroApp is a firm-owned integration registration. It defines the app’s identity, permitted scopes and active status—not a requirement to build an embedded interface. Choose the flow that fits your work.
Your backend creates or updates onboarding cases without a user launching an app.
Client credentials → system token → scoped API calls
Follow the API quickstartA firm user opens your hosted web app from the firm portal. The app redeems a short-lived, one-time launch token.
Portal launch → token redemption → user-scoped API calls
Access is limited by both the app’s scopes and the launching user’s permissions. This is separate from the client-credentials quickstart below.
Using MCP? MCP connects compatible assistants through OAuth sign-in as a user, rather than the background client-credentials flow. Available tools remain limited by that user’s organisation and permissions.
Quickstart
Set up access once, then connect your case-management system. Start with the four steps below.
In Firm → Apps & API access, create an integration with only the scopes you need.
Save appId as clientId. Keep the one-time clientSecret in your backend secret manager.
Exchange credentials from your backend for a short-lived system token.
Use the bearer token to create a case, add parties and send onboarding invites.
export VERIFY_CLIENT_BASE_URL="https://verifyclient.co.uk"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.
# Requires jq and an exported VERIFY_CLIENT_CLIENT_SECRET.
jq -n '{
"clientId": "00000000-0000-0000-0000-000000000000",
"clientSecret": env.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/microapps/system-tokens" \
-H "Content-Type: application/json" \
--data-binary @-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://verifyclient.co.uk/.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. Optional referral details are supported: send referred, referrerName, amountMinor in GBP pence and feePayer (firm or client); for a direct enquiry send only referred: false. Omit referral to keep existing behaviour. This does not alter the quote or record consent. Later referral edits use the firm app; API integration tokens cannot PATCH matters.
{
"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 and short-lived authority for a third-party quote or Terms of Business PDF. Upload the PDF, then save the returned key and receipt on the matter.
{
"filename": "Q-123.pdf",
"contentType": "application/pdf",
"kind": "quote",
"checksumCRC32": "base64-encoded CRC32 of the PDF"
}{
"uploadUrl": "https://...",
"key": "org/org_123/case/case_123/quotes/quote/upload_opaque/1775000000000-Q-123.pdf",
"uploadReceipt": "signed-short-lived-receipt",
"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; every new PDF key must carry the matching receipt returned by the presign endpoint.
{
"externalQuote": {
"provider": "gc-crm",
"quoteReference": "Q-123",
"quotePdfKey": "org/org_123/case/case_123/quotes/quote/upload_opaque/1775000000000-Q-123.pdf",
"termsPdfKey": "org/org_123/case/case_123/quotes/terms/upload_opaque/1775000000000-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
},
"externalQuoteUploadReceipts": {
"quote": "signed-quote-receipt",
"terms": "signed-terms-receipt"
}
}{
"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
}Returns safe onboarding progress for 1 to 100 unique case IDs in one request. Use this for scheduled CRM reconciliation instead of one request per case. Reuse the system token until shortly before expiresAt, and stop polling terminal cases. Missing and other-firm IDs are deliberately indistinguishable.
{
"caseIds": ["case_opaque_id_1", "case_opaque_id_2"]
}{
"items": [
{
"caseId": "case_opaque_id_1",
"matterRef": "MAT-2026-001",
"matterType": "Purchase",
"status": "onboarding_in_progress",
"updatedAt": "2026-08-15T09:00:00.000Z",
"parties": []
}
],
"notFoundCaseIds": ["case_opaque_id_2"]
}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 |
| cases.read | Read case onboarding progress | POST /api/v1/cases/batch, GET /api/v1/cases/{caseId} |
| 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.