Skip to content

Build Compliance Dashboards with the Metrics API ​

The Metrics API gives your own dashboards the numbers behind CryptoComply: onboarding volumes and turnaround, screening hits, alert and RFI queues, and open workload. Individuals and entities are covered by the same metrics.

EndpointWhat it does
GET /api/orgs/{org_id}/analytics/metrics/The catalog: the metrics and dimensions your organization can report on, and the report limits.
POST /api/orgs/{org_id}/analytics/report/Runs a report over a date range, bucketed by day, week or month, grouped and filtered.

Paths are relative to https://api.cryptocomply.xyz. Use current as {org_id} to mean the organization of your API key.

Authenticate ​

Ask the CryptoComply team for an API key with the analytics.read permission. It reads metrics and nothing else. Send it on every request, and keep it in a secret manager. See Authentication & API keys.

bash
curl https://api.cryptocomply.xyz/api/orgs/current/analytics/metrics/ \
  -H "Authorization: Api-Key $CRYPTOCOMPLY_API_KEY"

Discover the metrics ​

The catalog is the source of truth: read it instead of hard-coding. Each metric has an id to send in reports, a label for chart titles, a unit (count, ratio from 0 to 1, or seconds), the granularities it takes (empty for a current count), and the dimensions it can be grouped and filtered by. Each dimension lists its values. Metrics for optional features appear only if your organization uses them.

A sample of what is available:

AreaMetrics
Profilesprofiles.created, profiles.decided, profiles.approval_rate, profiles.time_to_decision_p50, profiles.time_to_decision_p90, profiles.pending_review
Cohorts (by arrival week or month)cohort.completion_rate, cohort.turnaround_p50, cohort.turnaround_p90
Screeningscreening.hits, screening.profiles_hit, screening.hits_resolved, screening.address_runs, screening.address_failure_rate
Blockchain addressesblockchain_addresses.added, blockchain_addresses.by_risk, blockchain_addresses.screened, blockchain_addresses.cannot_be_screened, blockchain_addresses.risk_rose, blockchain_addresses.time_to_first_screening_p50
Intake formsintake.submissions, intake.invitations_sent, intake.completion_rate, intake.time_to_submit_p50, intake.held_duplicates
Transaction alertstransaction_alerts.received, transaction_alerts.closed, transaction_alerts.time_to_decide_p50, transaction_alerts.override_rate
Alertsalerts.created, alerts.closed, alerts.time_to_close_p50, alerts.open
RFIsrfis.sent, rfis.answered, rfis.response_rate, rfis.response_time_p50, rfis.open, rfis.overdue
Workload and SLAsalerts.unassigned, reviews.overdue

Common dimensions: profile.type (individual or entity), profile.group, profile.review_status, profile.risk, alert.type (including blockchain_address for blockchain address alerts), alert.status, hit.kind, hit.resolution and review.age.

Run a report ​

Only metrics is required.

bash
curl -X POST https://api.cryptocomply.xyz/api/orgs/current/analytics/report/ \
  -H "Authorization: Api-Key $CRYPTOCOMPLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "metrics": ["profiles.created", "profiles.decided"],
    "dimensions": ["profile.type"],
    "filters": [{ "dimension": "profile.risk", "op": "in", "values": ["high", "severe"] }],
    "time": {
      "from": "2026-07-06",
      "to": "2026-09-27",
      "granularity": "week",
      "timezone": "America/New_York",
      "compare": "previous_period"
    },
    "include": ["totals"]
  }'
FieldWhat it does
metricsUp to 20 metric IDs. Each becomes a column.
dimensionsUp to 2 dimensions to group by. Every metric must take them.
filtersdimension, op (eq, in, not_in) and values. All must pass.
timefrom and to (both included), granularity (day, week, month), timezone (IANA, default UTC) and compare (previous_period). Without a range, the last 30 days.
order, limitSort by a metric, dimension or bucket; cap the rows (up to 5000).
include["totals"] recomputes each metric over the whole range.

The response has meta (range, timezone, granularity, rows_count, is_truncated, computed_at, is_cached), columns (labels and units for headers and formatting), rows and totals.

  • Empty buckets are filled with zeros; a rate or median with nothing behind it is null.
  • bucket is the first day of the bucket: a Monday for weeks, the 1st for months.
  • compare on each row holds the matching bucket of the previous period.
  • partial is true for the bucket still running; style it differently.
  • Use totals for headline numbers, especially rates and percentiles, which can't be summed from the buckets.
  • Durations are in seconds.
  • Reports are cached for up to an hour, so refreshing every few minutes is plenty.

Recipes ​

Onboarding funnel by profile type ​

Applications received and decided each week, split by profile type, with the previous period for comparison:

json
{
  "metrics": ["profiles.created", "profiles.decided"],
  "dimensions": ["profile.type"],
  "time": { "from": "2026-07-06", "to": "2026-09-27", "granularity": "week", "compare": "previous_period" },
  "include": ["totals"]
}

Chart one line per profile.type. Add a filter on profile.type to build a view for individuals or entities only. For turnaround, run profiles.time_to_decision_p50 and profiles.time_to_decision_p90 by month and divide by 86,400 for days.

Screening and alert workload ​

Hits found and alerts raised per week, then what is still open now:

json
{
  "metrics": ["screening.hits", "alerts.created", "alerts.closed"],
  "time": { "from": "2026-07-06", "to": "2026-09-27", "granularity": "week" }
}
json
{
  "metrics": ["alerts.open", "alerts.unassigned"],
  "dimensions": ["alert.type", "alert.status"]
}

Current counts such as alerts.open take no granularity. Group by alert.type to show watchlist, blockchain address, adverse media and duplicate alerts side by side, and use hit.kind on screening.hits to split sanctions, PEP and adverse media.

Open reviews by status and age (SLAs) ​

json
{
  "metrics": ["profiles.pending_review"],
  "dimensions": ["profile.review_status", "review.age"],
  "include": ["totals"]
}

review.age buckets profiles by days in their current status: 0_2, 3_7, 8_14 and 15_plus. Show it as a stacked bar or heat map, next to reviews.overdue and rfis.overdue as SLA indicators.

Limits and errors ​

A report covers at most 366 days, 2 dimensions, 20 metrics and 5000 rows; the catalog's limits gives the current values. An API key can call the analytics endpoints 30 times a minute; past that the API answers 429 with a Retry-After header.

Errors share one body with a detail, a stable code and a correlation_id. A refused report answers 422 with errors[] pointing at the field to fix.

StatuscodeFix
401auth.invalid_credentials, auth.token_expiredCheck the Authorization header and the key.
403auth.permission_deniedThe key needs analytics.read for this organization.
422analytics.unknown_metric, analytics.incompatible_dimension, analytics.invalid_filterUse IDs and values from the catalog; drop granularity for current counts.
422analytics.range_too_large, analytics.cardinality_exceededShorten the range or use fewer dimensions.
429rate.throttledWait the seconds in Retry-After.

See Errors for the codes every endpoint shares.

What's next? ​