Skip to main content

    Developer docs

    Build scoped onboarding integrations

    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.

    Versionv1 REST endpoints and MCP
    AuthClient credentials -> scoped system JWT
    AuditAll actions attributed to the integration
    Machine filesllms.txt and OpenAPI YAML

    Quickstart

    From firm setup to first case

    The integration flow is intentionally small: create credentials, request a scoped token, then call only the endpoints enabled by the firm.

    1

    Create the integration

    A firm admin creates an API integration in Firm -> Apps & API access and chooses the least scopes required.

    2

    Store credentials

    Use the returned appId as clientId and store the one-time clientSecret in a backend secret manager.

    3

    Issue a system token

    Exchange client credentials for a short-lived MicroApp system token.

    4

    Call scoped endpoints

    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

    Client credentials issue short-lived tokens

    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.

    Create system tokenbash
    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
      }'
    Create case with tokenbash
    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"
          }
        ]
      }'

    One-time secret

    The clientSecret is shown only when created or rotated.

    Scoped token

    Requested scopes are intersected with firm-enabled scopes.

    IP restrictions

    allowedIpRanges are enforced at token issuance and API use.

    MCP

    Connect AI assistants with user auth

    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 OAuth setupbash
    codex mcp add verify-client --url "$VERIFY_CLIENT_BASE_URL/api/mcp"
    codex mcp login verify-client
    Claude OAuth setupbash
    claude mcp add --transport http verify-client "$VERIFY_CLIENT_BASE_URL/api/mcp"
    claude mcp login verify-client

    Connection details

    Public MCP URL
    /api/mcp
    Local direct API
    http://localhost:8081/mcp
    Discovery
    /.well-known/oauth-protected-resource/mcp
    OAuth scopes
    vc.mcp.read, vc.mcp.write

    Auth trigger

    Register 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.

    Discovery and challengehttp
    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"

    Safety boundaries

    • Available tools are whoami, list_cases, search_cases, and get_case.
    • Tools only return cases visible to the authenticated principal's organisation and role.
    • Internal users must supply an orgId for list and search operations.
    • Raw client workflow payloads, evidence keys, and large sensitive blobs are omitted.
    • Permission failures return MCP auth challenges so clients can request the right sign-in or scope.
    • Future write tools should require vc.mcp.write, explicit confirmation, dry-run previews, and audit attribution.

    Endpoint reference

    Supported automation endpoints

    These are the endpoints currently documented for server-to-server onboarding. Do not invent endpoints; inspect implementation before using anything outside this list.

    Generated Client Care Letters are HTML in client onboarding. When the client signs, the client app renders the signed letter to A4 PDF, uploads it with the document snapshot upload endpoint, and acknowledges using that signed snapshotKey. Unsigned generated PDF keys do not complete acknowledgement.
    POST/api/v1/microapps/system-tokens

    Create system token

    Exchanges an API integration clientId and clientSecret for a scoped JWT.

    client credentials
    Request bodyjson
    {
      "clientId": "00000000-0000-0000-0000-000000000000",
      "clientSecret": "stored-secret",
      "scopes": ["cases.create", "parties.manage", "client-care-letters.generate", "invites.send"],
      "expiresInSeconds": 3600
    }
    Response bodyjson
    {
      "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
    }
    POST/api/v1/cases

    Create case

    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.

    cases.create
    Request bodyjson
    {
      "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
        }
      ]
    }
    Response bodyjson
    {
      "caseId": "case_opaque_id",
      "status": "NotStarted",
      "parties": [
        {
          "partyId": "party_opaque_id",
          "name": "Alex Client",
          "clientAccessStatus": "not_sent"
        }
      ]
    }
    POST/api/v1/cases/{caseId}/external-quote/presign

    Upload quote PDF

    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.

    cases.create
    Request bodyjson
    {
      "filename": "Q-123.pdf",
      "contentType": "application/pdf",
      "kind": "quote"
    }
    Response bodyjson
    {
      "uploadUrl": "https://...",
      "key": "org/org_123/case/case_123/quotes/quote/Q-123.pdf",
      "expiresIn": 900
    }
    PUT/api/v1/cases/{caseId}/external-quote

    Save quote metadata

    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.

    cases.create
    Request bodyjson
    {
      "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
      }
    }
    Response bodyjson
    {
      "case": {
        "caseId": "case_opaque_id",
        "matterRef": "MAT-2026-001"
      }
    }
    GET/api/v1/microapps/api/cases/ids

    List case IDs

    Returns paginated opaque case IDs for remote ledger sync and duplicate-check connectivity.

    cases.ids.list
    Requesthttp
    GET /api/v1/microapps/api/cases/ids?limit=200&cursor=<nextCursor>
    Authorization: Bearer <system-token>
    Response bodyjson
    {
      "ok": true,
      "items": ["case_opaque_id"],
      "nextCursor": null
    }
    POST/api/v1/cases/{caseId}/parties

    Add party

    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.

    parties.manage
    Request bodyjson
    {
      "name": "Jordan Client",
      "email": "jordan@example.com",
      "mobile": "+447700900456",
      "role": "Seller",
      "partyType": "individual"
    }
    Response bodyjson
    {
      "partyId": "party_opaque_id",
      "name": "Jordan Client",
      "email": "jordan@example.com",
      "role": "Seller",
      "clientAccessStatus": "not_sent"
    }
    PATCH/api/v1/cases/{caseId}/parties/{partyId}

    Edit party

    Updates contact, role, or company details for an existing party. Email must remain unique within the case; supplied mobile numbers are validated and normalized.

    parties.manage
    Request bodyjson
    {
      "name": "Jordan Client",
      "email": "jordan.updated@example.com",
      "mobile": "+447700900456",
      "role": "Seller"
    }
    Response bodyjson
    {
      "partyId": "party_opaque_id",
      "name": "Jordan Client",
      "email": "jordan.updated@example.com",
      "mobile": "+447700900456",
      "role": "Seller",
      "clientAccessStatus": "not_sent"
    }
    PUT/api/v1/cases/{caseId}/primary-client

    Set primary client

    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.

    parties.manage
    Request bodyjson
    {
      "partyId": "party_opaque_id"
    }
    Response bodyjson
    {
      "ok": true,
      "party": {
        "partyId": "party_opaque_id",
        "name": "Alex Client",
        "isPrimaryContact": true
      }
    }
    GET/api/v1/cases/{caseId}/client-care-letter/status

    Check Client Care Letter status

    Returns whether a Client Care Letter is required, whether a generated template is configured, and what matter data is missing before generation.

    cases.read or client-care-letters.generate
    Requesthttp
    GET /api/v1/cases/{caseId}/client-care-letter/status
    Authorization: Bearer <system-token>
    Response bodyjson
    {
      "required": true,
      "mode": "generated",
      "readinessStatus": "ready_to_generate",
      "templateConfigured": true,
      "templateName": "Purchase Client Care Letter",
      "document": null,
      "missingFields": []
    }
    POST/api/v1/cases/{caseId}/client-care-letter/generate

    Generate Client Care Letter

    Renders the configured matter-type or firm default template from stored matter data and firm defaults, then saves an invite-ready HTML document.

    client-care-letters.generate
    Request bodyjson
    {}
    Response bodyjson
    {
      "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"
        }
      }
    }
    GET/api/v1/cases/{caseId}/evidence-packs/{packId}/zip

    Download evidence archive

    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.

    evidence.pack.read
    Requesthttp
    GET /api/v1/cases/{caseId}/evidence-packs/{packId}/zip
    Authorization: Bearer <token>
    Response bodyjson
    {
      "packId": "pack_opaque_id",
      "downloadUrl": "https://...",
      "expiresIn": 900,
      "filename": "ABC123 - evidence pack.zip",
      "fileCount": 8
    }
    POST/api/v1/cases/{caseId}/invites

    Send invite

    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.

    invites.send
    Request bodyjson
    {
      "partyId": "party_opaque_id",
      "autoGenerateClientCareLetter": true,
      "sendSms": false
    }
    Response bodyjson
    {
      "ok": true,
      "inviteId": "invite_opaque_id",
      "channel": "email"
    }

    Duplicate safeguards

    Keep live creation blocked when remote state is unknown

    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.

    Recommended flow

    1. 1Check your local sync ledger for the source matter/reference.
    2. 2Refresh the remote case-ID ledger when your workflow requires a remote duplicate check.
    3. 3Create the case only when local and remote checks are conclusive.
    4. 4Treat a duplicate matterRef response from POST /api/v1/cases as an existing case, not a retryable failure.

    Remote check outcomes

    200
    Use the returned page and continue until nextCursor is null when a complete sync is required.
    400
    Fix the request, usually an invalid cursor. Do not retry unchanged.
    401
    Refresh the system token or rotate/check credentials. Do not report this as duplicate uncertainty.
    403
    A firm admin must enable the scope, integration status, or source IP/origin configuration.
    5xx
    Remote state is unknown. Block live creation unless an operator explicitly accepts local-ledger-only dedupe.

    Scopes and schemas

    Least-privilege permissions

    Use the smallest scope set possible. Treat all identifiers as opaque strings and persist only the IDs needed to continue a workflow.

    ScopePurposeEndpoint
    cases.createCreate onboarding casesPOST /api/v1/cases
    parties.manageAdd/edit parties and choose the primary clientPOST/PATCH /api/v1/cases/{caseId}/parties, PUT /api/v1/cases/{caseId}/primary-client
    client-care-letters.generateGenerate configured Client Care LettersPOST /api/v1/cases/{caseId}/client-care-letter/generate
    invites.sendSend onboarding invitesPOST /api/v1/cases/{caseId}/invites
    cases.ids.listList case IDsGET /api/v1/microapps/api/cases/ids
    users.listList firm usersGET /api/v1/microapps/api/users

    Errors and audit

    Plan for explicit failure states

    Integrations should branch on HTTP status first. Error body fields are useful but should be treated as optional.

    Status codes

    400
    Missing or invalid payload field
    401
    Missing, expired, malformed, or invalid credentials. Refresh or rotate credentials.
    403
    Valid credentials but missing scope, active integration status, or source IP allowance.
    404
    Route not found
    500
    Server error. Retry safely; block live creation if remote duplicate state is required and unknown.

    Audit attribution

    Case, party, and invite actions performed by system tokens are written as MicroApp audit actors with the integration appId and identifier.

    Audit actor shapejson
    {
      "actorType": "microApp",
      "actorId": "<appId>",
      "meta": {
        "actorTokenKind": "system",
        "actorIdentifier": "<identifier>"
      }
    }

    AI files

    Machine-readable entrypoints

    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.

    AI builder rules

    • Do not expose clientSecret in frontend code, logs, source control, or prompts.
    • Request only the scopes needed for the current workflow.
    • Cache tokens only until expiry and refresh server-side.
    • Do not retry invalid credentials indefinitely.
    • Validate returned caseId and partyId before continuing.
    • Use fixed egress IPs when firms configure allowedIpRanges.

    Need production access?

    Firm admins manage integrations from Apps & API access.

    Talk to us