game set engage
  • Solutions

    • Fan Engagement
    • Revenue Growth
    • Community Building
    • Data & Analytics

    By Fan Base

    • Local Up to 1K fans
    • City Up to 10K fans
    • National Up to 100K fans
    • Global Up to 1M fans

    Resources

    • Earnings Calculator New
    • Implementation Guide
    • Developers
    • Support Center
    See All Club Features 10+ seasons of experience
  • Venue Types

    • Sports Bars
    • Pubs & Restaurants
    • Entertainment Venues
    • Retail Partners

    Why Partner?

    Pay per check-in

    Simple per-visit pricing

    Quick approval

    Get listed in minutes

    More foot traffic

    Fans discover you via clubs

    See All Venue Features Partner with clubs & athletes
  • For Fans
  • Pricing
  • Learn

    • Blog
    • Use Cases
    • Guides & eBooks

    Support

    • Help Center
    • Developers
    • System Status
    • Contact Us
Sign In Start Engaging Free

Solutions

Fan Engagement Revenue Growth Community Building Data & Analytics

By Fan Base

LocalUp to 1K fans CityUp to 10K fans NationalUp to 100K fans GlobalUp to 1M fans

Resources

Earnings Calculator New Implementation Guide Developers

Venue Types

Sports Bars Pubs & Restaurants Entertainment Venues Retail Partners

Why Partner?

Pay per check-in
Quick approval
More foot traffic
See All Venue Features
For Fans Pricing

Learn

Blog Use Cases Guides & eBooks

Support

Help Center Developers System Status Contact Us
Start Engaging Free Sign In
Developers

Your club. Your app. Our engine.

The white-label API for clubs on the National and Global plans — register your club, generate your keys, and ship a fan app powered by the Game Set Engage platform.

Last updated: September 28, 2026

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

  1. Register your club — create your club account and complete onboarding.
  2. 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.
  3. Generate your keys — in your club dashboard open Club Management → API Access and press Generate API keys. If your club owner hasn't accepted the updated Club Agreement yet, the page asks them to first.

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 Authenticates your server-to-server calls (§4): verification emails, fan imports, password setup. 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. Generating keys, and rotating the secret, need your club owner's acceptance of the updated Club Agreement, given on the same page. It covers what your club takes on when it emails its fans and imports them. If your club already has keys, accepting it there also issues a new secret, so your servers need the new one.

A white-label integration needs a server, not just an app. In your app, Game Set Engage doesn't send your fans their verification emails: your club does, from its own systems, along with the password-setup emails for fans you import. Your server gets the tokens for those emails with your secret key (§4).

2. The basics

  • Production: https://api.gamesetengage.com/api/v1
  • Staging: https://dev.gamesetengage.com/api/v1 — a separate environment with its own accounts and data, so your production keys don't work there. Contact us if you want to test on it.
  • 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. Your server's calls to /server/… send the secret key as well (§4).

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 password reset, account deletion and fraud protection come built in (password-reset emails come from Game Set Engage). Verifying the fan's email is your club's job: you send that email (below).

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": "fan@example.com",
      "password": "aStrongPassword!",
      "first_name": "Alex",
      "last_name": "Carter"
    },
    "terms_accepted": true
  }'

We don't send the fan a verification email. In your app, your club sends it, and the response says so:

{
  "success": true,
  "message": "Registration successful. Please verify your email.",
  "data": {
    "user": { "unique_id": "usr_9f2ac1…", "email": "fan@example.com", "email_verified": false },
    "verification_required": true,
    "verification": { "sent_by": "club" }
  }
}

To verify the fan:

  1. Your app tells your server that the fan registered.
  2. Your server calls POST /server/verification_tokens with the fan's email (§4) and gets back { "token": "…", "expires_at": "…" }. The token lasts 24 hours.
  3. Your server emails the fan a link that carries the token, into your app or to your website.
  4. When the fan opens it, your app or site calls POST /auth/verify-email with X-Club-Key and { "token": "<token from the link>", "device_info": { … } }. That verifies the email and returns the JWT tokens in one step. On a website, make this call from your server: the API sends no CORS headers, so browsers block calls to it from your site's pages. The same goes for setting an imported fan's password with POST /auth/reset_password (§4).

