Authentication
UI = MCP — single surface
Every protected endpoint accepts either authentication scheme. Whatever the LDM web app does, an autonomous agent (Claude Desktop, Cursor, custom MCP client, n8n, server-to-server integration) can do via the same URL — just swap the JWT cookie for a Bearer token. There is no parallel /v1/* mirror to keep in sync.
Internally a single HybridAuthGuard resolves either method to a uniform req.user + req.tenantId so controller code is identical for both callers. Per-method @RequireScope(...) enforces fine-grained scope checks for Bearer keys; UI sessions get the scopes their role grants.
Bearer token (agents / MCP / SDK)
Tokens are issued by POST /v1/signup and look like ldm_pk_<random>. Send them in the Authorization header on every request; scopes are checked against the key.
Authorization: Bearer ldm_pk_a1b2c3d4...Use Bearer for: AI agents (Claude Desktop, Cursor), MCP servers, scheduled scripts, server-to-server integrations. The agent-card at /.well-known/agent-card.json advertises this scheme so discovery-aware clients can find the auth requirements without prior config.
Cookie / JWT session (web UI)
Visit https://app.live-direct-marketing.online/login and authenticate with email + password. The browser receives an HttpOnly cookie carrying a short-lived JWT; subsequent /api/* requests use it automatically (no manual header).
# After /login completes, the UI calls the API like this — same endpoints,
# auth comes from the session cookie set by /login.
curl -b cookies.txt https://api.live-direct-marketing.online/api/companiesUse Cookie/JWT for: the LDM web app itself, manual exploration in a browser, local dev. Roles OWNER and ADMIN carry all scopes; other roles get scopes per their assignment in the workspace.
Generic 401 (anti-enumeration)
A missing header, malformed scheme, unknown key, or revoked key — all four failure modes return the same response. Distinct error messages would let attackers enumerate valid key prefixes or formats.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"statusCode": 401,
"message": "Invalid credentials"
}If you receive this response, verify (in order): (1) the Authorization header is sent at all, (2) the value starts with Bearer (with the trailing space), (3) the key was copied verbatim from /v1/signup response, (4) the key has not been revoked.
Server-side storage
Plaintext keys are returned by /v1/signup exactly once. The server stores only a SHA-256 hash with a 7-character prefix for fast lookup. The plaintext is never logged.