# Sporfie Public API — guide for AI agents

You are reading the agent-oriented entry point for the Sporfie Public API. It is one
self-contained document: everything needed to write a working integration is on this page, so
you should not need to crawl further.

- Machine-readable spec: **https://docs.sporfie.com/openapi.json** (OpenAPI 3.0, declares
  `bearerAuth`, so code generators emit the auth header correctly)
- Human reference: **https://docs.sporfie.com/**
- This page: **https://docs.sporfie.com/agents**

If you were given an API token and asked to "integrate with Sporfie", read the whole of
[Rules the OpenAPI spec does not tell you](#rules-the-openapi-spec-does-not-tell-you) before
writing code. Every item there is a failure that is easy to hit and confusing to debug.

---

## Facts to load first

| | |
|---|---|
| Base URL | `https://api.sporfie.com` |
| Auth | `Authorization: Bearer {token}` on every request |
| Format | JSON in, JSON out |
| Rate limit | ~1 request/second per token by default → `429` |
| All times | UTC, **milliseconds** since the Unix epoch, as numbers |
| Where secrets live | Server side only. Never in a browser, mobile app, or repo |
| Scope of a personal token | The organizations the token's owner **administers** |

---

## Prefer the MCP server if your client supports it

If you are an MCP-capable agent (Claude Code, Claude apps, Cursor, …) you do not need to write
HTTP calls at all: Sporfie ships an MCP server that wraps this API as 19 curated tools.

| | |
|---|---|
| Endpoint | `https://mcp.sporfie.com/mcp` (Streamable HTTP) |
| Auth | the same `Authorization: Bearer {token}` header, sent with every request |
| Tools | events create/read/update/close/delete · search & live lookup · places & sports · highlight moments · event webhooks |
| Support | `search_help_center` / `get_help_article` / `list_help_center_sections` — no token needed |

Claude Code:

```bash
claude mcp add --transport http sporfie https://mcp.sporfie.com/mcp \
  --header "Authorization: Bearer $SPORFIE_TOKEN"
```

Everything on this page stays true through MCP — same identifiers, same token scope, same rate
limits — because every tool call is the corresponding REST call underneath.

---

## Authentication

Every request carries the token:

```
Authorization: Bearer eyJhbGciOi...
```

A user creates a token at **https://sporfie.com/settings/developer**. The secret is shown
**once**, at creation — it cannot be retrieved later, only revoked and replaced.

**Never call this API from client-side code.** The token grants everything its owner can do
across their organizations. It belongs in a server-side environment variable or secret manager.
If you are generating code, read it from the environment; do not inline it, and do not write it
into a file that could be committed.

### Verify the token before doing anything else

```bash
curl -sS -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer $SPORFIE_TOKEN" \
  https://api.sporfie.com/public/sports
```

`200` means the token is live. `401` means it is wrong, revoked, or expired. Do this first: it
turns "my integration is broken" into a one-line answer.

---

## The domain in four objects

- **Organization** (called `company` in the API, `companyKey` in payloads) — the account that
  owns places and events. Almost every call is scoped to one.
- **Place** — a physical location that records: a court, a field, a rink. One event at a time.
- **Event** — one occurrence of an activity at a time and location: a match, a training session.
  Events may be attached to a Place (and then get cameras and streams) or exist standalone.
- **Moment** — a captured highlight inside an Event, with one or more video clips.

Two identifiers matter and are easy to confuse:

- **`eventKey`** — Sporfie's internal id, returned when you create an event.
- **`externalID`** — **your** id, which you choose. You set it as the path segment when creating
  an event, and afterwards either id works anywhere the docs say `eventKey`.

Using your own ids is the recommended pattern: your system stays the source of truth and you
never have to store a mapping table.

---

## What a token can reach

A personal token acts as its owner. It can operate on the organizations where that user is an
**admin or owner** — not organizations where they are merely a member, and not organizations
they have no relationship with.

A token may additionally be **restricted to specific organizations** at creation. That is a
narrowing, never a grant: the server checks the restriction *and* the owner's current
membership on every call. Two consequences worth designing for:

- The token you were given may reach fewer organizations than its owner administers. Never
  assume a `companyKey` will work because the person who gave you the token can see it —
  handle `403` per organization.
- Access can disappear without the token changing. If the owner leaves an organization, calls
  for it start returning `403` while everything else keeps working. That is expected, not a
  broken token; do not retry it, and do not treat it as an auth failure.

There is no per-endpoint scoping: within the organizations it can reach, the token can do
anything its owner can do. Treat it as a full credential.

If an integration needs to manage organizations the owner does not administer, a personal token
is the wrong tool — that requires a Sporfie-issued integration key. Tell the user to contact
Sporfie support rather than trying to work around it.

---

## Recipes

Every example assumes `SPORFIE_TOKEN` and `COMPANY_KEY` are set in the environment.

### Create an event

The path segment is **your** id for the event. The response gives you Sporfie's.

```bash
curl -sS -X POST "https://api.sporfie.com/public/events/my-match-2026-08-19" \
  -H "Authorization: Bearer $SPORFIE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "companyKey": "'"$COMPANY_KEY"'",
    "name": "Under-16 semi-final",
    "sport": "volleyball",
    "location": { "name": "Municipal Hall", "geoLoc": { "lat": 50.8466, "lng": 4.3528 } }
  }'
```

```json
{ "eventKey": "-Nq7xK2mBv9pL3aRt8Zc" }
```

`companyKey` and `name` are always required. `location` is required **unless** you pass
`placeKey`. See the pitfalls section before adding scheduling fields.

### Create a scheduled event on a Place

Scheduling only exists for events attached to a Place. Find the place first:

```bash
curl -sS -H "Authorization: Bearer $SPORFIE_TOKEN" \
  "https://api.sporfie.com/public/places?companyKey=$COMPANY_KEY"
```

then create with all three fields together:

```json
{
  "companyKey": "...",
  "name": "Court 1 — evening session",
  "placeKey": "-NpQ...",
  "scheduledStartTime": 1755600000000,
  "scheduledEndTime": 1755607200000
}
```

Both times must be in the future, and the end must be at least **5 minutes** after the start.
`location` may be omitted — it is inherited from the Place.

### Find events

```bash
# by organization and state: future | current | past
curl -sS -H "Authorization: Bearer $SPORFIE_TOKEN" \
  "https://api.sporfie.com/public/event-search-by-company?companyKey=$COMPANY_KEY&state=future&pageSize=20&page=0"

# what is running on a place right now
curl -sS -H "Authorization: Bearer $SPORFIE_TOKEN" \
  "https://api.sporfie.com/public/places/$PLACE_KEY/activeEventKey"
```

`event-search-by-company` is paginated and returns a trimmed representation; fetch
`GET /public/events/{eventKey}` for the full object. Ordering depends on state: `future`
ascending by scheduled start, `past` descending by start.

### Push a score

```bash
curl -sS -X POST "https://api.sporfie.com/public/events/my-match-2026-08-19/score" \
  -H "Authorization: Bearer $SPORFIE_TOKEN" -H "Content-Type: application/json" \
  -d '{ "timeStamp": 1755600123456, "left": 12, "right": 9, "period": 2 }'
```

`timeStamp` is required. `left`/`right`/`period` are the generic shape; some sports carry a
richer `scoreValue` (tennis uses `gameScores` + `pointScore`, volleyball uses `gameScores`).
`timeStamp` and `key` are reserved names inside the score object.

You can also post to `POST /public/places/{placeKey}/score`, which routes to whatever event is
running there. **If nothing is running, the score is silently dropped** and you get a `warning`
field instead of keys — check for it.

### Capture a highlight

```bash
curl -sS -X POST "https://api.sporfie.com/public/events/my-match-2026-08-19/clicks" \
  -H "Authorization: Bearer $SPORFIE_TOKEN" -H "Content-Type: application/json" \
  -d '{ "timeStamp": 1755600123456 }'
```

Returns a `momentKey`. A `200` means the request was **accepted**, not that video exists —
clips take seconds to generate. Poll `GET /public/moments/{momentKey}` if you need to know when
they are ready.

While an event is open only the **last minute** of footage can be clipped, so call this close to
the action. Once an event has ended, the whole recording is available.

### React to changes with a webhook

```bash
curl -sS -X PUT "https://api.sporfie.com/public/events/my-match-2026-08-19/watch" \
  -H "Authorization: Bearer $SPORFIE_TOKEN" -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/sporfie", "contentType": "json" }'
```

Both `url` and `contentType` are required, and `json` is the only supported content type.
Registering again replaces the previous webhook rather than adding a second one.

Sporfie then POSTs the full event object to your URL on every change. **The registration expires
after 12 hours** — re-register on a schedule or you will silently stop receiving events. `DELETE`
the same path to stop.

Prefer this over polling.

---

## Rules the OpenAPI spec does not tell you

These are the failures that cost the most time. Read them before writing code.

1. **`scheduledStartTime` / `scheduledEndTime` are rejected when there is no `placeKey`.** The
   prose reads as if they are generally optional; the server rejects them outright on a
   non-place event with *"scheduledStartTime and scheduledEndTime cannot be set for a non-place
   event"*. Either pass a `placeKey` and both times, or neither.

2. **Both scheduling times move together.** When present, both are required, both must be in the
   future, and the end must be ≥ 5 minutes after the start.

3. **`companyKey` is what authorizes the call**, not just metadata. A token that cannot act on
   that organization gets `403`, no matter how valid it is.

4. **The path segment on create is yours, not Sporfie's.** `POST /public/events/{X}` sets the
   event's `externalID` to `X`. It must match `[-_A-Za-z0-9]+`, be at most 64 bytes, and be
   unique for your token.

5. **Reusing an `externalID` is not idempotent.** If the event is still open you get `409`. If it
   was closed, the call **reopens that event** rather than creating a new one. Generate a fresh
   id per event unless reopening is what you want.

6. **`location` requires both `name` and `geoLoc`**, and `geoLoc` requires both `lat` and `lng`.
   A partial location is a `400`.

7. **Time is milliseconds, UTC.** Seconds-precision timestamps are a very common bug here and
   produce events dated 1970.

8. **`sport` must come from `GET /public/sports`.** An unknown code is a `400`. Omit it and you
   get the Place's sport, or `other`.

9. **Rate limiting is per token at about 1 request/second.** Bulk work needs pacing and a retry
   on `429` — do not parallelize a loop of creates.

10. **Score posted to a place with no active event is dropped, with a `200`.** The response
    carries `warning` instead of `eventKey`/`key`. Silent data loss if unchecked.

11. **Accepted ≠ done** for clicks and video generation. Poll if you need the artifact.

12. **Event watches expire after 12 hours.** Re-register.

13. **`PATCH /public/events/{eventKey}` has no required fields, whatever the spec says.** It
    reuses the `EventCreate` schema, so `openapi.json` lists `companyKey` and `name` as required
    and generated clients will demand them. The server does not: it updates only the fields you
    send. Send just what you want to change — resending `name` on a PATCH will overwrite the
    event's name.

---

## Errors

| Status | Meaning | What to do |
|---|---|---|
| `400` | A field is missing or invalid | Read the `errors` array; it names the field |
| `401` | Token missing, revoked, or expired | Verify with `GET /public/sports`; re-issue if needed |
| `403` | Token may not act on that `companyKey` | The owner must administer it **and** the token must not be restricted away from it |
| `404` | Object does not exist, or is not visible to this token | Check the key, then the scope |
| `409` | `externalID` already in use | Use a new id |
| `417` | Event still running | Close or end it before deleting |
| `429` | Rate limited | Back off and retry; ~1 req/s |

Do not retry `400`, `403`, `404` or `409` — they will never succeed unchanged. Retry `429` with
backoff, and `5xx` a few times.

---

## Building an integration

A workable order of operations:

1. **Verify** the token with `GET /public/sports`.
2. **Resolve the organization.** Ask the user for their `companyKey` (visible on their API tokens
   settings page) rather than guessing.
3. **Generate a typed client** from `https://docs.sporfie.com/openapi.json`. It declares
   `bearerAuth`, so generators wire the header for you.
4. **Read the token from the environment.** Never inline it, never commit it, never send it to
   the browser.
5. **Map your ids onto `externalID`** so your system stays the source of truth.
6. **Centralize the HTTP layer**: one wrapper handling the auth header, `429` backoff, and error
   decoding. Every recipe above goes through it.
7. **Use webhooks, not polling**, and refresh the registration inside 12 hours.
8. **Never log the token**, including in error paths.

If a call fails and you cannot tell whether it is auth, scope, or payload: check `401` vs `403`
first — that single distinction separates "bad token" from "wrong organization", and they have
completely different fixes.

## Every endpoint

Generated from the OpenAPI spec (1.7.1). Full parameter and response detail is in
[openapi.json](https://docs.sporfie.com/openapi.json).

### Place data

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/public/places` | Places |
| `POST` | `/public/places/{placeKey}/score` | Add a score entry in the score stream |

### Event data

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/public/event-lookup-by-place-and-time` | Event lookup by place and time |
| `GET` | `/public/places/{placeKey}/activeEventKey` | Active Event on Place |
| `GET` | `/public/event-search-by-company` | Event search by company/organization |
| `GET` | `/public/events/{eventKey}` | Event data |
| `POST` | `/public/events/{eventKey}` | Create event |
| `PATCH` | `/public/events/{eventKey}` | Modify event |
| `DELETE` | `/public/events/{eventKey}` | Delete event |
| `POST` | `/public/events/{eventKey}/close` | Close an event |
| `POST` | `/public/events/{eventKey}/score` | Add a score entry in the score stream |
| `DELETE` | `/public/events/{eventKey}/score/{scoreKey}` | Delete a score entry |

### Event Watch

| Method | Path | Purpose |
|---|---|---|
| `PUT` | `/public/events/{eventKey}/watch` | Watch an event |
| `DELETE` | `/public/events/{eventKey}/watch` | Stop watching an event |

### Video generation

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/public/overlay-preview` | Video Overlay Preview |
| `POST` | `/public/events/{eventKey}/clicks` | Click |

### Moment data

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/public/moments/{momentKey}` | Moment |
| `PATCH` | `/public/moments/{momentKey}` | Update moment data |
| `DELETE` | `/public/moments/{momentKey}` | Delete a Moment |
| `DELETE` | `/public/moments/{momentKey}/clips/{clipKey}` | Delete a Video Clip |

### Bookmark management

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/public/bookmark-collections/events/{eventKey}` | Bookmark Collections of Event |
| `POST` | `/public/bookmark-collections/events/{eventKey}` | Create Bookmark Collection for Event |
| `GET` | `/public/bookmark-collections/events/{eventKey}/{userID}` | Bookmark Collections of Event by User |
| `GET` | `/public/bookmark-collections/media-files/{mediaFileKey}` | Bookmark Collections of Media File |
| `POST` | `/public/bookmark-collections/media-files/{mediaFileKey}` | Create Bookmark Collection for Media File |
| `GET` | `/public/bookmark-collections/media-files/{mediaFileKey}/{userID}` | Bookmark Collections of Media File by User |
| `GET` | `/public/bookmark-collections/{bookmarkCollectionKey}` | Bookmark Collection |
| `PUT` | `/public/bookmark-collections/{bookmarkCollectionKey}` | Update Bookmark Collection |
| `DELETE` | `/public/bookmark-collections/{bookmarkCollectionKey}` | Delete Bookmark Collection |
| `GET` | `/public/bookmark-collections/{bookmarkCollectionKey}/shortURL` | Bookmark Collection Short URL |
| `POST` | `/public/bookmark-collections/{bookmarkCollectionKey}/export` | Export Bookmark Collection by Tag |
| `DELETE` | `/public/bookmark-collections/{bookmarkCollectionKey}/export/{exportKey}` | Delete Bookmark Collection Export |
| `GET` | `/public/bookmark-collections/{bookmarkCollectionKey}/bookmarks/{bookmarkKey}/shortURL` | Bookmark Short URL |
| `POST` | `/public/bookmark-collections/{bookmarkCollectionKey}/bookmarks` | Create Bookmark |
| `PUT` | `/public/bookmark-collections/{bookmarkCollectionKey}/bookmarks/{bookmarkKey}` | Update Bookmark |
| `DELETE` | `/public/bookmark-collections/{bookmarkCollectionKey}/bookmarks/{bookmarkKey}` | Delete Bookmark |

### Reel management

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/public/companies/{companyKeyOrSlug}/reels` | Reels of a Company/Organization |
| `POST` | `/public/companies/{companyKeyOrSlug}/reels` | Create a Reel |
| `GET` | `/public/companies/{companyKeyOrSlug}/reels/{reelKey}` | Reel |
| `PUT` | `/public/companies/{companyKeyOrSlug}/reels/{reelKey}` | Update a Reel |
| `DELETE` | `/public/companies/{companyKeyOrSlug}/reels/{reelKey}` | Delete a Reel |
| `POST` | `/public/companies/{companyKeyOrSlug}/reels/{reelKey}/convertToVideoFile` | Convert a Reel to a video file |

### Scope data

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/public/scopes/{scopeKey}/companies` | Companies of a Scope |

### Sport data

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/public/sports` | Sports |


## Request bodies at a glance

Required fields for every operation that takes a JSON body. Sending a body without these
returns `400`.

| Method | Path | Required | Also accepts |
|---|---|---|---|
| `POST` | `/public/places/{placeKey}/score` | `timeStamp` | — |
| `POST` | `/public/events/{eventKey}` | `companyKey`, `name` | `description`, `notSearchable`, `placeKey`, `scheduledStartTime`, `scheduledEndTime`, `thumbnailURL`, `homeTeam`, `awayTeam`, `sport`, `pinCode`, `cameraPinCode`, `location`, … |
| `PATCH` | `/public/events/{eventKey}` | — *(partial update)* | `companyKey`, `name`, `description`, `notSearchable`, `placeKey`, `scheduledStartTime`, `scheduledEndTime`, `thumbnailURL`, `homeTeam`, `awayTeam`, `sport`, `pinCode`, … |
| `POST` | `/public/events/{eventKey}/score` | `timeStamp` | — |
| `PUT` | `/public/events/{eventKey}/watch` | `url`, `contentType` | — |
| `POST` | `/public/overlay-preview` | `overlays` | `sourceURL` |
| `POST` | `/public/events/{eventKey}/clicks` | `timeStamp` | `startTime`, `endTime`, `score`, `overlays`, `players`, `metadata` |
| `PATCH` | `/public/moments/{momentKey}` | — *(partial update)* | `score`, `players`, `metadata` |
| `POST` | `/public/bookmark-collections/events/{eventKey}` | `name`, `isPublic` | — |
| `POST` | `/public/bookmark-collections/media-files/{mediaFileKey}` | `name`, `isPublic` | — |
| `PUT` | `/public/bookmark-collections/{bookmarkCollectionKey}` | `name`, `isPublic` | — |
| `POST` | `/public/bookmark-collections/{bookmarkCollectionKey}/export` | `name`, `tag`, `mediaFileKey` | — |
| `POST` | `/public/bookmark-collections/{bookmarkCollectionKey}/bookmarks` | `startTime` | `endTime`, `tag`, `comment`, `thumbnailBase64` |
| `PUT` | `/public/bookmark-collections/{bookmarkCollectionKey}/bookmarks/{bookmarkKey}` | — | `startTime`, `endTime`, `tag`, `comment`, `thumbnailBase64` |
| `POST` | `/public/companies/{companyKeyOrSlug}/reels` | `name` | `eventKey`, `thumbnail`, `thumbnailURL`, `playlist` |
| `PUT` | `/public/companies/{companyKeyOrSlug}/reels/{reelKey}` | `name` | `eventKey`, `thumbnail`, `thumbnailURL`, `playlist` |


## Sport codes

Valid values for an event's `sport`. Also available live at `GET /public/sports`.

`ballhockey` · `baseball` · `basketball` · `beachvolleyball` · `billiard` · `bowling` · `cheer` · `cricket` · `dekhockey` · `football` · `frontenis` · `futsal` · `golf` · `handball` · `hockey` · `lacrosse` · `other` · `padel` · `pickleball` · `pongbeyond` · `rollerderby` · `rugby` · `soccer` · `softball` · `swimming` · `tennis` · `unihockey` · `volleyball` · `waterpolo` · `wrestling`


---

*Generated from the Sporfie OpenAPI spec, version 1.7.1. The spec is the
source of truth: when this page and [openapi.json](https://docs.sporfie.com/openapi.json) disagree,
believe the spec and please report the drift.*