For a "send it again" button, have your server ask for a new token (it replaces the old one) and send a new email. POST /auth/resend-verification sends nothing from your app: it answers 409 CLUB_SENDS_VERIFICATION, whatever the email. We don't send your fans a verification email from the Game Set Engage app either: only your club does.

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": "fan@example.com", "password": "aStrongPassword!" },
    "device_info": { "device_id": "3F2504E0-4F89-11D3-9A0C-0305E82C3301", "platform": "ios", "app_version": "1.0.0" }
  }'

Send device_info on login and on POST /auth/verify-email. Generate device_id once, on first launch, and keep it in the Keychain or Keystore so it survives app updates. The tokens are tied to it: you send it again to refresh, and it's how we know which phone is the fan's registered device (see below). If you leave it out we generate a random one that isn't returned to you, so your app has nothing to send when it refreshes.

{
  "success": true,
  "message": "Login successful",
  "data": {
    "user": {
      "unique_id": "usr_9f2ac1…",
      "email": "fan@example.com",
      "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" },
      "legal_updates": []
    },
    "tokens": {
      "access_token": "eyJhbGciOiJIUzI1NiJ9…",
      "refresh_token": "9f8c4e2b…",
      "expires_in": 900
    }
  }
}

Access tokens live 15 minutes. Refresh with POST /auth/refresh and { "refresh_token": "…", "device_id": "<the same device_id>" }. Send Authorization: Bearer <access_token> plus X-Club-Key on every call from here on.

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, left your club less than 90 days ago (the message says when it can come back), or is suspended, login returns 403 with 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.

One registered device per fan

A fan's account is registered to the first phone it signs in on. Taking part in a campaign, checking in at a venue and claiming a venue offer only work from that phone. From any other device they return 403 DEVICE_NOT_REGISTERED, with data.changes_remaining, data.change_quota and data.next_change_available_at.

GET /profile/device tells you whether the current phone is the registered one (bound_to_this_device) and how many changes are left. Offer a "Use this phone" button that calls POST /profile/device/rebind. A fan gets 2 changes in any 12 months; once they're used up, rebind returns 409 CHANGE_QUOTA_EXHAUSTED. A phone that is already registered to another account can't be taken over (422 DEVICE_TAKEN).

Legal updates

Fans who register in your app accept our Terms of Service, Privacy Policy and Fan Terms when they register (fans you import accept them later, below). When we change one of them in a way that needs their agreement again, or a fan has never accepted a document that applies to them, legal_updates lists what they owe. You get it on the user object from login and GET /auth/me, on GET /profile, and on the refresh response. It's an empty array when there's nothing to accept.

{
  "key": "terms",
  "title": "Terms of Service",
  "version": "2026-10-15",
  "reason": "updated",
  "effective_date": "2026-10-15",
  "required_from": "2026-10-15",
  "url": "https://api.gamesetengage.com/terms",
  "api_url": "https://api.gamesetengage.com/api/v1/legal/terms",
  "changes_intro": "…",
  "changes": [ { "what": "…", "why": "…" } ]
}

