Customer 360 API and MCP Server
Integration guide for frontend engineers, platform engineers, and QA/QC teams.
The route inventory and behavior below reflect the current application wiring in
app.py, core/apps/, core/routers/, and core/mcptools/.
1. Service Overview
customer360-api exposes:
- A FastAPI REST API under
/api/v1for Customer 360 data, identity resolution, CRM, segmentation, reporting, content, campaigns, and administration. - A FastMCP server mounted under
/mcpfor authenticated AI-agent workflows.
The REST API uses PostgreSQL for domain data, Redis for caching/auth support, Dagster for asynchronous long-running jobs, and S3-compatible storage for behavioral-event queries and master-profile event projections.
2. Base Paths and Runtime
- Service root:
GET /andGET /health - REST prefix:
/api/v1 - MCP mount:
/mcp - REST OpenAPI UI:
/docs - REST OpenAPI schema:
/openapi.json - Application
root_path:/c360api - Default local port:
8008
When deployed behind a reverse proxy, include /c360api in the externally
visible URL as configured by the ingress.
Behavioral event queries
These endpoints are read-only and query the S3/MinIO event lake. They return aggregates, not raw event rows:
GET /api/v1/events/: complete UTC-day event totals.GET /api/v1/events/device-types: totals grouped by normalized device type.
Both endpoints accept days, event_time_from, and optional filters. The daily
totals endpoint also accepts master_profile_id, domain, channel,
event_category, event_name, and data_source_id. The device-type endpoint
accepts data_source_id. days must be at least 1 and cannot exceed
EVENT_QUERY_MAX_DAYS (default 180).
The repository resolves active, tenant-owned data sources from PostgreSQL and
reads immutable event objects from S3/MinIO. It does not write event data to
PostgreSQL. By default, an unset EVENT_S3_BUCKET selects the per-source
data-tracking-{data_source_id} bucket; set it to use a shared bucket layout.
Master-profile timeline
GET /api/v1/master-profiles/{master_profile_id}/timeline reads the profile’s
JSON event projection from MASTER_PROFILE_S3_BUCKET (default
c360-master-profiles) at {master_profile_id}.json. It returns a
most-recent-first unified feed of behavioral events, CRM transactions, and
customer-service contacts.
Query parameters:
limit: 1-100, default20.data_source_id: optional active source owned by the profile’s tenant.from_event_timeandto_event_time: optional, order-independent bounds.
With no bounds, the endpoint uses the most recent seven days. Entries expose
source lineage, event category/name, device/domain metadata, page URL/title and
referrer, and a bounded scalar-only event_data object. It does not return the
complete raw event payload or identity object.
3. Architecture and Ownership
- App entrypoint:
app.py - REST app factory and router registration:
core/apps/http_api_app.py - MCP app factory:
core/apps/mcp_app.py - REST authentication middleware:
core/auth.py - MCP authentication and tenant context:
core/mcptools/context.py - MCP tool registry:
core/mcptools/__init__.py - One MCP tool per module:
core/mcptools/*.py - Shared application settings:
customer360-dao/src/leo_customer360_dao/config.py
Business logic belongs in repositories/services. Routers provide transport, validation, authorization, and response mapping. Tenant scope is resolved on the server and reinforced by tenant-scoped queries and PostgreSQL RLS; clients must not use caller-controlled tenant identifiers to access another tenant.
4. Authentication and Tenant Context
4.1 REST authentication
Protected REST requests use:
Authorization: Bearer <access_token>SSO_LOGIN selects the authentication mode:
SSO_LOGIN=true: the bearer token is introspected with Keycloak and the result is cached in Redis. Tenant/user claims are resolved and, when needed, the Keycloak identity is provisioned insys_user.SSO_LOGIN=false:POST /api/v1/auth/loginissues a locally signed HS256 JWT. The token uses the same bearer contract as SSO. ConfigureDEV_JWT_SECRETandDEV_JWT_EXPIRES_MINUTESfor local development.
The public REST paths are:
GET /healthGET /api/v1/metadata/(the bare metadata route)POST /api/v1/auth/loginPOST /api/v1/auth/callbackPOST /api/v1/auth/logoutGET /api/v1/auth/zalo-redirect
Nested metadata routes, including /metadata/dagster, /metadata/domains, and
/metadata/smtp, remain protected. Failed authentication and failed dev-login
attempts are Redis-rate-limited.
For database work, the resolved tenant context is pinned to the transaction before tenant-scoped reads/writes. SSO admin actions require the appropriate admin role; local mode intentionally relaxes those role checks for development.
4.2 MCP authentication
MCP requests use:
X-API-Key: <api_key>Redis maps the key to a tenant:
redis-cli SET 'apikey:<api_key>' '<tenant_id>'The MCP dependency validates the key, binds the mapped tenant to request
context, and tools read that server-side context rather than accepting a
tenant id from the caller. Invalid keys return 401; an unavailable Redis
authentication backend returns 503.
5. Frontend Integration
5.1 Local login
curl -s -X POST http://localhost:8008/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<DEFAULT_ROOT_PASSWORD>"}'Send the returned access_token as a bearer token on protected REST requests.
In SSO mode, use POST /api/v1/auth/callback to exchange the Keycloak
authorization code for tokens. POST /api/v1/auth/logout returns a Keycloak
end-session URL in SSO mode and a no-op response in dev mode.
5.2 Pagination and errors
Resource lists generally use skip/limit; profile and campaign analytics
lists may use page/page_size. Common limits are controlled by
API_DEFAULT_PAGE_SIZE/API_MAX_PAGE_SIZE or by endpoint-specific bounds.
Clients should handle at least 401, 403, 404, 409, 422, 429, 500,
and 503. Show user-safe messages in the UI and retain backend detail values
for diagnostic logs.
6. Local Run and Operations
From customer360-api:
python3 -m venv .venv
./.venv/bin/pip install -r requirements.txt
./.venv/bin/uvicorn app:app --host 0.0.0.0 --port 8008 --reloadHealth probes:
GET /health: verifies the pooled SQLAlchemy connection withSELECT 1and reports build metadata and SSO mode.GET /mcp/health: MCP sub-application liveness response.
Runtime dependencies:
- PostgreSQL for persistent domain data and RLS.
- Redis for response caching, auth/token and identity caches, rate limiting, and MCP API-key-to-tenant mappings.
- Keycloak when
SSO_LOGIN=true. - Dagster for segmentation, identity, analytics, CRM-sync, and campaign jobs.
- S3/MinIO for event-lake and master-profile event projections.
Important configuration
The settings object is defined in the local customer360-dao checkout. Common
API settings include:
- Database:
DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_NAME,DB_SCHEMA. - Auth:
SSO_LOGIN,SSO_LOGIN_URL,KEYCLOAK_REALM,KEYCLOAK_CLIENT_ID,KEYCLOAK_CLIENT_SECRET,KEYCLOAK_CALLBACK_URL,KEYCLOAK_VERIFY_SSL,DEFAULT_ROOT_USERNAME,DEFAULT_ROOT_PASSWORD,DEV_JWT_SECRET, andDEV_JWT_EXPIRES_MINUTES. - Redis/cache:
REDIS_HOST,REDIS_PORT,REDIS_DB,REDIS_PASSWORD,CACHE_ENABLED, andCACHE_TTL_SECONDS. - Event lake:
EVENT_QUERY_MAX_DAYS,EVENT_S3_BUCKET,EVENT_RAW_PREFIX,S3_ENDPOINT_URLorANALYTICS_S3_ENDPOINT_URL,S3_REGION,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY,S3_SESSION_TOKEN,S3_FORCE_PATH_STYLE, andS3_VERIFY_SSL. - Master-profile projections:
MASTER_PROFILE_S3_BUCKET. - Dagster:
DAGSTER_GRAPHQL_HOSTandDAGSTER_GRAPHQL_PORT, plus the per-service job/location/repository overrides in the settings class. - Email health:
EMAIL_DISPATCH_ADAPTERand theSMTP_*settings.
7. MCP Tooling
MCP endpoints and documentation are provided by FastMCP under /mcp; the exact
transport routes are library-version dependent. Current registered tools are:
get_system_metrics
Read-only host summary returning current_date, ram_usage_percent,
ram_used_gb, and ram_total_gb.
search_data_sources_by_name
Inputs:
keywords: strlimit: int, default5, clamped to1-20
The tool tokenizes and normalizes keywords, escapes SQL wildcard characters,
searches only the authenticated tenant’s data sources, and ranks matches by
relevance and activity. Each result contains only id, name,
total_tracked_event, avg_daily_event, avg_events_per_profile,
javascript_tags, and qr_code_data.
To add a tool, create a module in core/mcptools/, implement
register_<tool>_tool(mcp: FastMCP), use get_bound_tenant_id() for tenant
scope, register it in core/mcptools/__init__.py, and add auth/contract tests.
8. REST Route Inventory
All paths below are relative to /api/v1. Generic CRUD resources expose
GET /, GET /count, GET /{id}, POST /, PATCH /{id}, and
DELETE /{id} unless noted otherwise. Several list/create routes also accept
the no-trailing-slash form, which may be hidden from OpenAPI to avoid duplicate
operations.
Health and metadata
GET /andGET /healthGET /metadata/GET /metadata/dagsterGET /metadata/smtpGET /metadata/domains- CRUD:
/data-sources - CRUD:
/ai-agents(item key isagent_code; also has/count)
Authentication and users
POST /auth/loginPOST /auth/callbackPOST /auth/logoutGET /auth/zalo-redirectGET /users/mePOST /usersGET /usersGET /users/{user_id}PATCH /users/{user_id}DELETE /users/{user_id}GET /users/{user_id}/sso-identities
CRM and campaign analytics
- Generic CRUD:
/campaigns,/campaign-members,/leads,/lead-sources,/contacts,/accounts,/opportunities,/industries GET /campaigns/analyticsGET /campaigns/analytics/summaryGET /campaigns/analytics/spend-trendGET /campaigns/analytics/top
Campaign draft workflow under /campaigns:
POST /campaigns/draftPOST /campaigns/zalo-draftGET /campaigns/{campaign_id}/content-itemsPATCH /campaigns/{campaign_id}/draftPOST /campaigns/{campaign_id}/approvePOST /campaigns/{campaign_id}/rejectGET /campaigns/{campaign_id}/history
Campaign activation and email administration under /admin:
POST /admin/campaigns/{campaign_id}/activateGET /admin/campaigns/{campaign_id}/dispatch-logsGET /admin/email-provider-configPUT /admin/email-provider-config
Identity resolution and personas
- Master profiles:
/master-profileswith list/count/item CRUD plus/{id}/links,/{id}/domain-profiles,/{id}/domain-attributes,/{id}/linked-raw-profiles/{raw_profile_id},/{id}/persona,/{id}/persona-history,/{id}/engagement-summary,/{id}/channel-activity,/{id}/top-interests, and/{id}/timeline. - Generic CRUD:
/raw-profiles,/profile-links,/domain-profiles,/profile-attributes, and/identity-index. - Merge history:
GETlist/item andPOST /profile-merge-history/. GET /resolution-status/.- Persona archetypes:
/persona/archetypeswith list/item operations and/{id}/master-profiles. - Customer personas:
GET /persona/list,GET /persona/analytics/summary,GET /persona/category/{persona_category}/master-profiles, item reads and nested feature/score/master-profile reads, plus item CRUD. - Persona explainability/history resources:
/persona/features,/persona/score-details, and/persona/historyprovide list/item reads and append-onlyPOSToperations.
Reporting, relations, and events
GET /reporting/summaryGET /reporting/master-profiles/duplicatesGET /reporting/identity-graph/coverage- CRUD:
/relation-types,/relations,/customer-contacts,/transactions GET /events/GET /events/device-types
Content and graph
-
Content CRUD:
/content-items -
Product listing and create:
GET /content-items/products,POST /content-items/products -
GET /content-items/supports tenant-scoped search, domain/type/status filters, and offset paging. -
GET /content-items/recommended?master_profile_id=<uuid>returns the latest persisted ranking results from the profile’s active segment workflows. Results are merged and deduplicated across segments by default; an optionalsegment_id=<uuid>restricts the response to one segment. -
POST /segments/{segment_id}/workflow/runexecutes configuredranking_recommendationsteps for active profiles currently in that segment, using only the step’s selected content candidates. Configure the step’s JSONconfigurationwithstrategy(tags,semantic, orhybrid),top_k,semantic_weight,tag_weight,minimum_score, and optionalsemantic_query. Semantic/hybrid ranking uses the existingDOCS_EMBEDDING_PROVIDERand provider-specificDOCS_*_EMBEDDING_*settings; selected content embeddings are generated and cached lazily. Profile query text uses domain and segment tags, plus the optional operator-provided context, not profile PII. Recommendation ranking evaluates only the configured content candidates using pgvector cosine distance and segment-tag overlap. To use Gemini, select it with the existingDOCS_EMBEDDING_PROVIDER=geminisetting and configure the existingDOCS_GEMINI_*embedding settings; no recommendation-specific embedding variables are introduced. -
GET /content-items/count -
POST /content-items/import/productsaccepts a tenant-authenticated multipart.tsvupload (maximum 10 MiB / 5,000 rows) and returns202with a Dagsterrun_id. The API validates and stages the complete source rows in the configuredPRODUCT_IMPORT_S3_BUCKET; thedata_synchDagster location generates display titles/summaries throughcustomer360-agent, then transactionally upsertscdp_product_itemsand linkedcdp_content_items. Re-imports update the existing product identified by(tenant_id, Source_Type, Store_ID, Product_ID_Type, Product_ID).Store_IDmaps to product-tablesource_id; the optionalSource_TypeTSV column maps to non-nullsource_typeand defaults to an empty string. Initialize the current database schema and apply the legacy upgrade to existing databases first. The upload API and Dagster worker must share the product-import bucket and S3 settings; the worker must also be able to reachAGENT_SERVICE_URLand use the matchingAGENT_API_TOKENwhen configured.TSV mapping:
Product_Typeuses<domain>_productand maps to contentdomainplusitem_type=product;STOCKmaps to the canonicalbankingdomain;Keywordsmap tosegment_tags;Image_URLandFull_URLmap to content image and CTA URL; source identity, prices, currency,ext_attributes, and every original TSV column are retained incdp_product_items. A blankext_attributescell maps to{}; non-blank cells must contain a JSON object.Name,Description, and all other source fields are sent to the agent service to generate the content title and summary. Product titles/summaries are not written if enrichment fails. If the agent reports that neitherLLM_API_KEYnorLLM_BASE_URLis configured, the worker skips generation, saves the source products withext_attributes.next_actions = "generate_content_item", and leaves theircontent_item_idunset for later enrichment. Provider/network failures still fail the import instead of being treated as a credential-free run. -
POST /content-items/import/contentaccepts a tenant-authenticated content TSV withDomain,Item_Type(article,news, orvideo), andTitlerequired; summary, image/CTA fields, tags, publication time, and active status are optional. The API validates every field and active domain, stages the rows inCONTENT_IMPORT_S3_BUCKET, and returns202with a Dagster run ID. The Data Synch worker inserts the content items transactionally; rerunning the same staged job is idempotent.
At API startup, the service checks PRODUCT_IMPORT_S3_BUCKET and
CONTENT_IMPORT_S3_BUCKET using the configured S3-compatible endpoint. Missing
buckets are created when S3_AUTO_CREATE_BUCKETS=true (the default); API
startup fails explicitly if access is denied or bucket creation fails. The
Data Synch worker must use the same product-import bucket name and S3
credentials/endpoint.
- Graph edges:
GET /graph-edges/,GET /graph-edges/count,GET /graph-edges/{edge_id},POST /graph-edges/,DELETE /graph-edges/{edge_id}
Segmentation and asynchronous processing
- Generic CRUD:
/segments GET /segments/{segment_id}/matched-profilesGET /segments/{segment_id}/matched-profiles/countGET /segments/{segment_id}/workflowPUT /segments/{segment_id}/workflow(atomically replace and reorder steps)POST /segments/{segment_id}/workflowPATCH /segments/{segment_id}/workflow/{workflow_id}DELETE /segments/{segment_id}/workflow/{workflow_id}
Workflow steps run in ascending execution_order. Each step may provide a
five-field schedule_definition cron override; when it is blank, the
workflow inherits the selected agent’s global cdp_ai_agents.schedule_definition.
PUT /segments/{segment_id}/workflow replaces the complete ordered list
atomically, which is the preferred endpoint for editors that reorder steps.
POST /segments/{segment_id}/recomputePOST /segments/admin/defaults/seedPOST /segments/admin/recompute-allGET /segments/admin/recompute-status/{run_id}GET /segments/segmentable-profile-attributesPOST /admin/crm/sync-segment/{segment_id}with optionaldry_run=trueGET /admin/crm/sync-runsGET /admin/crm/sync-runs/{sync_run_id}GET /analytics/source-analytics/statusPOST /analytics/source-analytics/processGET /analytics/source-analytics/status/{run_id}
Zalo OA administration
PUT /admin/zalo/connector-configGET /admin/zalo/oa-configGET /admin/zalo/oauth-urlPOST /admin/zalo/templates/syncGET /admin/zalo/templatesGET /admin/zalo/templates/{template_id}
Zalo credentials are tenant-scoped. The OAuth consent URL is generated by the
authenticated admin route; the browser callback is the public
GET /auth/zalo-redirect route and is bound to the tenant by its signed state.
9. QA and QC Test Guide
Minimum release gate
- Verify dev JWT and Keycloak bearer flows.
- Verify cross-tenant reads and writes are blocked.
- Verify MCP invalid-key (
401), Redis-unavailable (503), and valid-key tenant-scoping behavior. - Verify frontend payloads, route shapes, and approved MCP fields.
- Run the unit suite.
Run all service unit tests from customer360-api:
./run_unit_tests.shRun MCP-focused tests:
./.venv/bin/python -m pytest tests/test_mcp_app.py tests/test_mcp_auth.py -qThe unit runner installs the sibling customer360-dao and customer360-agent
checkouts, installs requirements.txt, sets SSO_LOGIN=true, and excludes
live deployment tests under tests/e2e.
10. Source of Truth
Implementation references:
customer360-api/app.pycustomer360-api/core/apps/http_api_app.pycustomer360-api/core/apps/mcp_app.pycustomer360-api/core/auth.pycustomer360-api/core/routers/*.pycustomer360-api/core/mcptools/*.pycustomer360-dao/src/leo_customer360_dao/config.py