Ardent AfricaDocs
Developers

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:

ParamTypeDefaultNotes
pageinteger11-based page number
limitinteger20max 50

All endpoints (generated)

Generated from the live /public/v1 API contract. Response shapes and examples are documented per resource in the sections below.

MethodPathHandler
GET/public/v1/durbarsList durbars
GET/public/v1/durbars/:slugGet durbar
GET/public/v1/durbars/:slug/transcriptGet transcript
GET/public/v1/eventsList events
GET/public/v1/events/:slugGet event
GET/public/v1/jobsList jobs
GET/public/v1/jobs/:slugGet job
GET/public/v1/marketplace/servicesList services
GET/public/v1/marketplace/services/:idGet service
GET/public/v1/marketplace/categoriesList categories
GET/public/v1/openapi.jsonGet open api
GET/public/v1/reviews/entitiesList entities
GET/public/v1/reviews/entities/:slugGet entity
GET/public/v1/safety/hazardsList hazards
GET/public/v1/safety/hazards/:caseNumberGet hazard
GET/public/v1/actorsList actors
GET/public/v1/actors/:handleGet actor
GET/public/v1/postsList posts
GET/public/v1/posts/:idGet post
GET/public/v1/groupsList groups
GET/public/v1/groups/:slugGet group
GET/public/v1/surveysList surveys
GET/public/v1/surveys/:slugGet survey
GET/public/v1/campaignsList campaigns
GET/public/v1/campaigns/:slugGet campaign
GET/public/v1/petitionsList petitions
GET/public/v1/petitions/:slugGet petition
GET/public/v1/blogList blog
GET/public/v1/blog/:slugGet blog post
GET/public/v1/profiles/:usernameGet profile
GET/public/v1/statsGet 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
}
GET/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
}
GET/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"]
}
GET/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
}
GET/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.

GET/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.

GET/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.

GET/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
}
GET/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
}
GET/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

On this page