Skip to main content

Overview

The Customer Portal allows you to create custom, customer-facing dashboards that demonstrate the tangible value your AI agent delivers. In the AI agent era, proving value is critical for sales conversion, renewals, and upsells—but it’s harder than traditional SaaS because AI outcomes are probabilistic, ROI is difficult to isolate, and trust matters more than ever. Base URL: https://api.adenhq.com/v1/portal

Why Customer Portals Matter for AI Agents

Unlike traditional SaaS which sells deterministic features, AI agents sell probabilistic outcomes. This creates unique challenges in proving value:

The Challenge

  • Value is Probabilistic: Agents deliver “improved outcomes” rather than guaranteed features, making ROI harder to predict upfront
  • Attribution is Complex: Agent impact blends with human performance, data quality, and process maturity
  • Trust and Control: Buyers need reassurance about hallucinations, brand risk, and autonomy concerns that didn’t exist in classic SaaS

The Solution

Customer Portals provide a low-implementation way to produce “proof of value” by:
  1. Demonstrating what the agent did with usage metrics and activity logs
  2. Quantifying impact through savings, revenue generated, and efficiency gains
  3. Building trust with transparent, customer-specific metrics
  4. Supporting renewals & upsells with ongoing value narration

Core Capabilities

1. Custom Dashboard Configuration

Select metrics from your metrics store, segment by customer, and create branded dashboards that can be embedded directly into your product.

2. Automated Value Reports

Schedule regular reports (daily, weekly, monthly) delivered via email or webhooks to keep customers informed of their agent’s performance.

3. Customer-Specific Metrics

All metrics are automatically segmented by customer_id, ensuring each customer only sees their own data with proper isolation and security.

4. Embeddable & Secure

Dashboards can be embedded in your application with whitelisting and authentication, or accessed via unique customer links.

Authentication

All API requests require authentication via API key in the header:
Authorization: Bearer sk-ant-xxxxxx
x-api-version: 2025-01-06

Data Models

Portal Configuration

interface PortalConfig {
  portal_id: string                       // Unique identifier
  name: string                            // Internal name
  customer_id: string                     // The customer this portal is for
  status: "draft" | "active" | "archived"

  branding: {
    title: string                         // Dashboard title
    logo_url?: string                     // Custom logo
    primary_color?: string                // Hex color code
    theme?: "light" | "dark" | "auto"
  }

  metrics: PortalMetric[]                 // Selected metrics to display
  layout: LayoutConfig                    // Dashboard layout

  access: {
    embed_enabled: boolean                // Allow embedding
    allowed_domains?: string[]            // Domain whitelist for embedding
    public_link?: string                  // Shareable link (if enabled)
    password_protected?: boolean
  }

  created_at: string                      // ISO 8601
  updated_at: string                      // ISO 8601
}

interface PortalMetric {
  metric_id: string                       // From metrics catalog or custom
  display_name: string                    // Override display name
  position: { row: number; col: number }  // Grid position
  size: { width: number; height: number } // Grid units
  chart_type: "line" | "bar" | "number" | "pie" | "table"

  time_range?: {
    default: "24h" | "7d" | "30d" | "90d" | "ytd"
    allow_user_selection: boolean
  }

  comparison?: {
    enabled: boolean                      // Show period-over-period
    period: "previous" | "ytd" | "yoy"
  }

  target?: {
    value: number                         // Target/goal value
    label: string                         // e.g., "Monthly Goal"
  }
}

interface LayoutConfig {
  grid_columns: number                    // 12, 16, or 24
  sections: LayoutSection[]
}

interface LayoutSection {
  title?: string                          // Section header
  collapsible?: boolean
  metrics: string[]                       // metric_ids in this section
}

Value Report Configuration

interface ReportConfig {
  report_id: string
  portal_id: string                       // Associated portal
  name: string
  schedule: {
    frequency: "daily" | "weekly" | "monthly"
    day_of_week?: number                  // 0-6 for weekly
    day_of_month?: number                 // 1-31 for monthly
    time: string                          // HH:mm in UTC
    timezone?: string                     // IANA timezone
  }

  delivery: {
    method: "email" | "webhook" | "both"
    recipients?: string[]                 // Email addresses
    webhook_url?: string
    webhook_headers?: Record<string, string>
  }

