Endpoint reference
Every public API endpoint, its parameters, and its exact response shape.
All endpoints are GET, under https://api.ardent.africa/public/v1. List endpoints
return { data, page, limit, total }. Detail endpoints return the resource object.
Send x-api-key for the higher rate tier (optional).
A machine-readable OpenAPI 3.1 spec of everything below is served
at https://api.ardent.africa/public/v1/openapi.json.
Common list parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
page | integer | 1 | 1-based page number |
limit | integer | 20 | max 50 |
All endpoints (generated)
Generated from the live /public/v1 API contract. Response shapes and examples are documented per resource in the sections below.
| Method | Path | Handler |
|---|---|---|
| GET | /public/v1/durbars | List durbars |
| GET | /public/v1/durbars/:slug | Get durbar |
| GET | /public/v1/durbars/:slug/transcript | Get transcript |
| GET | /public/v1/events | List events |
| GET | /public/v1/events/:slug | Get event |
| GET | /public/v1/jobs | List jobs |
| GET | /public/v1/jobs/:slug | Get job |
| GET | /public/v1/marketplace/services | List services |
| GET | /public/v1/marketplace/services/:id | Get service |
| GET | /public/v1/marketplace/categories | List categories |
| GET | /public/v1/openapi.json | Get open api |
| GET | /public/v1/reviews/entities | List entities |
| GET | /public/v1/reviews/entities/:slug | Get entity |
| GET | /public/v1/safety/hazards | List hazards |
| GET | /public/v1/safety/hazards/:caseNumber | Get hazard |
| GET | /public/v1/actors | List actors |
| GET | /public/v1/actors/:handle | Get actor |
| GET | /public/v1/posts | List posts |
| GET | /public/v1/posts/:id | Get post |
| GET | /public/v1/groups | List groups |
| GET | /public/v1/groups/:slug | Get group |
| GET | /public/v1/surveys | List surveys |
| GET | /public/v1/surveys/:slug | Get survey |
| GET | /public/v1/campaigns | List campaigns |
| GET | /public/v1/campaigns/:slug | Get campaign |
| GET | /public/v1/petitions | List petitions |
| GET | /public/v1/petitions/:slug | Get petition |
| GET | /public/v1/blog | List blog |
| GET | /public/v1/blog/:slug | Get blog post |
| GET | /public/v1/profiles/:username | Get profile |
| GET | /public/v1/stats | Get stats |
Campaigns
GET/public/v1/campaigns
Lists active and won fundraising campaigns. Extra params: category, country
(target country code, for example GH).
curl "https://api.ardent.africa/public/v1/campaigns?limit=2&category=Education"{
"data": [
{
"id": "c7ee417f-...",
"slug": "clean-water-for-tamale",
"title": "Clean water for Tamale",
"description": "...",
"category": "Education",
"status": "active",
"goal_amount": 50000,
"raised_amount": 12500,
"image_url": "https://.../image.jpg",
"country": "Ghana",
"target_country": "GH",
"target_country_name": "Ghana",
"progress": { "raised_amount": 12500, "goal_amount": 50000, "percent": 25 },
"goal_reached_at": null,
"deadline": null,
"created_at": "2026-06-13T23:31:27.767Z"
}
],
"page": 1,
"limit": 2,
"total": 6
}/public/v1/campaigns/:slug
A single campaign, plus its public updates. Same fields as above, with an updates
array (id, title, content (sanitized HTML), media_urls, created_at).
{
"id": "c7ee417f-...",
"slug": "clean-water-for-tamale",
"title": "Clean water for Tamale",
"progress": { "raised_amount": 12500, "goal_amount": 50000, "percent": 25 },
"updates": [
{ "id": "...", "title": "Drilling begins", "content": "<p>...</p>", "media_urls": [], "created_at": "2026-06-14T..." }
]
}There are no creator, donor, or internal fields. Only active or won campaigns are
returned; anything else gives 404.
Marketplace
GET/public/v1/marketplace/services
Lists active marketplace listings. Extra params: q (full-text search), listing_type
(service or goods), category (category slug), location.
curl "https://api.ardent.africa/public/v1/marketplace/services?limit=2&listing_type=service"{
"data": [
{
"id": "9b1e...",
"slug": "wedding-photography-accra",
"title": "Wedding photography in Accra",
"description": "...",
"listing_type": "service",
"category_slugs": ["photography"],
"price_min_pesewas": 150000,
"price_max_pesewas": 500000,
"currency": "GHS",
"location": "Accra",
"rating": 4.8,
"review_count": 23,
"is_verified": true,
"is_featured": false,
"cover_url": "https://.../cover.jpg",
"supports_campaign_id": null,
"created_at": "2026-06-14T..."
}
],
"page": 1,
"limit": 2,
"total": 12
}/public/v1/marketplace/services/:id
A single active listing. Adds public packages (each name, price_pesewas,
currency, description, delivery_days), portfolio_urls, and languages. Provider
contact details and identity are never returned.
{
"id": "9b1e...",
"slug": "wedding-photography-accra",
"title": "Wedding photography in Accra",
"rating": 4.8,
"review_count": 23,
"packages": [
{ "name": "Half day", "price_pesewas": 150000, "currency": "GHS", "description": "...", "delivery_days": 7 }
],
"portfolio_urls": ["https://.../1.jpg"],
"languages": ["English"]
}/public/v1/marketplace/categories
The active marketplace category taxonomy.
{
"data": [
{ "id": "...", "slug": "photography", "name": "Photography", "kind": "service" }
]
}Events
GET/public/v1/events
Lists published events. Extra params: q (full-text search), event_type, category,
location, is_paid (true/false), is_online (true/false), date_from,
date_to.
curl "https://api.ardent.africa/public/v1/events?limit=2&is_paid=true"{
"data": [
{
"id": "4f2c...",
"slug": "accra-tech-summit-2026",
"title": "Accra Tech Summit 2026",
"description": "...",
"event_type": "conference",
"category": "Technology",
"start_date": "2026-09-12T09:00:00Z",
"end_date": "2026-09-12T17:00:00Z",
"timezone": "Africa/Accra",
"location": "Accra International Conference Centre",
"is_online": false,
"image_url": "https://.../event.jpg",
"is_paid": true,
"currency": "GHS",
"tags": ["tech", "startups"],
"is_featured": true,
"created_at": "2026-06-14T..."
}
],
"page": 1,
"limit": 2,
"total": 7
}/public/v1/events/:slug
A single published event, plus public content blocks (agenda, speakers, sponsors,
gallery). For paid events it also returns ticket_tiers, each with name, description,
price_pesewas, price_minor, currency, and an is_available flag. Raw inventory counts, promo
codes, organizer details, and online joining links are never returned.
price_pesewas and price_minor
Both fields carry the same number, the price in the minor unit of currency.
price_pesewas is the original field name and is kept for compatibility: it is what
existing integrations read, and it is not going away. It is a misnomer once an event is
priced in something other than cedis, which is why price_minor was added beside it.
Read currency alongside either one. A price of 20000 is GHS 200.00 or NGN 200.00
depending on it, and neither field tells you which on its own. New integrations should use
price_minor.
{
"slug": "accra-tech-summit-2026",
"title": "Accra Tech Summit 2026",
"is_paid": true,
"currency": "GHS",
"agenda": [],
"speakers": [],
"sponsors": [],
"gallery": [],
"ticket_tiers": [
{ "id": "...", "name": "Early bird", "description": "...", "price_pesewas": 20000, "currency": "GHS", "is_available": true }
]
}An unpublished or unknown slug gives 404.
Jobs
GET/public/v1/jobs
Lists open job postings. Extra params: q (full-text search), employment_type,
workplace_type (onsite / hybrid / remote), seniority, location, category.
Featured postings come first, then the newest.
curl "https://api.ardent.africa/public/v1/jobs?limit=2&workplace_type=remote"{
"data": [
{
"id": "...",
"slug": "senior-nurse-accra",
"title": "Senior Nurse",
"description": "...",
"employment_type": "full_time",
"workplace_type": "onsite",
"seniority": "senior",
"location": "Accra",
"salary_min_pesewas": 500000,
"salary_max_pesewas": 800000,
"salary_currency": "GHS",
"salary_period": "month",
"salary_hidden": false,
"skills": ["nursing"],
"application_deadline": null,
"is_featured": true,
"published_at": "2026-07-01T00:00:00Z",
"created_at": "2026-07-01T00:00:00Z"
}
],
"page": 1,
"limit": 2,
"total": 37
}When an employer chooses to hide pay, salary_hidden is true and both salary bounds come
back null. The API respects that choice, not just the website.
/public/v1/jobs/{slug}
Adds responsibilities, requirements, benefits, and the hiring employer
(id, slug, name, logo_url, industry, location, is_verified). A closed or
unknown slug gives 404.
There is no candidate or application endpoint, and there never will be. A job posting is public; the people applying to it are not. Nothing here exposes applicants, applications, screening answers, messages, interviews, offers, or fees. Employers get applicant detail through their authenticated dashboard, not through this API.
Polls and surveys
GET/public/v1/surveys
Lists public, standalone polls and surveys. Extra params: category (a category slug),
status (open or closed).
Two kinds of survey are deliberately absent. An unlisted survey is exactly as private as its link is secret, and this API is the opposite of a secret link. A survey attached to an event, a job posting or a community belongs to that flow and is reachable through it; publishing it here would hand out a registration form as though it were an opinion poll.
curl "https://api.ardent.africa/public/v1/surveys?limit=2&status=open"{
"data": [
{
"id": "...",
"slug": "what-should-accra-fix-first",
"title": "What should Accra fix first?",
"description": "One question, one answer per person.",
"purpose": "poll",
"is_anonymous": true,
"guests_allowed": true,
"result_visibility": "public_live",
"status": "open",
"opens_at": null,
"closes_at": "2026-09-01T00:00:00Z",
"category_slug": "governance",
"published_at": "2026-08-01T00:00:00Z",
"created_at": "2026-08-01T00:00:00Z",
"results_revealed": true,
"response_count": 412,
"url": "/polls/what-should-accra-fix-first"
}
],
"page": 1,
"limit": 2,
"total": 9
}Counts are null when withheld, never 0. Every survey chooses a result_visibility, and
that choice is a promise made to respondents before they answered, so this API keeps it. Only
public_live and a closed after_close survey reveal here; after_respond never can, because
revealing it needs the reader's own de-duplication hash and an API key does not have one.
Check results_revealed before you render a number. A 0 you substituted for null reads to
your users as "nobody answered", which is a different and false statement.
/public/v1/surveys/{slug}
Adds questions[], which is what an embeddable card needs:
{
"questions": [
{
"question_key": "priority",
"input_kind": "single_choice",
"label": "What should the city fix first?",
"help_text": null,
"required": true,
"options": [
{ "option_key": "drainage", "label": "Drainage", "response_count": 220 },
{ "option_key": "roads", "label": "Roads", "response_count": 192 }
]
}
]
}Per-option response_count is withheld with the headline, by the same rule. Matrix rows and
columns are layout rather than choices and are excluded from options.
There is no endpoint for a response, and no submit route. Answering needs the privacy notice,
the consent state and the question set in front of the person before they commit, plus a start
ticket the platform issues. An API key is not a respondent, so this surface shows the question and
sends people to ardent.africa to answer.
The absence goes deeper than a missing route. On an anonymous survey, nothing stored anywhere links a response to a person: the de-duplication key lives in a separate table with no foreign key to the response, and there is no branch in the platform's own permission functions that lets an administrator read one either. So there is no version of this API, however authenticated, that could return an individual answer. The service that backs these two routes never reads the response tables at all: every total you see is a counter column the database maintains.
Durbars
A durbar is a live audio room: one host opens it, a few people speak, many listen, and a recorded room leaves a replay and a transcript behind.
GET/public/v1/durbars
Lists public durbars. Extra params: q (searches titles), status (scheduled, live or
ended), language, is_recorded.
Live rooms come first, then the soonest scheduled, then the most recent. There is no visibility
parameter and there will not be one: an unlisted durbar is exactly as private as its link is
secret, and an index any key can page through would publish every one of them at once. Unlisted
rooms stay reachable by slug on the detail route below, because a slug is a link. A cancelled
room is never published anywhere: it should not sit in an index advertising a thing that never
happened.
curl "https://api.ardent.africa/public/v1/durbars?status=live&limit=2"{
"data": [
{
"id": "...",
"slug": "harmattan-town-hall",
"title": "Harmattan town hall",
"description": "Two hours on the season and the water table.",
"status": "ended",
"scheduled_for": "2026-08-01T18:00:00Z",
"started_at": "2026-08-01T18:04:00Z",
"ended_at": "2026-08-01T19:41:00Z",
"is_recorded": true,
"language": "en",
"peak_listener_count": 412,
"total_listeners": 903,
"host": {
"handle": "ardentafrica",
"name": "Ardent Africa",
"avatar_url": null,
"kind": "organization",
"verified": true
},
"url": "/durbar/harmattan-town-hall",
"created_at": "2026-07-28T09:00:00Z"
}
],
"page": 1,
"limit": 2,
"total": 14
}Listener counts are null until a room has ended, and null is not 0. A live room's peak
moves, so a figure published mid-room goes down when somebody refreshes, which reads to your users
as a room emptying out rather than as the honest answer to a question nobody asked. Substituting
0 for null says "nobody came", which is a different and false statement.
/public/v1/durbars/{slug}
Adds speakers[], replay and has_transcript:
{
"speakers": [
{
"handle": "ama",
"name": "Ama",
"avatar_url": null,
"kind": "person",
"verified": false,
"role": "host",
"speaking_minutes": 51
}
],
"replay": {
"url": "https://.../durbar-recordings/<id>/<uuid>.m4a",
"duration_seconds": 5820,
"mime_type": "audio/mp4"
},
"has_transcript": true
}replay is null until an ended, recorded room's archive has finished uploading. The live HLS
playlist is never published here: it is a moving target with a short life, and a cached embed
pointing at it would break within the hour.
Speakers are named. Listeners are only ever a number, on every surface we have. A speaker talked in public, in a public room, into a recording that is itself public and indexed, so naming them here says nothing the replay page does not already say out loud. A listener did the opposite of all of that. There is no participant list on this API, for anybody, at any tier.
Raised hands are absent, and not merely unpublished. Knowing that somebody asked to speak in a known room identifies them, and a request a host declined is a private fact about a person that they did not publish and cannot take back. The table that holds them is not readable by this surface at the database level, so there is no version of this endpoint that could return one.
GET/public/v1/durbars/{slug}/transcript
The machine transcript of a recorded durbar, as timestamped segments. This is the point of recording: a durbar becomes searchable, quotable, translatable text.
{
"durbar": { "slug": "harmattan-town-hall", "title": "Harmattan town hall", "url": "/durbar/harmattan-town-hall" },
"language": "en",
"duration_seconds": 5820,
"segment_count": 1180,
"mean_confidence": 0.82,
"completed_at": "2026-08-01T20:10:00Z",
"disclaimer": "Machine transcription. Names, places and code-switched speech are frequently wrong. The recording is the record; this text is an index into it.",
"segments": [
{ "start_ms": 0, "end_ms": 4200, "text": "Good evening everyone.", "lang": "en", "confidence": 0.94 }
]
}Returns 404 when a room was not recorded, or has not been transcribed yet.
Read the disclaimer and mean it. This is automatic speech recognition of multi-speaker
Ghanaian English that code-switches into Twi, Ga, Ewe and Pidgin, which is a much harder problem
than the read speech these engines are scored on. Names fail worst of all, and the names in a
durbar are the host, the speakers and whoever they are talking about. Every segment carries its
own confidence so you can render the weak ones differently or not at all.
Segments carry no speaker, permanently. Attribution would need either diarisation, which is a
guess dressed as a fact, or per-track egress, which doubles what a room costs to run. The field is
absent rather than always-null so nobody builds against something that will never arrive. Do not
present a line from this text as a quotation attributed to a named person.
Segments the platform's moderation pass flagged are withheld from this surface entirely. A transcript is generated text about real people, and this API is where it would be indexed, cached and mirrored beyond our reach.
There is no join endpoint, and there will not be one. Joining a room mints a short-lived token
scoped to that room, an identity and a permission set. An API key has no actor, no trust level, no
block relationships and nobody to show a recording consent notice to, and every one of those is a
precondition of being in a room rather than a formality. Reading a durbar is a public act;
entering one is not. Send people to ardent.africa to listen.
Petitions
GET/public/v1/petitions
Lists active petitions. Extra params: category, country.
{
"data": [
{
"id": "6844cb10-...",
"slug": "pass-the-affirmative-action-bill",
"title": "Pass the Affirmative Action Bill",
"description": "...",
"category": "Governance",
"status": "active",
"petition_target": 10000,
"signatures_count": 3421,
"target_country": "GH",
"target_country_name": "Ghana",
"country": "Ghana",
"image_url": null,
"progress": { "signatures_count": 3421, "petition_target": 10000, "percent": 34 },
"created_at": "2026-06-10T..."
}
],
"page": 1,
"limit": 20,
"total": 4
}/public/v1/petitions/:slug
A single petition, plus aggregate momentum. Adds momentum.signatures_last_7_days.
Signer names, emails, and comments are never returned.
{
"slug": "pass-the-affirmative-action-bill",
"signatures_count": 3421,
"progress": { "signatures_count": 3421, "petition_target": 10000, "percent": 34 },
"momentum": { "signatures_last_7_days": 188 }
}Blog
GET/public/v1/blog
Lists published posts. Extra param: category (category id).
{
"data": [
{
"title": "How tipping works",
"slug": "how-tipping-works",
"excerpt": "A short explainer",
"published_at": "2026-06-13T23:54:21.892Z",
"featured_image_url": null,
"blog_categories": { "name": "Product", "slug": "product" }
}
],
"page": 1,
"limit": 20,
"total": 1
}/public/v1/blog/:slug
A single published post. content is sanitized HTML.
{
"title": "How tipping works",
"slug": "how-tipping-works",
"excerpt": "A short explainer",
"content": "<p>Sanitized HTML body.</p>",
"published_at": "2026-06-13T23:54:21.892Z",
"featured_image_url": null,
"category": { "name": "Product", "slug": "product" }
}Profiles
GET/public/v1/profiles/:username
A public supporter profile (only where the user has enabled a public profile). Returns aggregate signature count. Never returns email, phone, or internal ids.
{
"username": "ama",
"full_name": "Ama O.",
"bio": "Supporter of education causes",
"avatar_url": "https://.../avatar.jpg",
"country": "Ghana",
"account_type": "personal",
"signatures_count": 12
}A username with no public profile gives 404.
Stats
GET/public/v1/stats
Platform-level aggregates. No per-user or donor data.
{
"total_active_campaigns": 10,
"total_campaigns": 11,
"countries_reached": 1,
"total_funds_raised": 288460,
"total_signatures": 51
}Last updated: 24 August 2026
