Club Integration API
Build your club's own fan app on the Game Set Engage platform. Your brand and your build — our campaign engine, points ledger, venue network and fan accounts underneath. Your users live in a single-club universe: they belong to your club from the moment they register, and they only ever see your campaigns, your venue offers and their own data.
The Game Set Engage API is a closed platform: it serves our own apps and approved club integrations. Your club's API key is your app's identity — there is no anonymous or general-purpose access.
1. Get access
- Register your club — create your club account and complete onboarding.
- Be on the National or Global plan — API access is included in National and Global. Lower tiers can upgrade at any time; your fans, points and history carry over.
- Generate your keys — in your club dashboard open Club Management → API Access and press Generate API keys.
You get two keys:
| Key | Looks like | Where it lives | What it does |
|---|---|---|---|
| Publishable key | gse_pk_… |
Inside your app | Identifies your club and scopes every request to it. Not a secret. |
| Secret key | gse_sk_… |
Your servers only | Reserved for server-to-server calls. Shown once at generation — store it in a secret manager, never in the app. |
You can rotate the secret at any time (the publishable key survives), rotate the publishable key (coordinate with an app release — it breaks shipped builds), or revoke access entirely.
2. The basics
- Production:
https://api.gamesetengage.com/api/v1 - Staging:
https://dev.gamesetengage.com/api/v1— same keys, test here first. - JSON in, JSON out. Non-GET requests need
Content-Type: application/json. - Send your publishable key on every request:
X-Club-Key: gse_pk_XXXXXXXXXXXXXXXXXXXXXXXXXXXX
An invalid or revoked key fails loudly with 401 INVALID_CLUB_KEY — it never
silently falls back. If your plan drops below National, requests return
403 CLUB_PLAN_REQUIRED until you upgrade again.
Every response uses one envelope. Quote meta.request_id when you contact
support.
{ "success": true, "message": "…", "data": { }, "meta": { "timestamp": "…", "request_id": "…", "version": "v1" } }
{ "success": false, "error": { "code": "FORBIDDEN", "message": "…" }, "meta": { } }
3. Register and sign in your users
Accounts created through your app are automatically subscribed to your club — no club pickers, no discovery screens. Under the hood they are platform accounts, so verified email, password reset, account deletion and fraud protection all come for free.
Create a fan (requires explicit terms acceptance):
curl -X POST https://api.gamesetengage.com/api/v1/auth/register \
-H "Content-Type: application/json" \
-H "X-Club-Key: gse_pk_XXXX" \
-d '{
"user": {
"email": "[email protected]",
"password": "aStrongPassword!",
"first_name": "Alex",
"last_name": "Carter"
},
"terms_accepted": true
}'
The fan receives a verification email; confirm with
POST /auth/verify-email ({ "token": "<6-digit code>" }) — the response
already includes the JWT tokens.
Sign in:
curl -X POST https://api.gamesetengage.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "X-Club-Key: gse_pk_XXXX" \
-d '{ "user": { "email": "[email protected]", "password": "aStrongPassword!" } }'
{
"success": true,
"message": "Login successful",
"data": {
"user": {
"unique_id": "usr_9f2ac1…",
"email": "[email protected]",
"first_name": "Alex",
"role": "fan",
"email_verified": true,
"engagement_points": 127,
"points_by_club": [
{ "club_unique_id": "club_hollowmere", "club_name": "Hollowmere Town FC", "points": 127 }
],
"subscribed_club": { "unique_id": "club_hollowmere", "name": "Hollowmere Town FC" }
},
"tokens": {
"access_token": "eyJhbGciOiJIUzI1NiJ9…",
"refresh_token": "9f8c4e2b…",
"expires_in": 900
}
}
}
Access tokens live 15 minutes — refresh with POST /auth/refresh. Send
Authorization: Bearer <access_token> plus X-Club-Key on every call from
here on.
Two login cases to handle in your UI:
- A fan who already has a Game Set Engage account is subscribed to your club automatically on first sign-in through your app.
- If that account already follows the platform maximum of 3 clubs, login
returns
403with a clear message — show it as-is.
After every successful login, register the device's push token
(POST /devices) so notifications follow the signed-in account.
4. List your campaigns
curl https://api.gamesetengage.com/api/v1/campaigns \
-H "X-Club-Key: gse_pk_XXXX" \
-H "Authorization: Bearer <access_token>"
Returns your club's active campaigns, paginated. Fetch one with
GET /campaigns/:unique_id — the detail includes everything your UI needs:
{
"success": true,
"data": {
"unique_id": "cmp_derby_checkin",
"name": "Derby Day Check-in",
"campaign_type": "event_check_in",
"engagement_points": 50,
"supportive_engagement_points": 10,
"venue_engagement_points": 30,
"start_date": "2026-09-12T16:00:00Z",
"end_date": "2026-09-12T23:00:00Z",
"can_participate": true,
"user_participated": false,
"my_club_points": 127,
"deal_codes_remaining": null,
"prediction_locked": false,
"checkin_location": { "latitude": 51.5549, "longitude": -0.1084, "radius_km": 0.5 },
"my_participation": null
}
}
Fields worth wiring up: can_participate (drive your CTA), my_club_points
(the fan's balance with your club — needed for auctions and point-spend
campaigns), deal_codes_remaining (stock indicator for special_deal_code),
my_participation (result + code after the fan has taken part).
5. Participation cookbook — every campaign type
All eight types hit the same endpoint —
POST /campaigns/:unique_id/participate — only the payload differs. Points
rules, per-fan limits, quotas and time windows are enforced server-side and
transactionally: a failed participation never burns points or codes.
Two things are true for every type:
- Who is participating comes from the
Authorizationheader, never the payload. An "empty"{}payload is only empty of campaign data — the JWT identifies the fan on every call. locationis optional everywhere. Types that don't require it (basic,deal_code,special_deal_code,survey,prediction,auction) still accept"location": { "latitude", "longitude", "accuracy" }— when you send it, it is stored on the participation record and enriches the club's engagement analytics. Send it whenever the fan has granted location permission.
event_check_in — GPS check-in
location is required. Points are tiered by where the fan is (values are set
per campaign by you, not auto-multiplied): full points inside the venue
radius, reduced "watching from home" points outside it (if you enabled them),
and partner-venue points through the venue QR flow (§6).
POST /campaigns/cmp_derby_checkin/participate
{ "location": { "latitude": 51.5549, "longitude": -0.1084, "accuracy": 8 } }
{
"success": true,
"data": {
"participation": { "points_earned": 50, "total_points": 50 },
"checkin": { "tier": "main", "at_main_location": true, "points": 50, "warning": null }
}
}
A home check-in returns tier: "home", lower points, and a warning string
to surface. If home points are disabled, an outside check-in is rejected
(422 You must be near the event location to check in).
basic — one-tap participation
Empty payload; awards the campaign's points.
POST /campaigns/cmp_season_kickoff/participate
{}
…or, if the fan has granted location permission, send it along (optional — stored on the participation record, enriches your analytics):
POST /campaigns/cmp_season_kickoff/participate
{ "location": { "latitude": 51.5549, "longitude": -0.1084, "accuracy": 8 } }
{ "success": true, "data": { "participation": { "points_earned": 2, "total_points": 2 } } }
deal_code — shared discount code
Empty payload (optional location accepted). Every fan receives the same
code; show it prominently.
POST /campaigns/cmp_friday_pint/participate
{}
{
"success": true,
"data": {
"participation": { "points_earned": 1, "total_points": 1 },
"deal_code": "HOLLOWMERE-FRIDAY-20OFF"
}
}
special_deal_code — unique code from a limited pool
Empty payload (optional location accepted). Each fan draws a different
code; when the pool runs out the call returns 422 All deal codes have been claimed. Use
deal_codes_remaining from campaign detail as a stock badge.
POST /campaigns/cmp_limited_jersey/participate
{}
{
"success": true,
"data": {
"participation": { "points_earned": 0, "total_points": 0 },
"deal_code": "JERSEY-7F3K9Q"
}
}
qr_based — scan at the venue
The fan scans your campaign QR; your app calls POST /campaigns/scan with the
QR payload to resolve the campaign, then participates with location (these
campaigns are usually pinned to a geofence — a fan outside the radius gets
422 You must be at the campaign location to participate).
POST /campaigns/cmp_east_stand_qr/participate
{ "location": { "latitude": 51.5549, "longitude": -0.1084, "accuracy": 9 } }
{ "success": true, "data": { "participation": { "points_earned": 3, "total_points": 3 } } }
survey — questions, optional quiz bonus
survey_responses is required (optional location accepted alongside),
keyed by the question index as a string.
Questions you marked with a correct answer pay a bonus per correct reply; pure
opinion surveys just pay the base points.
POST /campaigns/cmp_matchday_quiz/participate
{ "survey_responses": { "0": "Hollowmere", "1": "Reyes" } }
{
"success": true,
"message": "Survey complete! 2 correct, 10 bonus points earned.",
"data": {
"participation": { "points_earned": 5, "quiz_bonus_points": 10, "quiz_correct_count": 2, "total_points": 15 }
}
}
prediction — points only for being right
Same payload shape as survey (optional location accepted alongside),
different economics: taking part earns nothing. If the outcome is already known the bonus (or a miss) is returned
immediately; otherwise the result is pending and points arrive when your
club resolves the outcome. Always render from the result object — never
frame a pending or wrong pick as a win.
POST /campaigns/cmp_final_score/participate
{ "survey_responses": { "0": "2-1" } }
{
"success": true,
"data": {
"participation": { "points_earned": 0, "total_points": 0 },
"result": {
"outcome": "pending",
"resolved": false,
"points_awarded": 0,
"title": "Prediction locked in",
"text": "We'll add your points once the result is confirmed."
}
}
}
After resolution, GET /campaigns/:unique_id shows the outcome under
my_participation, and prediction_locked: true closes new entries.
auction — bid engagement points
bid_amount is required (optional location accepted alongside) and is paid
in club engagement points, never money. A bid must clear the current bid plus the step, and fit the fan's
balance (my_club_points). Outbid fans can always re-bid; the winner pays at
close via automatic settlement.
POST /campaigns/cmp_signed_shirt/participate
{ "bid_amount": 75 }
{ "success": true, "data": { "participation": { "points_earned": 0, "total_points": 0 } } }
Rejections are explicit:
422 Bid must be at least 80 · 422 Insufficient points. You have 60 points for this club.
Poll GET /campaigns/:unique_id/auction for the live state (highest bid,
your position, time left) or subscribe to the WebSocket for realtime updates.
6. Venue network
Your partner venues come with the platform — pubs, bars and restaurants where your fans check in, earn points and claim your venue offers. Every venue flow follows the same mechanic: your app shows a QR, venue staff scan and confirm it in person, your app polls until the decision lands. QRs expire after 10 minutes.
Find venues
GET /fan/nearby_venues?latitude=51.55&longitude=-0.10&radius_km=5 — partner
venues around the fan:
{
"success": true,
"data": {
"venues": [
{
"unique_id": "ven_redlion",
"name": "The Red Lion",
"category": "Pub",
"address": "12 High St",
"city": "London",
"rating": 4.6,
"distance_km": 0.42,
"operating_status": "Open",
"coordinates": { "latitude": 51.5521, "longitude": -0.1044 }
}
],
"search_params": { "latitude": 51.55, "longitude": -0.1, "radius_km": 5.0, "total_found": 1 }
}
}
For a check-in campaign, GET /campaigns/:unique_id/venues lists its
affiliated venues with each venue's side deal (e.g. "Buy 1 beer, 2nd 50%
off") and checked_in_today so your UI can mark venues as done.
Venue check-in (QR confirmed by staff)
POST /campaigns/cmp_derby_checkin/venue_checkins
{ "venue_id": "ven_redlion" }
{
"success": true,
"message": "Show this QR to the venue to confirm",
"data": {
"unique_id": "vchk_8f2a…",
"status": "pending",
"qr_payload": "VCHK:Xb7…",
"venue_points": 30,
"side_deal": "Buy 1 beer, 2nd 50% off",
"expires_at": "2026-09-12T21:10:00Z"
}
}
Render qr_payload as a QR code, then poll GET /venue_checkins/:unique_id
until status is approved (points + side deal to show staff) or rejected
/ expired. Re-calling the create endpoint while a QR is still live resumes
the same QR (and re-notifies the venue) instead of duplicating it. Venue
check-in points are once-per-day; on later same-day check-ins the fan still
gets the side deal, just no extra points.
Venue offers (standalone promos)
GET /venue_offers?latitude=…&longitude=… — your club's active offers nearby,
each with discount, fan_points, runs_today and the venue block. Claiming
mirrors the check-in mechanic:
POST /venue_offers/vof_9ad21c/claim
{}
{
"success": true,
"message": "Show this QR to the venue to confirm",
"data": {
"unique_id": "vofc_8f2a…",
"status": "pending",
"qr_payload": "VOFR:Xb7…",
"discount": "50% off mains",
"fan_points": 25,
"expires_at": "2026-09-10T19:40:00Z"
}
}
Poll GET /venue_offer_claims/:unique_id until approved — the response then
carries points_awarded and the discount to show at the till. Claims are
once per offer per day; claiming outside the offer's weekdays returns
422 This offer isn't available today.
7. Profile & points
The profile object
GET /profile:
{
"success": true,
"data": {
"unique_id": "usr_9f2ac1…",
"email": "[email protected]",
"first_name": "Alex",
"last_name": "Carter",
"avatar_url": "https://…",
"engagement_points": 127,
"points_by_club": [
{ "club_unique_id": "club_hollowmere", "club_name": "Hollowmere Town FC", "points": 127 }
],
"subscribed_clubs": [
{ "unique_id": "club_hollowmere", "name": "Hollowmere Town FC", "subscribed_at": "2026-08-01T10:00:00Z" }
]
}
}
PUT /profile updates first_name / last_name. Avatar upload is the one
multipart endpoint: POST /profile/avatar with an avatar file field.
DELETE /profile (password-confirmed) anonymizes the account permanently —
wire it to your "delete account" screen; app-store rules require it.
Points & history
GET /profile/engagement_points — balance, last action and the fan's five
most recent participations. GET /profile/campaign_history — the full
paginated log; each row is self-contained:
{
"id": 4211,
"campaign": { "unique_id": "cmp_friday_pint", "name": "Friday Pint Deal", "campaign_type": "deal_code", "club_name": "Hollowmere Town FC" },
"points_earned": 1,
"bonus_points_earned": 0,
"total_points": 1,
"status": "success",
"participated_at": "2026-08-07T18:12:00Z",
"deal_code": "HOLLOWMERE-FRIDAY-20OFF",
"location": { "latitude": 51.5549, "longitude": -0.1084, "accuracy": 8.0 }
}
deal_code re-surfaces the fan's earned codes (shared or unique) so your
"my rewards" screen never loses them; location echoes what you sent at
participation (null if you didn't).
Notifications & devices
GET /profile/notification_preferences returns exactly four booleans —
all_campaigns, matchday_reminders, weekly_digest, partner_deals;
PUT accepts a partial object of the same keys (anything else is rejected).
Register the push token after every successful login:
POST /devices
{
"device_token": "a1b2c3…",
"platform": "ios",
"device_id": "3F2504E0-…",
"app_version": "1.0.0",
"apns_environment": "production"
}
Registration is idempotent and follows the signed-in account — on an account
switch the handset's pushes switch with it. Call
DELETE /devices/unregister on logout.
8. Errors your app should handle
Every error uses the same envelope; error.message is written to be shown to
the fan as-is, so most error UI is one generic sheet:
{
"success": false,
"error": { "code": "UNPROCESSABLE_ENTITY", "message": "You've already participated in this campaign" },
"meta": { "timestamp": "2026-09-12T18:00:00Z", "request_id": "7223a11b-…", "version": "v1" }
}
| Code | Status | What to do |
|---|---|---|
INVALID_CLUB_KEY |
401 | Your X-Club-Key is wrong or revoked. Config error — fail the build loudly, check the dashboard. |
CLUB_PLAN_REQUIRED |
403 | Plan dropped below National — API access paused until upgrade. |
AUTH_REQUIRED · TOKEN_EXPIRED · INVALID_TOKEN |
401 | Silently POST /auth/refresh; only on refresh failure send the fan to login. |
EMAIL_NOT_VERIFIED |
403 | Points-earning actions need a verified email — reopen your verification screen (POST /auth/resend-verification). |
ACCOUNT_SUSPENDED |
403 | Fraud-blocked account — show the message with your support link. |
FORBIDDEN (on login) |
403 | The 3-club cap — show the server's message as-is. |
NOT_FOUND |
404 | Doesn't exist — or belongs outside your club's universe. Treat both the same. |
UNPROCESSABLE_ENTITY |
422 | Business rule: already participated, pool empty, bid too low, off-schedule offer, outside the geofence… Show message. |
VALIDATION_ERROR |
422 | Invalid input — error.details is an array of field errors. |
BAD_REQUEST |
400 | Malformed request (missing param, wrong content type). |
Concrete 422 messages you will meet in the wild — all display-ready:
You've already participated in this campaign
All deal codes have been claimed
You must be near the event location to check in
You must be at the campaign location to participate
Bid must be at least 80
Insufficient points. You have 60 points for this club.
Survey responses are required
This offer isn't available today
You've already claimed this offer today
Rate limits: 100 requests/min per IP, 300 per token; login, register and
password-reset endpoints are throttled harder. A 429 comes from the edge
with a plain body (error, message, retry_after seconds) — back off and
retry after the given delay.
9. Launch checklist
Setup
- Generate keys in Dashboard → API Access; embed
gse_pk_…in the app, vaultgse_sk_…on your servers. - Send
X-Club-Keyon every request — including register and login. - Point your builds at staging (
dev.gamesetengage.com) until your flows are green; the same keys work on both.
Auth flows to test end-to-end
- Register → verify email → login → refresh loop (tokens live 15 minutes — refresh proactively, not on failure only).
- Login with an existing Game Set Engage account (auto-subscribe path) and
with an account at the 3-club cap (expect the
403, show it kindly). - Register the push token after every successful login; unregister on logout. Test an account switch on one device — pushes must follow.
Campaign flows to test per type you'll run
- One participation per type from §5, plus the repeat attempt (expect the
friendly
422), the empty deal-code pool, and — for geofenced types — a check-in from outside the radius. - Send optional
locationwherever the fan has granted permission — it costs nothing and feeds your analytics.
Venue flows (if you use the venue network)
- Open a venue check-in QR, let it expire (10 min), reopen; then a real
staff-confirmed approve and the poll loop to
approved.
Before the store release
- Wire
DELETE /profile(account deletion) into settings — app-store rules require it. - Make sure error sheets show
error.messageverbatim and quotemeta.request_idin your support links. - Switch base URL to production, do one full smoke pass, ship.
Questions, or a capability you're missing? Contact us — we answer fast.