  content: {
    include_summary: boolean              // Executive summary
    include_charts: boolean               // Chart images
    include_raw_data: boolean             // CSV attachment
    comparison_period?: "previous" | "ytd"
  }

  is_active: boolean
  created_at: string
  updated_at: string
  last_sent_at?: string
}

Portal Access Token

interface PortalAccessToken {
  token: string                           // JWT token for accessing portal
  portal_id: string
  customer_id: string
  expires_at: string                      // ISO 8601
  permissions: string[]                   // e.g., ["read:metrics", "export:data"]
}

API Endpoints

1. Create Portal Configuration

Create a new customer portal.
POST /portal/configs
Request Body:
{
  "name": "Acme Corp Dashboard",
  "customer_id": "cust_acme_123",
  "branding": {
    "title": "Your AI Agent Performance",
    "logo_url": "https://example.com/logo.png",
    "primary_color": "#2563EB",
    "theme": "light"
  },
  "metrics": [
    {
      "metric_id": "usage.requests.total",
      "display_name": "Total Requests Handled",
      "position": { "row": 0, "col": 0 },
      "size": { "width": 6, "height": 2 },
      "chart_type": "number",
      "comparison": {
        "enabled": true,
        "period": "previous"
      }
    },
    {
      "metric_id": "cost.total",
      "display_name": "Your Investment",
      "position": { "row": 0, "col": 6 },
      "size": { "width": 6, "height": 2 },
      "chart_type": "number",
      "comparison": {
        "enabled": true,
        "period": "previous"
      }
    },
    {
      "metric_id": "autonomy.deflection_rate",
      "display_name": "Self-Service Success Rate",
      "position": { "row": 2, "col": 0 },
      "size": { "width": 12, "height": 4 },
      "chart_type": "line",
      "time_range": {
        "default": "30d",
        "allow_user_selection": true
      },
      "target": {
        "value": 85,
        "label": "Target: 85% Deflection"
      }
    },
    {
      "metric_id": "cm_roi_per_session",
      "display_name": "ROI per Customer Interaction",
      "position": { "row": 6, "col": 0 },
      "size": { "width": 6, "height": 3 },
      "chart_type": "bar"
    }
  ],
  "access": {
    "embed_enabled": true,
    "allowed_domains": ["*.acme.com"]
  }
}
Response: 201 Created
{
  "portal_id": "portal_acme_dashboard",
  "name": "Acme Corp Dashboard",
  "customer_id": "cust_acme_123",
  "status": "draft",
  "public_link": "https://portal.adenhq.com/p/portal_acme_dashboard",
  "embed_code": "<iframe src=\"https://portal.adenhq.com/embed/portal_acme_dashboard\" width=\"100%\" height=\"800\" frameborder=\"0\"></iframe>",
  "created_at": "2025-01-06T10:00:00Z"
}

2. Get Portal Configuration

Retrieve a portal configuration.
GET /portal/configs/{portal_id}
Response: 200 OK
{
  "portal_id": "portal_acme_dashboard",
  "name": "Acme Corp Dashboard",
  "customer_id": "cust_acme_123",
  "status": "active",
  "branding": { ... },
  "metrics": [ ... ],
  "access": { ... },
  "created_at": "2025-01-06T10:00:00Z",
  "updated_at": "2025-01-06T11:30:00Z"
}

3. Update Portal Configuration

Update an existing portal.
PATCH /portal/configs/{portal_id}
Request Body (partial update):
{
  "status": "active",
  "metrics": [
    {
      "metric_id": "quality.sentiment_shift",
      "display_name": "Customer Satisfaction Trend",
      "position": { "row": 9, "col": 0 },
      "size": { "width": 12, "height": 3 },
      "chart_type": "line"
    }
  ]
}
Response: 200 OK

4. List Portal Configurations