reason is updated (a new version) or not_yet_accepted (the fan never accepted any version: show the full text; changes_intro is null and changes is empty). changes_intro and any why can also be null on an update, and changes can be empty. Show the list of changes, each with its what and why, next to the full text (the item's api_url, GET /legal/:key, returns it as HTML in data.html), with an "I accept" button that calls POST /legal/accept. Send { "keys": ["terms"] } to accept particular documents, or an empty body to accept everything owed; a key that doesn't apply to the account returns 422 UNKNOWN_LEGAL_DOCUMENT. The response lists what was accepted and the updated legal_updates. We don't refuse any request while a document is owed (except for fans you imported, below), but show the prompt at every sign-in until the fan accepts.

Terms for imported fans

Fans you import (§4) haven't accepted our Terms of Service, Privacy Policy and Fan Terms: your club created the account. Until they accept all three, every signed-in request returns 403 TERMS_ACCEPTANCE_REQUIRED, with data.legal_updates listing what to accept, except these:

  • GET /auth/me and DELETE /auth/logout
  • GET /legal, GET /legal/:key and POST /legal/accept
  • GET /profile, and DELETE /profile, so the fan can always delete their account
  • POST /devices and DELETE /devices/unregister
  • GET and PUT /profile/notification_preferences, so the fan can turn notifications off before accepting

Sign-in, refresh, password setup and email verification work as usual, and the login response already carries legal_updates. So when a fan's legal_updates has not_yet_accepted items, show the acceptance screen straight after sign-in. Once POST /legal/accept has recorded all of them, everything opens up; accepting only some keeps the rest closed. Until then, the fan gets no push notifications you send from your dashboard (they aren't counted as reachable either), and no emails from us about updates to these documents. Fans who register in your app accept at registration, so this never applies to them.

4. Server-to-server

Three calls are for your servers only, because what they return lets someone verify an email or set a password: verification tokens, fan imports and password-setup tokens. Never make them from your app, and never give their tokens to your app directly: a token should reach your app or site only through the link the fan opens from your email. Anyone else holding it could verify an email they don't own, or take over the account.

Authentication

Send both keys, and no Authorization header:

