Colony Colony Segmentation

Audience Segmentation API

Overview

The Audience Segmentation API analyses listener behaviour and automatically groups listeners into meaningful engagement segments. It combines RFM scoring, semantic vector search, AI inference, predictive intelligence and an AI agent layer to build a complete audience intelligence platform for radio stations.


Authentication

All /api/v1/* endpoints require an API key in the request header:

X-Segmentation-Key: <your-api-key>

Public routes — no authentication required: - GET / — landing page - GET /health — service health check - GET /docs — Swagger UI - GET /docs/api-guide — this guide - GET /redoc — ReDoc

Requests without a valid key return:

{"detail": "Invalid or missing API key"}

HTTP Status Codes

Code Meaning When
200 OK Request succeeded
201 Created Resource created (audiences)
400 Bad Request Invalid parameters
401 Unauthorized Missing or invalid X-Segmentation-Key
404 Not Found Company, listener or audience not found
422 Unprocessable Could not parse brief or invalid input
503 Service Unavailable Qdrant or OpenAI unavailable

Error response format

{
  "detail": "No profile found for 27821234567 at company 184"
}

Common error scenarios

Scenario Status Detail
Missing API key 401 "Invalid or missing API key"
Wrong API key 401 "Invalid or missing API key"
Unknown company 404 "No profiles found for company 99999"
Unknown listener 404 "No profile found for {originator} at company {id}"
Unknown audience 404 "Audience {id} not found for company {id}"
Already erased 400 "Profile already erased"
Brief too vague 422 "Could not parse brief — please provide more detail"
Search unavailable 503 "Search unavailable: {reason}"

How Segmentation Works

RFM Scoring

Score Measures Range
Recency Days since last interaction 1–5 (5 = most recent)
Frequency Interactions in last 30 days 1–5 (5 = highest)
Momentum Trend vs previous 30 days -1 / 0 / +1

Seven Primary Segments

Priority Segment Behaviour
0 new_listener First interaction within the last 7 days
1 super_fan Recent, frequent, multi-channel, stable/growing
2 show_loyalist Same show ≥60%, stable/growing
3 competition_chaser 70%+ competition entries, active within 60d
4 occasional_engager Active within 30d
5 at_risk Last seen 30–60d, declining momentum
6 dormant No recent engagement

How Demographics Are Populated

1. Survey answers — structured profile questions.

2. USSD/bot conversations — Q&A extraction, SA ID → gender + age auto-derived.

3. ORE signals — extracted from every interaction by live-core: - language — AWS Comprehend detection - location — AWS Comprehend location - gender — ore mining (stored as male / female) - display_name — WhatsApp/Telegram display name - profile_image — profile image URL

4. Free text extraction — SA ID number (13 digits) detected via regex. Gender and age group derived automatically.

5. AI inference — on demand when a producer views a listener. Cached 30 days.


Endpoints

Base URL: https://segmentation.live.colonyhq.com


GET /health — Public

{
  "status": "ok",
  "mongodb": "ok",
  "qdrant": "ok",
  "openai": "ok — model: gpt-4o-mini"
}

GET /api/v1/segments/{company_id}/summary

{
  "company_id": 184,
  "total_listeners": 136348,
  "high_engagement_count": 513,
  "segment_counts": {
    "new_listener": 11135,
    "super_fan": 38,
    "dormant": 120529
  }
}
Status Scenario
200 Success
404 No profiles found for company

GET /api/v1/segments/{company_id}/insights

Segment distribution with percentages and plain English explanations.

Status Scenario
200 Success
404 No profiles found for company

GET /api/v1/segments/{company_id}/tags

Tag distribution per segment with counts, percentages and descriptions.

Status Scenario
200 Success
404 No profiles found for company

GET /api/v1/segments/{company_id}/filter

Retargeting filter. Gender matching is case-insensitive (male/Male/female/Female).

Parameter Description
segment Filter by segment key
tags Comma-separated — must have ALL
gender Declared gender — case-insensitive
age_group Declared age group
location Partial match, case-insensitive
language Declared language
email Partial match, case-insensitive
name Partial match, case-insensitive
inferred_gender AI-inferred gender — case-insensitive
inferred_age_group AI-inferred age group
inferred_lsm AI-inferred LSM tier
inferred_interests Comma-separated — must have ALL
channel_type Primary channel ID only
has_channel Channel ID used at any point (not just primary)
timeslot_id Filter by show/timeslot
last_interaction_after ISO datetime — engaged after this date
last_interaction_before ISO datetime — engaged before this date
opt_in Marketing consent
page Page number (default: 1)
page_size Results per page (default: 100)
/filter?segment=dormant&inferred_gender=male&inferred_interests=automotive
/filter?gender=female&language=English&opt_in=true
Status Scenario
200 Success — returns empty list if no matches

GET /api/v1/segments/{company_id}/aggregate

Aggregated breakdown counts for the full filtered dataset — not just a page. Accepts the same filters as /filter. Use this to power dashboard charts so totals reflect the true audience size regardless of pagination.

Parameter Description
(all /filter params supported) segment, gender, age_group, location, language, email, name, has_channel, timeslot_id, last_interaction_after/before, etc.
/aggregate
/aggregate?segment=dormant
/aggregate?segment=super_fan&gender=Female
{
  "company_id": 184,
  "total": 107286,
  "channel_counts": {"12": 33010, "9": 13664, "0": 14212},
  "gender_counts": {"other": 107280, "male": 5, "female": 1},
  "age_group_counts": {"Other": 107281, "35-49": 2},
  "language_counts": {"other": 107278, "english": 8},
  "location_counts": {"Other": 106621, "Johannesburg": 5},
  "segment_counts": {"dormant": 107279, "new_listener": 6},
  "momentum_counts": {"0": 107279, "1": 7},
  "campaign_type_counts": {"message_board": 95945, "competition": 11193}
}

Gender and language keys are normalised to lowercase (Male/male merge into male). Location is capped at the top 20 by count.

Status Scenario
200 Success — counts are 0 if no matches

GET /api/v1/segments/{company_id}/export

Returns all matching listener records for a filter set — no pagination. Intended for bulk CSV export and campaign targeting.

Parameter Description
(all /filter params supported) segment, gender, location, opt_in, etc.
channel_pair Comma-separated pair of channel_type ints — e.g. 9,0 — returns only listeners active on both channels
/export?segment=dormant
/export?segment=super_fan&opt_in=true
/export?channel_pair=9,0
{
  "company_id": 184,
  "total": 1,
  "listeners": [
    {
      "originator": "27724430967",
      "originator_type": "mobile_number",
      "current_segment": "super_fan",
      "gender": "Male",
      "age_group": "18-24",
      "location": "Johannesburg",
      "language": "English",
      "primary_channel_type": 9,
      "opt_in": true,
      "permission": true,
      "tags": ["drive_time_listener", "campaign_loyal"],
      "recency_days": 0,
      "interactions_total": 5492,
      "inferred_lsm": null
    }
  ]
}
Status Scenario
200 Success — returns empty list if no matches
400 channel_pair malformed — must be two comma-separated integers

GET /api/v1/segments/{company_id}/cross-channel

Cross-channel co-engagement matrix. For every pair of channels, returns how many listeners are active on both, plus per-channel unique totals and percentages. Powers the Cross Platform Engagement grid.

Parameter Description
(all /filter params supported) segment, gender, location, opt_in, etc.
/cross-channel
/cross-channel?segment=super_fan
{
  "company_id": 184,
  "total": 107286,
  "channels": [0, 9, 12],
  "uniques": {"9": 13664, "0": 14212, "12": 33010},
  "matrix": {"9": {"0": 1200, "12": 800}, "0": {"9": 1200}},
  "percentages": {"9": {"0": 1.1, "12": 0.7}, "0": {"9": 1.1}}
}

Read matrix["9"]["0"] = 1200 as: 1,200 listeners are active on both WhatsApp (9) and SMS (0). percentages["9"]["0"] is that count as a percentage of the total filtered audience — use it for tooltips like "1.1% of your audience engaged on both WhatsApp and SMS."

Status Scenario
200 Success — empty matrix if no matches

GET /api/v1/segments/{company_id}/search

Semantic search. Qdrant finds by meaning — MongoDB narrows by demographics.

Parameter Description
q Natural language query (required)
min_score Minimum similarity score 0.0–1.0 (default: 0.68)
limit Max results (default: 100, max: 1000)
segment Filter by segment — applied in Qdrant
gender Filter by declared gender — applied in MongoDB
age_group Filter by declared age group — applied in MongoDB
location Filter by declared location — applied in MongoDB
language Filter by declared language — applied in MongoDB
/search?q=music lovers
/search?q=car enthusiasts&segment=dormant&min_score=0.65
Status Scenario
200 Success — returns empty list if no matches
400 Query parameter q is missing
503 Qdrant unavailable

GET /api/v1/listeners/{company_id}/{originator}

Full behavioural profile including ORE-enriched demographics.

{
  "demographics": {
    "name": "Ozias Munyamela",
    "display_name": "Ozias Munyamela",
    "gender": "male",
    "age_group": "35-49",
    "location": "Johannesburg",
    "language": "English",
    "profile_image": "https://..."
  }
}
Status Scenario
200 Success
404 Listener not found

DELETE /api/v1/listeners/{company_id}/{originator}

POPIA erasure. Clears all PII, keeps anonymised behavioural data. Cannot be undone.

{
  "erased": true,
  "originator_hash": "sha256:a3f9c2d1...",
  "erased_at": "2026-08-22T10:00:00Z"
}
Status Scenario
200 Successfully erased
400 Profile already erased
404 Listener not found

GET /api/v1/ai/segments/{company_id}/insights

AI-powered plain English insights for all segments.

Status Scenario
200 Success
404 No profiles found for company

GET /api/v1/ai/listeners/{company_id}/{originator}

AI demographic inference and interest tagging. Cached 30 days — no charge on repeat calls. Response includes "cached": true/false.

Status Scenario
200 Success
404 Listener not found

PUT /api/v1/ai/listeners/{company_id}/{originator}/promote-demographics

Promotes AI-inferred to declared. Only where declared is null and confidence ≥ 0.70.

{
  "promoted": {"gender": "female", "age_group": "25-34"},
  "skipped": {"income_bracket": "confidence too low (0.60)"},
  "message": "2 field(s) promoted to declared demographics"
}
Status Scenario
200 Success — promoted may be empty if nothing eligible
404 Listener not found

POST /api/v1/ai/segments/{company_id}/custom-segment

Find listeners using plain English. Stateless — use Audiences for persistence.

{"theme": "pet lovers", "description": "listeners who own dogs or cats", "limit": 200}
Status Scenario
200 Success — returns empty list if no matches
404 No profiles found for company

GET /api/v1/intelligence/listeners/{company_id}/{originator}/churn-risk

{
  "churn_risk": "high",
  "churn_risk_score": 75,
  "churn_signals": [
    "Engagement momentum is declining",
    "Last engaged 45 days ago"
  ]
}

Risk levels: low (0–29) / medium (30–59) / high (60–100)

Status Scenario
200 Success
404 Listener not found

GET /api/v1/intelligence/listeners/{company_id}/{originator}/similar

Behaviourally similar listeners via Qdrant cosine similarity.

/similar?limit=20&min_score=0.75&exclude_segment=dormant
Status Scenario
200 Success — returns empty list if no similar listeners
404 Listener not found

GET /api/v1/intelligence/segments/{company_id}/{segment_key}/profile

Composite demographic and behavioural profile for a segment.

{
  "segment": "dormant",
  "total_in_segment": 120529,
  "channels": {"primary": "USSD"},
  "top_campaigns": [{"title": "Parmalat", "total_interactions": 336}]
}
Status Scenario
200 Success
404 No listeners in segment

POST /api/v1/agent/{company_id}/recommend

AI campaign recommendation from an advertiser brief. Results cached 24 hours — same brief returns cached result instantly. Response includes "cached": true/false.

{
  "brief": "Caltex fuel advertiser targeting male drivers aged 25-49 in Gauteng",
  "sample_size": 500
}
Status Scenario
200 Success
404 No profiles found for company

POST /api/v1/agent/{company_id}/parse-brief

Parses an advertiser brief into structured targeting criteria.

{"brief": "FMCG brand targeting female listeners aged 25-34 in Gauteng"}
Status Scenario
200 Success
422 Brief too vague to parse

POST /api/v1/audiences/{company_id}

Creates a named persistent custom audience.

{"name": "Colony Campaigns Pet Lovers", "theme": "pet lovers", "limit": 500, "expiry_days": 30}
Status Scenario
200 Success

GET /api/v1/audiences/{company_id}

Lists all custom audiences — member lists excluded for performance.

Status Scenario
200 Success — returns empty list if none

GET /api/v1/audiences/{company_id}/{audience_id}

Returns audience with full member list enriched with profile data.

Status Scenario
200 Success
404 Audience not found

PUT /api/v1/audiences/{company_id}/{audience_id}/refresh

Re-runs search strategy, updates members with latest data.

Status Scenario
200 Success
404 Audience not found

DELETE /api/v1/audiences/{company_id}/{audience_id}

Deletes a custom audience. Cannot be undone.

Status Scenario
200 Successfully deleted
404 Audience not found

Channel Type Reference

channel_type Channel
0, 11, 15, 16, 17, 18 SMS
1 Email
2 Twitter
3 Facebook
8 USSD
9 WhatsApp
10 Instagram
12 Telegram
19 Survey
20 Data import
22 TikTok

Phased Roadmap

Phase 1 — Behaviour-led segmentation     ✓ Complete
Phase 2 — Semantic search                ✓ Complete
Phase 3 — AI inference layer             ✓ Complete
Phase 4 — Predictive intelligence        ✓ Complete
Phase 5 — AI agent layer                 ✓ Complete
Phase 6 — Full-dataset aggregation       ✓ Complete