List all portals, optionally filtered by customer.
GET /portal/configs
Query Parameters:
ParameterTypeDefaultDescription
customer_idstringallFilter by specific customer
statusstringallFilter by status: draft, active, archived
limitinteger50Results per page
cursorstring-Pagination cursor
Response: 200 OK
{
  "data": [
    {
      "portal_id": "portal_acme_dashboard",
      "name": "Acme Corp Dashboard",
      "customer_id": "cust_acme_123",
      "status": "active",
      "metrics_count": 4,
      "last_accessed": "2025-01-06T09:45:00Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null,
    "total": 1
  }
}

5. Generate Portal Access Token

Generate a JWT token for accessing a customer portal.
POST /portal/configs/{portal_id}/tokens
Request Body:
{
  "expires_in": 3600,                     // Seconds (default: 3600)
  "permissions": ["read:metrics", "export:data"]
}
Response: 201 Created
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_at": "2025-01-06T12:00:00Z",
  "portal_url": "https://portal.adenhq.com/p/portal_acme_dashboard?token=eyJhbG..."
}
Usage: Tokens can be used to authenticate portal access:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  https://portal.adenhq.com/p/portal_acme_dashboard

6. Get Portal Data

Retrieve the actual metric data for a portal (used by embedded dashboards).
GET /portal/configs/{portal_id}/data
Query Parameters:
ParameterTypeDefaultDescription
startstring30d agoStart time (ISO 8601)
endstringnowEnd time (ISO 8601)
metric_idsstringallComma-separated list of specific metrics
Response: 200 OK
{
  "portal_id": "portal_acme_dashboard",
  "customer_id": "cust_acme_123",
  "period": {
    "start": "2024-12-07T00:00:00Z",
    "end": "2025-01-06T10:00:00Z"
  },
  "data": [
    {
      "metric_id": "usage.requests.total",
      "display_name": "Total Requests Handled",
      "value": 45230,
      "unit": "count",
      "change": 12.5,
      "change_period": "vs previous 30d",
      "chart_data": [
        { "timestamp": "2024-12-07T00:00:00Z", "value": 1420 },
        { "timestamp": "2024-12-08T00:00:00Z", "value": 1538 }
        // ... more data points
      ]
    },
    {
      "metric_id": "cost.total",
      "display_name": "Your Investment",
      "value": 1547.82,
      "unit": "usd",
      "change": 8.3,
      "change_period": "vs previous 30d",
      "chart_data": null
    }
  ],
  "generated_at": "2025-01-06T10:00:00Z"
}

7. Create Value Report

Configure an automated value report.
POST /portal/reports
Request Body:
{
  "portal_id": "portal_acme_dashboard",
  "name": "Monthly Value Report - Acme Corp",
  "schedule": {
    "frequency": "monthly",
    "day_of_month": 1,
    "time": "09:00",
    "timezone": "America/New_York"
  },
  "delivery": {
    "method": "email",
    "recipients": [
      "ceo@acme.com",
      "cto@acme.com"
    ]
  },
  "content": {
    "include_summary": true,
    "include_charts": true,
    "include_raw_data": true,
    "comparison_period": "previous"
  }
}
Response: 201 Created
{
  "report_id": "report_acme_monthly",
  "portal_id": "portal_acme_dashboard",
  "name": "Monthly Value Report - Acme Corp",
  "schedule": { ... },
  "delivery": { ... },
  "is_active": true,
  "next_scheduled": "2025-02-01T09:00:00-05:00",
  "created_at": "2025-01-06T10:00:00Z"
}

8. Send Report Now

Trigger an immediate report delivery (for testing or ad-hoc sends).
POST /portal/reports/{report_id}/send
Response: 202 Accepted
{
  "report_id": "report_acme_monthly",
  "status": "queued",
  "estimated_delivery": "2025-01-06T10:05:00Z"
}

9. List Reports

Get all configured reports for a portal.
GET /portal/reports
Query Parameters:
ParameterTypeDefaultDescription
portal_idstring-Filter by portal
is_activebooleanallFilter by active status
Response: 200 OK
{
  "data": [
    {
      "report_id": "report_acme_monthly",
      "portal_id": "portal_acme_dashboard",
      "name": "Monthly Value Report - Acme Corp",
      "schedule": { "frequency": "monthly", "day_of_month": 1 },
      "is_active": true,
      "last_sent_at": "2025-01-01T09:00:00Z",
      "next_scheduled": "2025-02-01T09:00:00Z"
    }
  ],
  "total": 1
}

10. Update Report Configuration

Modify report settings.
PATCH /portal/reports/{report_id}
Request Body:
{
  "is_active": false,
  "schedule": {
    "frequency": "weekly",
    "day_of_week": 1,
    "time": "08:00"
  }
}

11. Delete Report