X-Club-Key: gse_pk_XXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-Club-Secret: gse_sk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Refusal What it means
401 INVALID_CLUB_KEY The publishable key is missing or unknown.
401 INVALID_CLUB_SECRET The secret is missing or wrong (for example, one you've since rotated), or your keys were revoked. The publishable key on its own never gets in.
403 CLUB_PLAN_REQUIRED Your plan is below National.
403 CLUB_AGREEMENT_REQUIRED Your club owner hasn't accepted the updated Club Agreement. They accept it in Dashboard → API Access; if your club already has keys, that also issues a new secret. If the club gets a new owner, they need to accept it too.

Server calls are limited to 60 a minute per club key.

Verification tokens

curl -X POST https://api.gamesetengage.com/api/v1/server/verification_tokens \
  -H "Content-Type: application/json" \
  -H "X-Club-Key: gse_pk_XXXX" \
  -H "X-Club-Secret: gse_sk_XXXX" \
  -d '{ "email": "fan@example.com" }'
{ "success": true, "message": "Verification token issued", "data": { "token": "Zk3…", "expires_at": "2026-09-29T10:00:00Z" } }

This works for fans who signed up in your app, or whom you imported, and haven't verified yet. Each new token replaces the last one, so older links stop working, and it lasts 24 hours. Put it in a link to your app or site, which redeems it with POST /auth/verify-email (§3). Asking for a token also cancels any email-address change the fan had started.

  • 409 ALREADY_VERIFIED: the fan's email is already verified.
  • 404 NOT_FOUND for everyone else, whether the email belongs to another club's fan, to someone who signed up in the Game Set Engage app, or to no one. The answer is the same in every case. A fan who signed up in the Game Set Engage app verifies through our email; they can ask for a new one in the Game Set Engage app.

Import fans

Create accounts for fans you already have, up to 100 per request:

curl -X POST https://api.gamesetengage.com/api/v1/server/fans/import \
  -H "Content-Type: application/json" \
  -H "X-Club-Key: gse_pk_XXXX" \
  -H "X-Club-Secret: gse_sk_XXXX" \
  -d '{
    "fans": [
      { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace", "verification_needed": false },
      { "email": "alan@example.com", "first_name": "Alan", "last_name": "Turing" }
    ]
  }'

first_name and last_name are required. verification_needed is true if you leave it out. Set it to false only for an email you have already checked belongs to that fan: you are confirming that to us, and we record it.

You get one result per fan, in the order you sent them:

{
  "success": true,
  "message": "Import processed",
  "data": {
    "results": [
      {
        "email": "ada@example.com",
        "status": "created",
        "unique_id": "usr_…",
        "email_verified": true,
        "password_setup_token": "x7Q…",
        "password_setup_token_expires_at": "2026-09-28T16:00:00Z"
      },
      {
        "email": "alan@example.com",
        "status": "created",
        "unique_id": "usr_…",
        "email_verified": false,
        "verification_token": "Zk3…",
        "verification_token_expires_at": "2026-09-29T10:00:00Z",
        "password_setup_token": "Qm2…",
        "password_setup_token_expires_at": "2026-09-28T16:00:00Z"
      },
      { "email": "grace@example.com", "status": "exists" },
      { "email": "not-an-email", "status": "invalid", "errors": ["Email is invalid"] }
    ],
    "summary": { "created": 2, "exists": 1, "invalid": 1 },
    "daily_limit": { "limit": 1000, "remaining_today": 998 }
  }
}
  • created: a new account, already following your club. We send it no email. Nobody knows its password yet: email the fan a link with the password_setup_token, and when they choose a password your app or site calls POST /auth/reset_password with { "user": { "reset_password_token": "<the token>", "password": "…", "password_confirmation": "…" } }. That token lasts 6 hours. If verification_needed was true you also get a verification_token for their verification email (24 hours, as above).
  • exists: that email already has a Game Set Engage account, and we leave it exactly as it is: no password, name, verification or club change, and no tokens. The fan signs in to your app with their own password, and that sign-in adds your club; if they already follow three clubs, sign-in is refused until they unfollow one in the Game Set Engage app. You learn only that an account exists for the email; the Club Agreement lets you use that only to invite the fan to sign in.
  • invalid: errors says why, for example a malformed email, a missing name, a verification_needed that isn't true or false, or an email that appears twice in the same request.

An imported fan has to accept our terms the first time they sign in (see Terms for imported fans).

Limits. More than 100 fans in one request returns 400 TOO_MANY_FANS. Your club can create 1,000 accounts in any 24 hours; a request that would go past that creates nothing and returns 429 IMPORT_LIMIT_REACHED, with error.details.remaining_today telling you how many new fans you can still send. We check every fan first, and only the ones we would create count: fans that come back exists or invalid never cause the refusal.

Password-setup tokens

If a setup link expires before the fan uses it, ask for a new one:

POST /server/password_setup_tokens
{ "email": "ada@example.com" }
{ "success": true, "message": "Password setup token issued", "data": { "token": "x7Q…", "expires_at": "2026-09-28T16:00:00Z" } }

It works only for fans you imported who haven't set a password yet, and replaces the previous token. Anyone else gets the same 404 NOT_FOUND.

5. 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).

6. Participation cookbook — every campaign type

All nine 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 Authorization header, never the payload. An "empty" {} payload is only empty of campaign data — the JWT identifies the fan on every call.
  • location is required only for event_check_in and for campaigns pinned to a place. A campaign is pinned when its detail carries checkin_location, and the /campaigns/scan verdict then lists "location" in requires. Every other campaign, of any type, works without it but still accepts "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 (§7).

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 anywhere, or at a pinned place

