Fanpodium API v1
Partners keep their existing systems. The API lets a club CMS, label feed, studio pipeline, ticketing provider or CRM push content and fixtures into a Fanpodium world and read aggregate reporting back. Keys are issued per organisation from the Partner Console, and every call is scoped to that organisation only.
This is a preview environment: no payments, ticketing or third-party providers are connected, and all data returned is labelled demo data.
Authentication
Keys look like fpk_xxxxxxxx_…. The full value is shown once at creation and stored only as a hash — rotate rather than recover. Send it as a bearer token.
curl -H "Authorization: Bearer fpk_1a2b3c4d_…" \
https://fanpodium.com/api/public/v1/pingScopes
content:readRead published posts for the organisation the key belongs to.
content:writeCreate or update posts, keyed by source + external_id (idempotent upserts).
events:writeCreate or update fixtures, shows and other calendar entries.
analytics:readRead aggregate, consent-aware analytics segments. No personal data.
Endpoints
All endpoints return JSON. Errors use standard status codes with a message body: 401 for an invalid or revoked key, 403 for a missing scope or unbound key, 422 for validation problems, and 429 when a rate limit or quota is reached.
Each key has an hourly rate limit and a monthly request quota (1,000/hour and 100,000/month by default, adjustable per partner). Usage is counted per key in fixed hourly windows; retry after the next window or ask Fanpodium to raise the ceiling.
/api/public/v1/pingscope: anyCredential introspection: confirms the key works and returns its organisation and scopes.
/api/public/v1/contentscope: content:readLatest published posts for the key's organisation. Supports ?limit= up to 100.
/api/public/v1/contentscope: content:writePush up to 50 items per call. Each item carries external_id, kind, title, body, media/video URL and status.
/api/public/v1/eventsscope: content:read / events:writeRead or sync fixtures, shows, screenings and community moments.
/api/public/v1/analyticsscope: analytics:readAggregate segments by dimension and period — buckets, people, conversions, indicative value.
Example: idempotent content sync
POST /api/public/v1/content
Authorization: Bearer fpk_1a2b3c4d_…
Content-Type: application/json
{
"source": "club-cms",
"items": [
{
"external_id": "news-4821",
"kind": "update",
"title": "Matchday build-up: round five",
"body": "Team news and the supporter travel plan.",
"status": "published"
}
]
}Repeating the same source and external_id updates the existing post instead of creating a duplicate, so a CMS can safely re-send its full feed.
Successful response
200 OK
{
"ok": true,
"org": { "id": "8f1c…", "slug": "bangalore-fc" },
"result": { "created": 0, "updated": 1, "skipped": 0 },
"items": [
{ "external_id": "news-4821", "id": "b27d…", "action": "updated" }
]
}Error response
403 Forbidden
{
"ok": false,
"error": "missing_scope",
"message": "This key does not have the content:write scope",
"required_scope": "content:write"
}Example: reading audience aggregates
Analytics responses are consent-gated aggregates with small-group suppression, so no row ever identifies an individual fan.
GET /api/public/v1/analytics/segments?dimension=country
Authorization: Bearer fpk_1a2b3c4d_…
200 OK
{
"ok": true,
"dimension": "country",
"suppression_threshold": 25,
"segments": [
{ "bucket": "India", "fans": 1840, "share": 0.62 },
{ "bucket": "Sweden", "fans": 412, "share": 0.14 },
{ "bucket": "Other", "fans": 702, "share": 0.24 }
]
}Scheduled platform jobs
Auctions close automatically on the server, not in the browser: a scheduled job runs every minute, settles any auction past its end time, records the winning bid and emits the auction.closed webhook. Partners never need to trigger this, and a closed auction can no longer accept bids even if a stale tab tries.
Outbound webhooks
Register endpoints in the Partner Console. Deliveries carry the event name in x-fanpodium-event and an HMAC-SHA256 signature in x-fanpodium-signature using the format t=<timestamp>,v1=<signature>. Verify the signature before acting on a payload, and treat delivery as at-least-once.
- content.published — a post goes live in the partner world
- event.created — a fixture or show is added or updated
- auction.closed — a lot settles and the revenue split is recorded
Every delivery attempt, status code and error is logged per organisation so a partner can audit exactly what left the platform.
Verifying a signature
const [tPart, vPart] = header.split(",");
const timestamp = tPart.split("=")[1];
const signature = vPart.split("=")[1];
const expected = crypto
.createHmac("sha256", process.env.FANPODIUM_WEBHOOK_SECRET)
.update(timestamp + "." + rawBody)
.digest("hex");
// Reject if it does not match, or if the timestamp is older than 5 minutes.
const ok = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);