Remove a scheduled report.
DELETE /portal/reports/{report_id}
Response: 200 OK
{
  "deleted": true,
  "report_id": "report_acme_monthly"
}

12. Configure Webhook for Portal Events

Set up webhooks to receive notifications about portal activity.
POST /portal/webhooks
Request Body:
{
  "portal_id": "portal_acme_dashboard",
  "url": "https://api.acme.com/webhooks/aden-portal",
  "events": [
    "portal.accessed",
    "portal.data_exported",
    "report.delivered",
    "metric.threshold_crossed"
  ],
  "headers": {
    "X-Webhook-Secret": "your_webhook_secret"
  }
}
Response: 201 Created Webhook Payload Example:
{
  "event": "portal.accessed",
  "timestamp": "2025-01-06T10:30:00Z",
  "data": {
    "portal_id": "portal_acme_dashboard",
    "customer_id": "cust_acme_123",
    "user_ip": "203.0.113.42",
    "session_duration": 342,
    "pages_viewed": 3
  }
}

SDK Examples

Python SDK

from aden import PortalClient

client = PortalClient(api_key="sk-ant-xxxxxx")

# Create a customer portal
portal = client.portals.create(
    name="Acme Corp Dashboard",
    customer_id="cust_acme_123",
    branding={
        "title": "Your AI Agent Performance",
        "primary_color": "#2563EB",
        "theme": "light"
    },
    metrics=[
        {
            "metric_id": "usage.requests.total",
            "display_name": "Total Requests Handled",
            "position": {"row": 0, "col": 0},
            "size": {"width": 6, "height": 2},
            "chart_type": "number",
            "comparison": {"enabled": True, "period": "previous"}
        },
        {
            "metric_id": "autonomy.deflection_rate",
            "display_name": "Self-Service Success Rate",
            "position": {"row": 2, "col": 0},
            "size": {"width": 12, "height": 4},
            "chart_type": "line",
            "time_range": {"default": "30d", "allow_user_selection": True},
            "target": {"value": 85, "label": "Target: 85% Deflection"}
        }
    ],
    access={
        "embed_enabled": True,
        "allowed_domains": ["*.acme.com"]
    }
)

print(f"Portal created: {portal.public_link}")
print(f"Embed code: {portal.embed_code}")

# Activate the portal
client.portals.update(portal.portal_id, status="active")

# Generate an access token
token = client.portals.create_token(
    portal.portal_id,
    expires_in=86400  # 24 hours
)

# Send token to customer
send_email_to_customer(
    to="ceo@acme.com",
    subject="Your AI Agent Performance Dashboard",
    portal_url=token.portal_url
)

# Set up automated monthly reports
report = client.reports.create(
    portal_id=portal.portal_id,
    name="Monthly Value Report",
    schedule={
        "frequency": "monthly",
        "day_of_month": 1,
        "time": "09:00",
        "timezone": "America/New_York"
    },
    delivery={
        "method": "email",
        "recipients": ["ceo@acme.com", "cto@acme.com"]
    },
    content={
        "include_summary": True,
        "include_charts": True,
        "comparison_period": "previous"
    }
)

print(f"Report scheduled: {report.next_scheduled}")

TypeScript SDK

import { PortalClient } from '@aden/sdk';

const client = new PortalClient({ apiKey: 'sk-ant-xxxxxx' });

// Create a customer portal
const portal = await client.portals.create({
  name: 'Acme Corp Dashboard',
  customerId: 'cust_acme_123',
  branding: {
    title: 'Your AI Agent Performance',
    primaryColor: '#2563EB',
    theme: 'light'
  },
  metrics: [
    {
      metricId: 'usage.requests.total',
      displayName: 'Total Requests Handled',
      position: { row: 0, col: 0 },
      size: { width: 6, height: 2 },
      chartType: 'number',
      comparison: { enabled: true, period: 'previous' }
    },
    {
      metricId: 'cost.total',
      displayName: 'Your Investment',
      position: { row: 0, col: 6 },
      size: { width: 6, height: 2 },
      chartType: 'number',
      comparison: { enabled: true, period: 'previous' }
    }
  ],
  access: {
    embedEnabled: true,
    allowedDomains: ['*.acme.com']
  }
});

console.log(`Portal created: ${portal.publicLink}`);

// Generate access token
const token = await client.portals.createToken(portal.portalId, {
  expiresIn: 86400 // 24 hours
});