The fan scans your campaign QR; your app calls POST /campaigns/scan with the QR payload to resolve the campaign, then participates. If the club pinned the campaign to a place, send location (the scan verdict's requires includes "location"): a fan outside the radius gets 422 You must be at the campaign location to participate. Without a pinned place the QR works anywhere and location is optional.

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. The result is pending until your club resolves the outcome after the event — points arrive with the resolution. (Know the answers upfront? That is a quiz campaign, below.) 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.

quiz — instant-scored answers

Same payload shape as survey and prediction. Every question ships with its correct answer (validated at creation), so the result comes back in the participate response: points_earned is always 0 and the reward is engagement_points per correct answer, paid instantly — on the fan's first participation only. Render from the result object; it is never pending for a quiz.

POST /campaigns/cmp_derby_quiz/participate
{ "survey_responses": { "0": "1965" } }

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 points · 422 Insufficient points. You have 60 of 60 points available 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.

7. 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 — your club's partner venues around the fan. Your app lists only venues your club has approved as partners, and only while they're active. A venue that works with other clubs doesn't appear until your club approves it too:

{
  "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", "latitude": 51.5521, "longitude": -0.1044 }

The fan has to be at the venue. Send latitude and longitude as top-level fields (not a location object). Further than 100 m from the venue returns 422 You appear to be too far from this venue to check in.

{
  "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. A fan can check in at each venue once a day; a second try the same day returns 422 You've already checked in at this venue today. Venue check-in points are paid once a day, whichever venue: a check-in confirmed at a second venue the same day still gets that venue's side deal, but points_awarded is 0. A venue's day runs from 06:00 to 06:00 in its local time, so a late match night counts as one day.

A venue can drop out for a while: when it has an overdue invoice, or when Game Set Engage suspends it. While it's out it disappears from venue lists and offers, and new check-ins and offer claims there return 422 This venue is not currently active. It reappears on its own once the invoice is settled or the suspension is lifted, and your app doesn't need to do anything.

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
{ "latitude": 51.5521, "longitude": -0.1044 }
{
  "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. As with check-ins, the fan must be within 100 m of the venue and send latitude and longitude. Most offers can be claimed once a day (422 You've already claimed this offer today); a venue can let fans claim on every visit, but points are paid once a day either way. Claiming outside the offer's weekdays returns 422 This offer isn't available today.

8. Profile & points

The profile object

GET /profile:

{
  "success": true,
  "data": {
    "unique_id": "usr_9f2ac1…",
    "email": "fan@example.com",
    "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.

9. 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_CONTENT", "message": "You have reached the participation limit for 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.
TERMS_ACCEPTANCE_REQUIRED 403 A fan you imported hasn't accepted our terms yet. Show the acceptance screen from data.legal_updates, call POST /legal/accept, then retry (§3).
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 and have your server send a new link (§4).
ACCOUNT_SUSPENDED 403 Fraud-blocked account — show the message with your support link.
DEVICE_NOT_REGISTERED 403 Not the fan's registered phone. Offer "Use this phone" (POST /profile/device/rebind) and show data.changes_remaining.
CHANGE_QUOTA_EXHAUSTED 409 On rebind: both device changes for the last 12 months are used. data.next_change_available_at says when the next one frees up.
DEVICE_TAKEN 422 On rebind: this phone is already registered to another account. Show the message as-is.
FORBIDDEN (on login) 403 The 3-club cap, a fan who left your club less than 90 days ago, or a suspended account. 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_CONTENT 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 · INVALID_CONTENT_TYPE 400 Malformed request (missing param, unreadable JSON), or a POST, PUT or PATCH without Content-Type: application/json.
CLUB_SENDS_VERIFICATION 409 POST /auth/resend-verification from your app. Your server sends verification emails (§4). Not a message for the fan.

Your server's calls (§4) have their own codes. These are for your logs and alerts, not for fans:

Code Status What to do
INVALID_CLUB_SECRET 401 The secret is missing or wrong, or your keys were revoked. Use the current secret from your secret manager.
CLUB_AGREEMENT_REQUIRED 403 Your club owner needs to accept the updated Club Agreement in Dashboard → API Access. With existing keys, that issues a new secret: update your servers.
ALREADY_VERIFIED 409 The fan's email is already verified; no email needed.
NOT_FOUND 404 Verification tokens: not a fan who signed up in your app or whom you imported. Setup tokens: not a fan you imported who still has to set a password.
TOO_MANY_FANS 400 More than 100 fans in one import. Split the batch.
IMPORT_LIMIT_REACHED 429 The import would pass 1,000 new accounts in 24 hours. Nothing was created. Send at most error.details.remaining_today new fans, or wait.

Concrete 422 messages you will meet in the wild — all display-ready:

You have reached the participation limit for 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 points
Insufficient points. You have 60 of 60 points available for this club.
Survey responses are required
Location is required to check in at a venue
You appear to be too far from this venue to check in
You've already checked in at this venue today
This venue is not currently active
This offer isn't available today
Location is required to claim an offer at a venue
You appear to be too far from this venue to claim this offer
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, and server calls (§4) are limited to 60 a minute per club key. A 429 from these limits comes from the edge with a plain body (error, message, retry_after seconds) — back off and retry after the given delay. 429 IMPORT_LIMIT_REACHED is different: it uses the usual error envelope, and retrying the same batch straight away won't help.

10. Launch checklist

Setup

  1. Generate keys in Dashboard → API Access (your club owner accepts the updated Club Agreement there); embed gse_pk_… in the app, vault gse_sk_… on your servers.
  2. Send X-Club-Key on every request — including register and login. Your server sends X-Club-Secret too, and nothing else ever does.
  3. If you test on staging (dev.gamesetengage.com), use the staging key we give you there; your production key only works on production.

Auth flows to test end-to-end

  1. Register → your server gets a verification token and emails the link → the link opens your app or site, which calls POST /auth/verify-email → login → refresh loop (tokens live 15 minutes — refresh proactively, not on failure only). Send the same stored device_id on verify, login and every refresh. Test the "send it again" path and an expired link.
  2. 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).
  3. Register the push token after every successful login; unregister on logout. Test an account switch on one device — pushes must follow.
  4. Sign the same fan in on a second phone: expect DEVICE_NOT_REGISTERED on participation, then move the account with "Use this phone".
  5. Build the legal_updates screen and wire it to POST /legal/accept. A new account owes nothing, so test the screen with a stubbed response, or with an imported fan, who owes all three documents.

Imports (if you bring existing fans)

  1. Import a small batch on staging: one fan with verification_needed: false, one without, and one email that already has an account (expect exists). Email the setup link, set a password through POST /auth/reset_password, sign in, and expect TERMS_ACCEPTANCE_REQUIRED until the fan accepts.
  2. Handle IMPORT_LIMIT_REACHED by sending smaller batches later, and alert on INVALID_CLUB_SECRET and CLUB_AGREEMENT_REQUIRED.

Campaign flows to test per type you'll run

  1. One participation per type from §6, plus the repeat attempt (expect the friendly 422), the empty deal-code pool, and — for geofenced types — a check-in from outside the radius.
  2. Send optional location wherever the fan has granted permission — it costs nothing and feeds your analytics.

Venue flows (if you use the venue network)

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

  1. Wire DELETE /profile (account deletion) into settings — app-store rules require it.
  2. Make sure error sheets show error.message verbatim and quote meta.request_id in your support links.
  3. Switch the base URL and keys to production, do one full smoke pass, ship.

Questions, or a capability you're missing? Contact us — we answer fast.

Game Set Engage

We help sports clubs know their fans, reward the ones who show up, and prove it to sponsors. Free to start.

Solutions

  • For Clubs
  • For Venues
  • For Fans
  • Fan Engagement
  • Revenue Growth
  • Community Building
  • Data & Analytics

Industries

  • College Athletics
  • Small Programs (D2/D3)
  • Sports Bars
  • Pubs & Restaurants
  • Entertainment
  • Retail Partners
  • Local Councils

Product

  • Pricing
  • Developers
  • naim AI
  • Use Cases
  • System Status

Resources

  • Fan Engagement Platform
  • What Is Fan Engagement?
  • Sports Fundraising Ideas
  • Blog
  • Compare
  • Guides & eBooks
  • The Data-Driven Fan
  • Help Center

Company

  • About Us
  • Careers
  • Contact
  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • GDPR
  • Delete your account
© 2026 Game Set Engage Ltd. All rights reserved.
Join Now! It’s Free

We use cookies to measure site performance and improve your experience. Cookie Policy.