Legal Hub Group — Partner API

Server-to-server API for LHG partners · base URL https://api.legalhubgroup.com · OpenAPI 3 spec

Environments

Every key belongs to one environment. Sandbox keys (lhg_sbx_…) exercise the full API with no real-world side effects: e-filing goes to the courts' test system, serve jobs are flagged sandbox and never dispatched, no email reaches a real person, no card is charged. Production keys (lhg_prd_…) do the real thing. Both environments live at this same base URL — the key decides.

Authentication — HMAC-SHA256 request signing

You hold a key id (public) and a secret (shown once when the key is issued — store it in your secret manager; we cannot show it again, only rotate it). Every request except GET /v1/health carries four headers:

HeaderValue
LHG-Key-IdYour key id
LHG-TimestampUnix seconds at signing time (accepted within ±300s of server time)
LHG-NonceA unique string per request, at most 64 characters (random hex is fine). Each nonce is accepted once.
LHG-SignatureLowercase hex HMAC-SHA256 of the canonical string, keyed by your secret

The canonical string

Five lines joined by \n (a literal newline, no trailing newline):

METHOD                       uppercase — GET
PATH                         path incl. query string — /v1/partner
TIMESTAMP                    exactly as sent in LHG-Timestamp
NONCE                        exactly as sent in LHG-Nonce
SHA256(BODY)                 lowercase hex of the raw request body;
                             for GET / empty body use the empty hash:
                             e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Worked example

Computed with the server's own signing code — reproduce these exact values and your implementation is correct. Request bodies, when you send one, must be JSON (Content-Type: application/json); hash the exact bytes you send.

key id:    lhg_sbx_k_9f2c41d8a6b3e07f
secret:    lhg_sbx_s_0000000000000000000000000000000000000000000000000000000000000000
request:   GET https://api.legalhubgroup.com/v1/partner   (no body)
timestamp: 1758400000
nonce:     5f2b9c0d7e4a13c6

canonical string (shown with \n visible):
GET\n
/v1/partner\n
1758400000\n
5f2b9c0d7e4a13c6\n
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

signature = HMAC_SHA256(secret, canonical) =
2c4fa8d0d3329eaaec62b0505030090101c84f2fc4dbec9b66e53734171ee165
// Node.js
const crypto = require('crypto');
function signedHeaders(keyId, secret, method, path, body) {
  const ts = String(Math.floor(Date.now() / 1000));
  const nonce = crypto.randomBytes(16).toString('hex');
  const bodyHash = crypto.createHash('sha256').update(body || Buffer.alloc(0)).digest('hex');
  const canonical = [method.toUpperCase(), path, ts, nonce, bodyHash].join('\n');
  const signature = crypto.createHmac('sha256', secret).update(canonical).digest('hex');
  return { 'LHG-Key-Id': keyId, 'LHG-Timestamp': ts, 'LHG-Nonce': nonce, 'LHG-Signature': signature };
}
// const res = await fetch('https://api.legalhubgroup.com/v1/partner',
//   { headers: signedHeaders(KEY_ID, SECRET, 'GET', '/v1/partner') });

Endpoints

EndpointAuthWhat it does
GET /v1/healthnoneConnectivity check — proves DNS, TLS and routing before you write signing code
GET /v1/partnersignedReturns your partner name, environment, scopes, rate limit and IP allowlist — the first signed call to make
GET /v1/openapi.jsonnoneThis API as an OpenAPI 3 document

Scopes (efile, serve, concierge) gate the e-filing, process-serving and concierge endpoints that ship in later phases; your key's granted scopes appear in GET /v1/partner.

Rate limits

Per key, default 60 requests/minute (sliding window; configurable per key — ask us if you need more). Exceeding it returns 429 with a Retry-After header. Back off and retry; do not tight-loop.

Errors

Every failure is JSON: {"error":{"code":"…","message":"…"}}. The code is stable; branch on it, not on the message.

HTTPCodeMeaning
401missing_key / unknown_key / revoked_keyThe LHG-Key-Id header is absent, unknown, or the key was revoked
401invalid_timestampLHG-Timestamp malformed or outside the ±300s window — check clock skew
401invalid_signatureSignature mismatch — recheck the canonical string, especially the query string and body hash
401nonce_replayedThat nonce was already used with this key — generate a fresh one per request
403ip_not_allowedYour source IP is not on the key's allowlist
403partner_inactive / insufficient_scopeAccount inactive, or the key lacks the scope the endpoint needs
429rate_limitedOver the per-key limit — respect Retry-After
404not_foundNo such endpoint

Webhooks

Each key can register one HTTPS webhook URL with its own signing secret (issued when the URL is set, shown once). We POST JSON events to it:

{
  "id": "whd_123",              // delivery id — dedupe on it; retries reuse it
  "type": "test.ping",          // event type
  "environment": "sandbox",     // sandbox events never mean real-world activity
  "created_at": "2026-09-21T18:00:00.000Z",
  "data": { … }
}
HeaderValue
LHG-Webhook-IdDelivery id (same as body id)
LHG-EventEvent type
LHG-Webhook-TimestampUnix seconds at send time
LHG-Webhook-SignatureHex HMAC-SHA256 of "<timestamp>.<raw body>" with your webhook secret

Verify by recomputing the HMAC over the timestamp header, a dot, and the raw body bytes, then compare constant-time; reject timestamps older than a few minutes. Answer 2xx quickly (do slow work async). Non-2xx or a timeout is retried with exponential backoff (2, 4, 8… minutes, 8 attempts). The only event so far is test.ping, sent from the LHG admin console; e-filing, serve and concierge events arrive with those phases.

Legal Hub Group · questions: [email protected]