// Embed in your React application
const PortalEmbed: React.FC = () => {
  return (
    <iframe
      src={`https://portal.adenhq.com/embed/${portal.portalId}?token=${token.token}`}
      width="100%"
      height="800"
      frameBorder="0"
      style={{ border: 'none' }}
    />
  );
};

// Set up webhook to track portal engagement
await client.webhooks.create({
  portalId: portal.portalId,
  url: 'https://api.acme.com/webhooks/aden-portal',
  events: ['portal.accessed', 'portal.data_exported'],
  headers: {
    'X-Webhook-Secret': process.env.WEBHOOK_SECRET
  }
});

Minimal Integration Example

For quick setup with minimal code (< 20 lines):
import { PortalClient } from '@aden/sdk';

const client = new PortalClient({ apiKey: process.env.ADEN_API_KEY });

// Create portal with default metrics
const portal = await client.portals.createQuick({
  customerId: 'cust_acme_123',
  title: 'Your Agent Performance',
  presetMetrics: ['standard_dashboard'] // Uses predefined metric set
});

// Activate and share
await client.portals.activate(portal.portalId);
const token = await client.portals.createToken(portal.portalId);

// Send to customer
await sendEmail({
  to: customer.email,
  template: 'portal_access',
  data: { portalUrl: token.portalUrl }
});

Before/After Comparisons (Optional)

For showing the impact of your agent versus the baseline, you can configure comparison metrics:
interface ComparisonMetric {
  metric_id: string
  display_name: string
  baseline: {
    label: string                         // e.g., "Before Agent"
    value: number                         // Baseline value
    source: "manual" | "historical"       // How baseline was determined
  }
  current: {
    label: string                         // e.g., "With Agent"
    metric_id: string                     // Current metric to compare
  }
  improvement: {
    show_percentage: boolean
    show_absolute: boolean
    highlight_positive: boolean           // Green for increase, red for decrease
  }
}
Example Usage:
{
  "metric_id": "comparison_response_time",
  "display_name": "Response Time Improvement",
  "baseline": {
    "label": "Before Agent (Manual)",
    "value": 2400,
    "source": "manual"
  },
  "current": {
    "label": "With Agent",
    "metric_id": "perf.latency.avg"
  },
  "improvement": {
    "show_percentage": true,
    "show_absolute": true,
    "highlight_positive": true
  }
}
This would render as: “Response time improved from 2400ms to 312ms (87% faster)“

Security & Data Isolation

Customer Data Isolation

All metrics are automatically filtered by customer_id. When a portal is accessed:
  1. The system validates the customer_id associated with the portal
  2. All metric queries are automatically scoped to that customer
  3. No cross-customer data is ever exposed

Embedding Security

When embedding portals:
  • Domain Whitelisting: Only allowed domains can embed the portal
  • Token-Based Access: Short-lived JWT tokens authenticate each session
  • CORS Protection: Proper CORS headers prevent unauthorized access
  • Content Security Policy: CSP headers prevent XSS attacks

Access Control

Tokens can have granular permissions:
  • read:metrics - View metric data
  • export:data - Download raw data as CSV
  • configure:portal - Modify portal settings (admin only)

Error Responses

{
  "error": {
    "type": "validation_error",
    "code": "INVALID_CUSTOMER_ID",
    "message": "Customer ID 'cust_invalid' does not exist",
    "param": "customer_id",
    "documentation_url": "https://docs.adenhq.com/errors/INVALID_CUSTOMER_ID"
  }
}

Error Codes

HTTP StatusError CodeDescription
400INVALID_METRIC_IDOne or more metric IDs are invalid
400INVALID_LAYOUTLayout configuration is malformed
401UNAUTHORIZEDInvalid or expired API key
403FORBIDDENInsufficient permissions
404PORTAL_NOT_FOUNDPortal does not exist
404CUSTOMER_NOT_FOUNDCustomer ID not found
409PORTAL_ALREADY_EXISTSPortal for this customer already exists
429RATE_LIMITEDToo many requests

Rate Limits

Endpoint CategoryRequests per Minute
Portal Config (GET)300
Portal Data (GET)600
Portal Config (POST/PATCH)60
Token Generation120
Report Operations30

Support

For API support and questions, contact contact@adenhq.com.