Skip to Content
APIAPI Overview

API Overview

Leadmetrics exposes four separate API surfaces — one per client — served from a single Fastify instance on the api service. Each surface has its own route prefix, auth middleware, and permission set. Shared middleware (authentication, tenant resolution, error handling, rate limiting) lives in a common/ module consumed by all four routers.

Related: Infrastructure — service ports | Multi-tenancy — tenant isolation | Governance — approval + permission model


Architecture: Gateway Layer

All requests pass through a gateway layer before reaching any surface router. The gateway owns every cross-cutting concern so surface routers stay focused on business logic.

Client → Gateway (auth · tenant · rate-limit · throttle · logging · audit) → Surface Router

See Gateway for the full spec — request lifecycle, hook order, audit log schema, and the future Option B upgrade path to a separate gateway service.


API Surfaces

SurfaceFastify PrefixAudience
Auth/auth/v1All apps — login, refresh, profile, registration, plans
Tenant/tenant/v1Dashboard + DM Portal — agents, billing, context, strategy, notifications, docs, knowledge
Media/media/v1Dashboard + DM Portal — image search and asset management
Admin/admin/v1Manage app — tenants, users, agents, billing, skills, templates, audit logs
Payment Gateway/pg/v1Payment provider webhooks (Razorpay)

The Next.js apps proxy requests through their own /api/ routes to the Fastify server. The Fastify prefixes above are the actual backend paths.


Base URLs

EnvironmentBase URL
Production (SaaS)https://api.leadmetrics.io
On-premhttps://api.<customer-domain>
Local developmenthttp://localhost:3001

Full endpoint URL = base + prefix + path, e.g.:

GET https://api.leadmetrics.io/dashboard/v1/campaigns

Authentication

All endpoints (except /auth/v1/login) require a Bearer token in the Authorization header:

Authorization: Bearer <JWT>

JWTs are issued by POST /auth/v1/login and refreshed via POST /auth/v1/refresh. The JWT payload carries:

interface LeadmetricsJWT { sub: string; // user.ref_id tenantId: string; // tenant.ref_id (absent for super_admin cross-tenant tokens) role: 'admin' | 'member' | 'reviewer' | 'super_admin'; appAccess: ('dashboard' | 'dm-portal' | 'manage')[]; iat: number; exp: number; // 15 min for access tokens, 7 days for refresh tokens }

Middleware enforcement per surface:

SurfaceRequired roleRequired appAccess
Dashboard APIadmin or memberdashboard
DM APIreviewer or admindm-portal
Admin APIsuper_adminmanage
Mobile APIadmin or memberdashboard

Access to the wrong surface returns 403 Forbidden.


Tenant Context

For Dashboard and Mobile APIs, the tenant is resolved from the JWT tenantId. Every query, queue operation, and data write is automatically scoped to that tenant — callers never pass tenantId explicitly.

For the DM API, the reviewer’s JWT has no fixed tenantId. Instead, endpoints accept an optional tenantId query parameter to filter to a specific tenant; omitting it returns data across all assigned tenants.

For the Admin API, there is no tenant scope by default. Tenant-specific sub-resources (e.g. GET /admin/v1/tenants/:tenantId/agents) accept a tenantId path segment.


Identifiers

All resource identifiers exposed in the API are ULIDs stored in the ref_id column (PostgreSQL) or refId field (MongoDB). Internal UUIDs are never exposed. ULIDs are:

