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.
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"}
| 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 |
{
"detail": "No profile found for 27821234567 at company 184"
}
| 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}" |
| 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 |
| 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 |
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.
Base URL: https://segmentation.live.colonyhq.com
{
"status": "ok",
"mongodb": "ok",
"qdrant": "ok",
"openai": "ok — model: gpt-4o-mini"
}
{
"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 |
Segment distribution with percentages and plain English explanations.
| Status | Scenario |
|---|---|
200 |
Success |
404 |
No profiles found for company |
Tag distribution per segment with counts, percentages and descriptions.
| Status | Scenario |
|---|---|
200 |
Success |
404 |
No profiles found for company |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
AI-powered plain English insights for all segments.
| Status | Scenario |
|---|---|
200 |
Success |
404 |
No profiles found for company |
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 |
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 |
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 |
{
"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 |
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 |
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 |
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 |
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 |
Creates a named persistent custom audience.
{"name": "Colony Campaigns Pet Lovers", "theme": "pet lovers", "limit": 500, "expiry_days": 30}
| Status | Scenario |
|---|---|
200 |
Success |
Lists all custom audiences — member lists excluded for performance.
| Status | Scenario |
|---|---|
200 |
Success — returns empty list if none |
Returns audience with full member list enriched with profile data.
| Status | Scenario |
|---|---|
200 |
Success |
404 |
Audience not found |
Re-runs search strategy, updates members with latest data.
| Status | Scenario |
|---|---|
200 |
Success |
404 |
Audience not found |
Deletes a custom audience. Cannot be undone.
| Status | Scenario |
|---|---|
200 |
Successfully deleted |
404 |
Audience not found |
| channel_type | Channel |
|---|---|
| 0, 11, 15, 16, 17, 18 | SMS |
| 1 | |
| 2 | |
| 3 | |
| 8 | USSD |
| 9 | |
| 10 | |
| 12 | Telegram |
| 19 | Survey |
| 20 | Data import |
| 22 | TikTok |
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