Skip to main content
Integration reference

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/ping

Scopes

  • content:read

    Read published posts for the organisation the key belongs to.

  • content:write

    Create or update posts, keyed by source + external_id (idempotent upserts).

  • events:write

    Create or update fixtures, shows and other calendar entries.

  • analytics:read

    Read 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.

    GET/api/public/v1/pingscope: any

    Credential introspection: confirms the key works and returns its organisation and scopes.

    GET/api/public/v1/contentscope: content:read

    Latest published posts for the key's organisation. Supports ?limit= up to 100.

    POST/api/public/v1/contentscope: content:write

    Push up to 50 items per call. Each item carries external_id, kind, title, body, media/video URL and status.

    GET / POST/api/public/v1/eventsscope: content:read / events:write

    Read or sync fixtures, shows, screenings and community moments.

    GET/api/public/v1/analyticsscope: analytics:read

    Aggregate 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),
);