Ardent AfricaDocs
Developers

Webhooks

Get Ardent events pushed to your server, signed and verifiable.

Instead of polling, subscribe to events and Ardent will POST them to your URL as they happen. Create and manage webhooks in your developer dashboard.

Events

EventWhenPayload
petition.signeda petition gets a new signaturepetition_slug, signatures_count
campaign.publisheda campaign goes liveslug, title, category, goal_amount, target_country
campaign.updateda live campaign's details changeslug, title
campaign.goal_reacheda campaign hits its goalslug, title, goal_amount, raised_amount
event.publishedan event is publishedslug, title, start_date, is_online, is_paid
event.ticket_solda paid ticket is confirmedevent_slug, quantity
event.cancelledan event is cancelled or moved onlineslug, title, status
event.updateda published event is rescheduledslug, title, start_date, previous_start_date
event.refundeda ticket refund completesevent_slug, amount_pesewas, amount_minor, currency
job.posteda job posting goes liveslug, title, employment_type, workplace_type, location, is_featured
job.closeda posting stops accepting applicationsslug, title, status
application.receivedsomeone applies to a postingposting_slug, posting_title, application_count
application.status_changedan application moves in the pipelineposting_slug, from_status, to_status
review.receiveda new review is publishedentity_slug, entity_type, overall_rating, verification_status
review.flaggeda review enters moderationentity_slug, entity_type
review.respondedan entity replies to a reviewentity_slug, entity_type
entity.claim.submitteda listing claim is filedentity_slug, entity_type
entity.claim.approveda listing claim is approvedentity_slug, entity_type
entity.question.askeda public question is posted on a listingentity_slug, entity_type
entity.suggestion.submittedan edit to a listing is suggestedentity_slug, entity_type
entity.update.publishedan entity posts an updateentity_slug, entity_type, kind
entity.verification.approvedan entity verification is approvedentity_slug, entity_type, verification_type
social.post.publisheda PUBLIC post is publishedid, kind, activity_type, author_handle, author_display_name, author_kind, published_at
social.follower.gainedsomebody follows youfollowee_handle, follower_handle, follower_display_name, followed_at
social.mention.createdyou are mentioned in a public postpost_id, mentioned_handle, author_handle, author_display_name

amount_pesewas and amount_minor on event.refunded

Both fields carry the same number, the refunded amount in the minor unit of currency.

amount_pesewas is the original field name and is kept for compatibility: your endpoint is already reading it and it is not going away. It is a misnomer once an event is priced in something other than cedis, which is why amount_minor was added beside it.

Read currency alongside either one. An amount 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 amount_minor. The same pair appears as price_pesewas and price_minor on the public events endpoint, for the same reason.

Some events are yours alone

Most topics above are broadcast: the content is already public, so every subscription listening for the topic receives every event.

social.follower.gained and social.mention.created are scoped. They are delivered only to subscriptions owned by the account the event is about. You will never receive a follower event for somebody else's profile.

That distinction is not cosmetic. Both handles in a follower event belong to public profiles, but the edge between them is not public, and an endpoint collecting broadcast edges would reconstruct the whole social graph. The same reasoning is why the read API has no endpoint for follows, connections or group members at all.

social.post.published is broadcast, because it fires only for a post whose author chose a public audience, which anybody can already read at its permalink. A post limited to followers, to connections, or to a group never produces a webhook, and a mention inside such a post does not either.

Payloads contain public fields only: never donor, signer, organizer, attendee, candidate, reviewer, or claimant personal data.

This is deliberate for the job events. application.received tells you a posting got an application and how many it now has; it does not tell you who applied. application.status_changed gives you the coarse pipeline status only. No name, email, phone, CV, cover letter, screening answer, or candidate id ever crosses a webhook boundary, because a job seeker's identity leaking to a third-party endpoint could cost them their current job. Use the employer dashboard or the authenticated API for applicant detail.

Delivery

Each delivery is a POST with a JSON body and these headers:

Content-Type: application/json
X-Ardent-Event: petition.signed
X-Ardent-Signature: t=1718900000,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

Respond with any 2xx to acknowledge. Non-2xx (or a timeout) is retried with exponential backoff for up to 8 attempts, after which the delivery is dead-lettered. You can replay it from the dashboard. You can also send a test event to any endpoint.

Verifying the signature

v1 is the hex HMAC-SHA256 of <t>.<raw-request-body>, keyed by your subscription's signing secret (shown once when you create or rotate the webhook). Always verify before trusting a payload, and reject timestamps outside a few minutes to prevent replays.

// Node - use the RAW request body, not a re-serialized object.
import crypto from 'node:crypto'

function verify(secret, signatureHeader, rawBody) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('=')))
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
  const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
  return ok && fresh
}
<?php
function ardent_verify(string $secret, string $header, string $rawBody): bool {
  parse_str(str_replace(',', '&', $header), $p);
  $expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
  return hash_equals($expected, $p['v1'] ?? '') && abs(time() - (int) $p['t']) < 300;
}

Security notes

  • Only HTTPS endpoints are accepted, and Ardent will not deliver to private, loopback, or link-local addresses.
  • Treat the signing secret like a password. If it leaks, rotate it from the dashboard.
  • Delivery is at-least-once. Handle the occasional duplicate idempotently.

Last updated: 24 August 2026

On this page