Authentication | API Reference Guide | CourtMesh
    Skip to main content

    Authentication

    How to authenticate a request, how key rotation and revocation work, and how organisations restrict calls by IP.

    Authentication
    Two accepted headers, one key format, and the refusals you will actually see.

    Every endpoint below requires a key, with two exceptions: GET /health, which is unauthenticated so you can probe availability from your own monitoring, and GET /coverage, which is public on purpose so you can check our corpus numbers before you ever create a key. The middleware reads X-API-Key first and falls back to a bearer token, so either of these is fine.

    # Either header authenticates. Pick one, do not send both.
    curl "https://research.courtmesh.ai/api/v1/prod/judges/search?q=nariman" \
      -H "X-API-Key: cm-YOUR_KEY_HERE"
    
    curl "https://research.courtmesh.ai/api/v1/prod/judges/search?q=nariman" \
      -H "Authorization: Bearer cm-YOUR_KEY_HERE"

    What a rejection looks like

    The key is checked for shape before it is looked up, then for existence, then for whether the account is entitled to call the API at all. Suspended users and deactivated organisations are refused at this layer too, and if the identity service cannot confirm your standing the request is refused with HTTP 503 rather than being let through.

    // 401, no key on the request
    { "error": "API key required. Provide it in X-API-Key header or Authorization: Bearer <key>" }
    
    // 401, the key does not match cm- or vv- followed by 32 then 4 characters
    { "error": "Invalid API key format" }
    
    // 401, well formed but unknown, or the key was deactivated
    { "error": "Invalid API key" }
    { "error": "API key is deactivated" }
    
    // 403, the account's plan is not entitled to call the API
    {
      "error": "API access is only available on the Enterprise plan. Upgrade to an Enterprise plan for API access.",
      "callsToday": 0,
      "maxAllowed": 0
    }
    
    // 403, the account is Enterprise but has never created a key
    {
      "error": "No API keys found. Please create an API key first.",
      "callsToday": 0,
      "maxAllowed": -1
    }

    Handling keys

    • Never put a key in client side code, a mobile binary, or a public repository
    • Keep keys in environment variables or a secret manager, never in source control
    • Rotate by creating the new key, deploying it, then deleting the old one
    • Use separate keys per environment: the rate limit is per key, so they do not compete
    API keys: rotate and revoke
    A 24 hour overlap window so rotating a key never breaks a live integration.

    POST /api/settings/api-keys/:id/rotate mints a replacement key with the same name (suffixed "(rotated)") and the same organisation, returns its plaintext once, and gives the OLD key a 24 hour grace window rather than revoking it outright: the old key keeps authenticating for 24 hours, so a process still holding it in an environment variable does not break the instant you rotate. After the window the old key is expired and answers with HTTP 401 API_KEY_EXPIRED. This is a dashboard action, called with your logged in session, not with an API key.

    # Dashboard session, not an API key: called from the API Keys settings
    # screen, or with your logged in session cookie.
    curl -X POST https://research.courtmesh.ai/api/settings/api-keys/<keyId>/rotate \
      -H "Cookie: <your dashboard session>"
    {
      "message": "API key rotated successfully",
      "apiKey": {
        "id": "68f2b0...",
        "name": "Production (rotated)",
        "fullKey": "cm-....................................-abcd",
        "displayKey": "cm-...-abcd"
      },
      "rotatedFrom": {
        "id": "68a110...",
        "expiresAt": "2026-09-19T06:00:00.000Z"
      }
    }

    Deleting a key is a soft revoke

    Deleting a key marks it inactive rather than erasing its row: it stops authenticating immediately (HTTP 401 API_KEY_REVOKED on its next use) while its call history stays intact for your audit log and usage reporting. An ORG_ADMIN can rotate or revoke any key that belongs to their organisation, not only their own; every other caller can only manage their own keys.

    IP allowlisting
    Organisation level, off by default, opt in through support.

    An organisation can restrict its API keys to a list of IP addresses or CIDR ranges. It is off by default for every organisation; enable it by contacting support@courtmesh.ai with the ranges to allow. Once enabled, a request from an address not on the list is refused with HTTP 403 IP_NOT_ALLOWED, for every key that organisation owns.

    // HTTP 403, strict mode organisation, calling IP not on the allowlist
    {
      "success": false,
      "code": "IP_NOT_ALLOWED",
      "message": "Access denied. Your IP address is not whitelisted for this organization.",
      "error": "Access denied. Your IP address is not whitelisted for this organization.",
      "requestId": "req_5d10ee31"
    }

    Propagation and strict mode

    Allowlist changes can take a short time to propagate before they take effect. An organisation can additionally opt into strict mode for a tighter compliance posture. Ask support to enable it and to discuss which behaviour is the right trade-off for your requirements.

    Scoped API keys
    Restrict a key to only the capabilities it needs. Optional and additive: a key with no scopes keeps full access.

    Every API key can optionally be restricted to a subset of six capability scopes. A request made with a key that lacks the scope its endpoint requires is refused with HTTP 403 before any other work happens, including before anything is billed against your usage.

    The six scopes

    ScopeCovers
    searchPOST /search/cases, POST /search/cases/semantic, GET /judges/search
    readGET /cases/:id, /cases/:id/related, /cases/:id/pdf, GET /coverage, /reference/*
    analyzePOST /analyze, POST /analyze-consolidated
    screenPOST /party/screen, POST /party/screen/batch (Litigation Check API)
    timelinePOST /request-timeline, GET /get-timeline
    accountGET /usage, GET /me, GET /audit

    No scopes means full access

    Scoping is opt in and backward compatible. A key created with no scopes selected - including every key created before capability scoping existed - is treated as unrestricted and can call every endpoint its plan and account otherwise allow. Scopes only ever narrow what a key can do; there is no way to configure a key that has fewer permissions than "no scopes set" other than listing the scopes you actually want it to have.

    Creating a read-only key

    For a key that should only search and read case data and check its own usage, without ever running an AI analysis, requesting a timeline, or screening a party, select exactly search, read and account when creating the key.

    # Dashboard session, not an API key: called from the API Keys settings
    # screen, or with your logged in session cookie.
    curl -X POST https://research.courtmesh.ai/api/settings/api-keys \
      -H "Cookie: <your dashboard session>" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Read-only reporting key",
        "scopes": ["search", "read", "account"]
      }'
    {
      "message": "API key created successfully",
      "apiKey": {
        "id": "68f2c1...",
        "name": "Read-only reporting key",
        "key": "cm-••••••••••••••••••••••••••••••••.abcd",
        "scopes": ["search", "read", "account"],
        "createdAt": "2026-09-19T10:12:04.000Z"
      }
    }
    // The plaintext key is only ever shown in this one response. Store it now.

    What a scope refusal looks like

    The same read-only key calling POST /analyze (which requires the analyze scope) is refused rather than billed:

    // HTTP 403, key has scopes but not the one this endpoint requires
    {
      "success": false,
      "code": "API_KEY_SCOPE_INSUFFICIENT",
      "message": "This API key does not have the \"analyze\" scope required for this endpoint.",
      "error": "This API key does not have the \"analyze\" scope required for this endpoint.",
      "requestId": "req_7a1f0c92"
    }