  • URL-safe (no special characters)
  • Lexicographically sortable by creation time
  • 26 characters, e.g. 01ARZ3NDEKTSV4RRFFQ69G5FAV

Path parameters use the ULID directly:

GET /dashboard/v1/campaigns/01ARZ3NDEKTSV4RRFFQ69G5FAV

Pagination

List endpoints that may return large result sets use cursor-based pagination:

// Query parameters (all optional) interface PaginationQuery { limit?: number; // default 25, max 100 cursor?: string; // ULID of the last item from the previous page order?: 'asc' | 'desc'; // default 'desc' (newest first) } // Response envelope interface PaginatedResponse<T> { data: T[]; pagination: { hasMore: boolean; nextCursor: string | null; // pass as cursor on next request total?: number; // included when count is cheap; omitted otherwise }; }

Filtering & Sorting

Endpoints that support filtering document their accepted filter.* query parameters. All filter parameters are optional — omitting them returns all records visible to the caller.

GET /dashboard/v1/activities?filter.status=in_progress&filter.agentRole=copywriter GET /dm/v1/approvals?filter.riskLevel=high&filter.tenantId=01ARZ...

Sort order is controlled by order=asc|desc. The sort key is always the primary timestamp for that resource (e.g. created_on for campaigns, updated_on for activities).


Error Format

All errors return a consistent JSON body:

interface ApiError { error: { code: string; // machine-readable, e.g. 'NOT_FOUND', 'VALIDATION_ERROR' message: string; // human-readable description field?: string; // for VALIDATION_ERROR — which field failed details?: unknown; // additional structured context (optional) }; }

Standard HTTP status codes:

StatusCodeWhen
400VALIDATION_ERRORRequest body or query param fails validation
401UNAUTHORIZEDMissing or invalid JWT
403FORBIDDENValid JWT but insufficient role/permission
404NOT_FOUNDResource does not exist or is not visible to caller
409CONFLICTOperation conflicts with current state (e.g. approval already resolved)
422UNPROCESSABLERequest is valid but cannot be acted on (e.g. retry a running activity)
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORUnexpected server error

Rate Limits

Rate limits are enforced per (tenantId, userId) pair using a sliding window in Redis.

SurfaceLimit
Dashboard API300 requests / minute
DM API600 requests / minute
Admin API120 requests / minute
Mobile API200 requests / minute
Agent callback60 requests / minute per runId

Rate limit headers are included on every response:

X-RateLimit-Limit: 300 X-RateLimit-Remaining: 247 X-RateLimit-Reset: 1704067261

On 429, Retry-After is also included.


Versioning

The API is versioned in the URL prefix (/v1/). Breaking changes increment the version. Non-breaking additions (new fields, new endpoints) are made in place without a version bump.

A version is supported for a minimum of 12 months after the next version is released. Deprecation is communicated via Deprecation and Sunset response headers.


Server-Sent Events (SSE)

Real-time streaming endpoints return Content-Type: text/event-stream and follow the SSE protocol. Each event has a type field:

event: text_delta data: {"delta":"Here is the","activityId":"01ARZ..."} event: activity_completed data: {"activityId":"01ARZ...","status":"done","outputRef":"mongo:..."} event: approval_created data: {"approvalId":"01ARZ...","type":"content_review","riskLevel":"high"}

SSE connections are served from the Fastify API service (not Next.js API routes), which supports long-lived connections. The client uses EventSource (web) or the eventsource npm package (React Native).


Package Structure

apps/api/src/ ├── app.ts # Fastify app setup — plugins, routers, error handler ├── index.ts # Bootstrap — starts server, Redis plugin, worker ├── lib/ │ ├── auth.ts # requireTenantUser, apiError, bearerToken helpers │ ├── jwt.ts # JWT sign/verify (returns null, never throws) │ └── logger.ts # Shared pino instance (LOG_LEVEL env) ├── services/ # Pure business logic, no Fastify deps │ ├── auth.service.ts # Registration helpers, invoice number generation │ ├── billing.service.ts # Invoice amounts, line items, billing dates │ ├── blog.service.ts # Blog post approval/rejection state machine │ ├── social.service.ts # Social post approval/rejection state machine │ ├── campaign.service.ts # Campaign import + create helpers │ ├── campaigns.service.ts # Google Ads token + service helpers │ ├── channels.service.ts # Channel metric aggregators, date utils │ ├── channel-connect.service.ts # OAuth helpers, callback URL builders │ ├── tenants.service.ts # Tenant invoice + social post RAG helpers │ ├── strategy.service.ts # Strategy pipeline helpers │ ├── landing-page.service.ts # Landing page helpers │ ├── lead.service.ts # Lead helpers │ ├── connected-channel.service.ts │ ├── baseline-sync.service.ts │ └── zoho-books-sync.ts # Zoho Books API integration ├── routers/ │ ├── auth.ts # /auth/v1 — login, refresh, logout, me, plans, register │ ├── admin.ts # /admin/v1 — tenants, users, agents, billing, skills, templates, audit │ ├── tenant.ts # /tenant/v1 — agents, billing, context, strategy, notifications, chat │ ├── media.ts # /media/v1 — image search (Pixabay/Unsplash), pick, assets │ ├── docs.ts # /tenant/v1/docs — document upload, list, delete (RAG) │ ├── knowledge.ts # /tenant/v1/knowledge — knowledge base queries │ └── rag-config.ts # /admin/v1/rag-config — Qdrant / embedding config ├── schemas/ # OpenAPI schema objects for Swagger docs │ ├── shared.ts # Bearer auth, error responses, common fragments │ ├── tenant.schema.ts # Schemas for /tenant/v1 routes │ ├── docs.schema.ts # Schemas for /tenant/v1/docs routes │ └── media.schema.ts # Schemas for /media/v1 routes ├── socket/ │ └── index.ts # Socket.IO — notifications, presence, agent events (4 Redis connections) ├── worker.ts # BullMQ worker — processes agent jobs └── __tests__/ ├── setup.ts # Test DB config, env var stubs ├── helpers.ts # cleanDb(), createTenant(), createUser() ├── jwt.test.ts # Unit tests for lib/jwt.ts ├── lib-auth.test.ts # Unit tests for lib/auth.ts ├── media-router.test.ts └── integration/ ├── admin-users.test.ts ├── auth-profile.test.ts # GET/PATCH /auth/v1/me, password change, plans, refresh, logout ├── docs.test.ts ├── knowledge.test.ts ├── notifications.test.ts ├── rag-config.test.ts ├── register.test.ts ├── security.test.ts ├── tenant-notification-prefs.test.ts └── tenant-pipeline.test.ts

© 2026 Leadmetrics — Internal use only